feat(ai): record AI resource consumption per backend call with Matrix sync#3403
Conversation
… 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.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR adds AI consumption tracking end to end: new impact and event models, persistence and sync plumbing, inference impact collection, workflow attribution, and ChangesAI Consumption Tracking
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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.
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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 winConsumption 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_recordConsumptionisn't reached until Line 754 — aftertaskEntityis re-fetched at Line 743 and validated at Line 746. If the linked task is deleted between kicking off generation and this fetch, theStateErrorat Line 747-750 propagates and the consumption/cost event for the already-completed call is silently lost (unlikerunTranscription/runImageAnalysis/runPromptGeneration, where the source entity is fetched before the inference call).Consider recording consumption immediately after
generateImagereturns, before the post-generation task-existence check, so a downstreamStateErrordoesn'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 winCapture the consumption IDs in
_ConversationHarness.sendMessage.
sendMessageCallsstill records onlyinferenceRepo,message,model,toolChoice, andtools, so the newconsumptionAgentId/consumptionWakeRunKey/consumptionThreadIdarguments 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 valueConsider Freezed for consistency.
MeliousCallImpacthand-rolls==/hashCode/toStringwhile the siblingAiConsumptionEventmodel 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 winConsider 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/_nonStreamingChatpath (used whenever animpactCollectoris supplied) doesn't. Given the increased_chatCompletionTimeoutand its role in cost/impact tracking correctness, aDuration.zerotimeout 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
_asIntduplicates the pre-existing_integerValuehelper.Both are private static methods with identical bodies (
int/num/String→ nullableintcoercion). 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 | 🔵 TrivialNon-streaming impact path suppresses progress updates for the full call duration (up to 300s).
_nonStreamingChatyields nothing until the whole response (or the 300s timeout) resolves, soonProgressinUnifiedAiInferenceRepositorywon'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 capturingenvironment_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 winMove the new fallback registration out of the test body, matching this file's own convention.
Every other
registerFallbackValue(...)call in this suite lives insetUpAll()(lines 106-124); this one is registered inline inside a single test. Consider movingregisterFallbackValue(consumption.makeConsumptionEvent())tosetUpAll()(or centralizing it intest/helpers/fallbacks.dartper convention) for consistency.As per path instructions: "Centralize fallback values in
test/helpers/fallbacks.dartand register withregisterFallbackValue(...)."🤖 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 valueMinor inconsistency:
impactCollectorisn't captured like the other parameters.Every other parameter here is recorded into a
lastXfield (e.g.lastSignatureCollector), butimpactCollectoris accepted and dropped. Harmless today since no test asserts on it, but consider addinglastImpactCollectorfor 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 | 🔵 TrivialDuplicated 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 inrunTranscription,runImageAnalysis, andrunPromptGeneration. 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 valueConsider
uuid.v4()instead ofuuid.v1()for the consumption event id.
uuid.v1()embeds a timestamp and a node identifier that (per theuuidpackage'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 viacreatedAt: 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 customnodeoption 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 winOFFSET-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
idand queryWHERE 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 winDrop the unused vector-clock query or route callers through it.
getConsumptionEventVectorClockByIdisn’t referenced in source, whileConsumptionRepository.getVectorClockstill uses the samejson_extract(... '$.vectorClock')SQL inline.ConsumptionDatabase._extractVectorClockis 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 winConsider
clock.now()instead of rawDateTime.now()for turn timing.
turnStart(line 283) and thedurationMscomputation in_recordTurnConsumption(line 495) use rawDateTime.now(), while sibling workflow files in this same PR (e.g.event_agent_workflow.dart'sfinal now = clock.now();) usepackage:clockfor testability. SincedurationMs/createdAtare 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 winCentralize the consumption-event fallback value instead of registering it inline.
registerFallbackValue(makeConsumptionEvent())is registered directly in this test'ssetUpAll, alongside the sharedregisterSyncProcessorFallbacks()helper. As per path instructions, fallback values should be centralized intest/helpers/fallbacks.dartrather than declared ad hoc per test file.♻️ Suggested consolidation
- setUpAll(() { - registerSyncProcessorFallbacks(); - registerFallbackValue(makeConsumptionEvent()); - }); + setUpAll(registerSyncProcessorFallbacks);And add the
AiConsumptionEventfallback registration insideregisterSyncProcessorFallbacks()intest/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 valueCache the
isRegisteredcheck once.
getIt.isRegistered<AiConsumptionRecorder>()is evaluated twice for the same two fields.day_agent_workflow.dartin this same PR caches this check into a local (recordConsumption) and reuses it — worth mirroring here (and intemplate_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 winForward the consumption IDs into
sendMessageDelegate.MockConversationRepository.sendMessageaccepts the new consumption fields, but the delegate type and call still drop them, so tests using this helper can’t assert the ownership IDs thatTaskAgentWorkflowpasses 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
📒 Files selected for processing (90)
lib/features/agents/workflow/event_agent_workflow.dartlib/features/agents/workflow/project_agent_execute.dartlib/features/agents/workflow/project_agent_workflow.dartlib/features/agents/workflow/soul_evolution_workflow.dartlib/features/agents/workflow/task_agent_execute.dartlib/features/agents/workflow/task_agent_workflow.dartlib/features/agents/workflow/template_evolution_session.dartlib/features/agents/workflow/template_evolution_workflow.dartlib/features/ai/conversation/conversation_repository.dartlib/features/ai/model/ai_call_impact.dartlib/features/ai/repository/ai_consumption_mapping.dartlib/features/ai/repository/cloud_inference_generate.dartlib/features/ai/repository/cloud_inference_generate_more.dartlib/features/ai/repository/cloud_inference_repository.dartlib/features/ai/repository/cloud_inference_wrapper.dartlib/features/ai/repository/inference_repository_interface.dartlib/features/ai/repository/melious_inference_repository.dartlib/features/ai/repository/ollama_inference_repository.dartlib/features/ai/repository/unified_ai_inference_repository.dartlib/features/ai/services/skill_inference_runner.dartlib/features/ai_consumption/README.mdlib/features/ai_consumption/consumption/ai_consumption_recorder.dartlib/features/ai_consumption/database/consumption_database.dartlib/features/ai_consumption/database/consumption_database.driftlib/features/ai_consumption/database/consumption_database.g.dartlib/features/ai_consumption/database/consumption_db_conversions.dartlib/features/ai_consumption/logic/consumption_bucketing.dartlib/features/ai_consumption/model/ai_consumption_enums.dartlib/features/ai_consumption/model/ai_consumption_event.dartlib/features/ai_consumption/model/ai_consumption_event.freezed.dartlib/features/ai_consumption/model/ai_consumption_event.g.dartlib/features/ai_consumption/model/consumption_aggregation_models.dartlib/features/ai_consumption/repository/consumption_repository.dartlib/features/ai_consumption/sync/consumption_sync_service.dartlib/features/daily_os_next/agents/workflow/day_agent_workflow.dartlib/features/sync/backfill/backfill_response_builders.dartlib/features/sync/backfill/backfill_response_handler.dartlib/features/sync/matrix/matrix_service.dartlib/features/sync/matrix/sync_event_processor.dartlib/features/sync/matrix/sync_event_processor_apply.dartlib/features/sync/matrix/sync_event_processor_consumption_handlers.dartlib/features/sync/model/sync_message.dartlib/features/sync/model/sync_message.freezed.dartlib/features/sync/model/sync_message.g.dartlib/features/sync/outbox/outbox_enqueue_writer.dartlib/features/sync/outbox/outbox_enqueue_writer_simple.dartlib/features/sync/outbox/outbox_scheduling.dartlib/features/sync/outbox/outbox_service.dartlib/features/sync/queue/queue_apply_adapter.dartlib/features/sync/sequence/sync_sequence_payload_type.dartlib/features/sync/ui/view_models/outbox_list_item_view_model.dartlib/get_it.dartlib/l10n/app_cs.arblib/l10n/app_de.arblib/l10n/app_en.arblib/l10n/app_es.arblib/l10n/app_fr.arblib/l10n/app_localizations.dartlib/l10n/app_localizations_cs.dartlib/l10n/app_localizations_de.dartlib/l10n/app_localizations_en.dartlib/l10n/app_localizations_es.dartlib/l10n/app_localizations_fr.dartlib/l10n/app_localizations_ro.dartlib/l10n/app_ro.arblib/services/db_notification.darttest/features/agents/workflow/soul_evolution_workflow_test.darttest/features/agents/workflow/task_agent_workflow_test_helpers.darttest/features/agents/workflow/template_evolution_workflow_test.darttest/features/ai/conversation/conversation_repository_test.darttest/features/ai/eval/local_task_agent_inference_eval_test.darttest/features/ai/eval/qwen_local_inference_eval_test.darttest/features/ai/model/ai_call_impact_test.darttest/features/ai/repository/cloud_inference_generate_more_test.darttest/features/ai/repository/cloud_inference_generate_test.darttest/features/ai/repository/inference_repository_interface_test.darttest/features/ai/repository/melious_inference_repository_test.darttest/features/ai/repository/unified_ai_inference_repository_test.darttest/features/ai/services/skill_inference_runner_test.darttest/features/ai_consumption/database/consumption_database_test.darttest/features/ai_consumption/logic/consumption_bucketing_test.darttest/features/ai_consumption/repository/consumption_repository_test.darttest/features/ai_consumption/sync/consumption_sync_service_test.darttest/features/ai_consumption/test_utils.darttest/features/daily_os_next/agents/workflow/day_agent_workflow_test.darttest/features/sync/backfill/backfill_response_handler_test.darttest/features/sync/matrix/sync_event_processor_consumption_handlers_test.darttest/features/sync/model/sync_message_consumption_test.darttest/features/sync/sequence/sync_sequence_payload_type_test.darttest/mocks/mocks.dart
- 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)
There was a problem hiding this comment.
🧹 Nitpick comments (2)
test/features/ai/conversation/conversation_repository_test.dart (1)
30-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract
_registerConsumptionRecorder()to a shared test helper.This helper is duplicated verbatim in
test/features/ai/repository/unified_ai_inference_repository_test.dart. Sincetest/features/ai_consumption/test_utils.dartis already shared across several files in this PR (e.g., formakeConsumptionEvent()), 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 winExtract the
AiConsumptionRecorderregistration/teardown boilerplate into a shared test helper.The register-singleton +
isRegistered-guardedaddTearDownunregister 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
📒 Files selected for processing (18)
test/features/agents/workflow/event_agent_workflow_test.darttest/features/agents/workflow/project_agent_workflow_test.darttest/features/agents/workflow/soul_evolution_workflow_test.darttest/features/agents/workflow/task_agent_workflow_test.darttest/features/agents/workflow/template_evolution_workflow_test.darttest/features/ai/conversation/conversation_repository_test.darttest/features/ai/model/ai_call_impact_test.darttest/features/ai/repository/melious_inference_repository_test.darttest/features/ai/repository/unified_ai_inference_repository_test.darttest/features/ai/services/skill_inference_runner_test.darttest/features/ai_consumption/model/consumption_aggregation_models_test.darttest/features/daily_os_next/agents/workflow/day_agent_workflow_test.darttest/features/sync/backfill/backfill_response_handler_test.darttest/features/sync/matrix/sync_event_processor_consumption_handlers_test.darttest/features/sync/outbox/outbox_enqueue_writer_test.darttest/features/sync/outbox/outbox_scheduling_test.darttest/features/sync/outbox/outbox_service_test.darttest/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
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
test/features/agents/workflow/event_agent_workflow_test.dart (1)
950-957: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsumption-recorder registration/teardown boilerplate duplicated.
This exact
getIt.registerSingleton<AiConsumptionRecorder>(MockAiConsumptionRecorder()); addTearDown(...)block is identical to the one at lines 414-421. Consider a smallregisterConsumptionRecorder()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
📒 Files selected for processing (8)
test/features/agents/workflow/event_agent_workflow_test.darttest/features/ai/repository/ai_consumption_mapping_test.darttest/features/ai_consumption/consumption/ai_consumption_recorder_test.darttest/features/ai_consumption/repository/consumption_repository_test.darttest/features/ai_consumption/sync/consumption_sync_service_test.darttest/features/sync/matrix/matrix_service_test.darttest/features/sync/ui/view_models/outbox_list_item_view_model_test.darttest/mocks/mocks.dart
🚧 Files skipped from review as they are similar to previous changes (1)
- test/mocks/mocks.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.
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_impactandbilling_costonly on non-streaming responses, so measured Melious calls now poststream: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/)ConsumptionDatabaseonai_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 (mirrorsAgentDatabase/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.AiConsumptionEventfreezed entity + response-type enum.Matrix sync
SyncMessage.consumptionEvent, appendedconsumptionEventpayload 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
InferenceImpactCollectorside-channel that mirrors the existingThoughtSignatureCollectorthreading 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)ConversationRepository.sendMessage, wired through every agent workflow (task, project, event, day, template/soul evolution).AiConsumptionRecordernever 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 formatclean.Notes
ai_consumption/README.mdwith Mermaid diagrams for capture flow, sync flow, and aggregation.syncPayloadConsumptionEventpayload label.Summary by CodeRabbit