Skip to content

fix: NeedsClarification for habit-flavored titles without frequency - #167

Merged
thomasluizon merged 13 commits into
mainfrom
fix/chat-clarification
May 19, 2026
Merged

fix: NeedsClarification for habit-flavored titles without frequency#167
thomasluizon merged 13 commits into
mainfrom
fix/chat-clarification

Conversation

@thomasluizon

@thomasluizon thomasluizon commented May 19, 2026

Copy link
Copy Markdown
Owner

Summary

Two-layer fix for the AI silently creating one-time tasks when the user describes something as a habit/rotina/hábito without naming a schedule.

  • Prompt layer: tightened CreateHabitTool.Description; new rule in StructuringStrategySection so the model asks before calling create_habit on a habit-flavored title
  • Tool layer: CreateHabitTool returns a ClarificationRequest payload when frequency_unit is absent and the title contains "habit"/"rotina"/"hábito"
  • Infra: new ActionStatus.NeedsClarification value, PendingClarification EF entity + store, POST /api/ai/clarifications/{operationId}/resolve endpoint that deep-merges the user's JSON-patch value into the stashed partial args and re-invokes the original tool deterministically
  • Prompt: new ClarificationGuidanceSection teaching the model not to over-ask after a clarification round-trip

Linked issues

Refs thomasluizon/orbit-ui-mobile#98
Refs thomasluizon/orbit-ui-mobile#99

(Issues live in the ui-mobile repo; the ui-mobile PR carries the Closes keywords.)

Paired PR

Design decisions

  1. Resume mechanism: server-side PendingClarificationStore + dedicated endpoint, not a synthetic chat message. Survives page reload, avoids LLM re-interpretation, no policy/confirmation/step-up lifecycle (separate concerns from PendingAgentOperationStore).
  2. Heuristic: frequency_unit absent AND title contains "habit"/"rotina"/"hábito" (case-insensitive). Verb-phrase cases handled by the prompt rule, not the tool.
  3. Payload contract: QuickAction.Value is a JSON merge patch. Generalizes for multi-key patches (e.g. "3 times per week" sets frequency_unit + frequency_quantity + is_flexible). Resolve handler does a shallow JSON merge.
  4. Capability reuse: reuses HabitsWrite capability; clarification is an intermediate state of create_habit, not a new gated operation.
  5. 30-min TTL on pending clarifications; resolve is one-shot (idempotent on retry).

Test plan

  • dotnet build (full solution): PASS
  • dotnet test Orbit.Application.Tests: PASS (1658 passed, 14 new clarification tests)
  • dotnet test Orbit.Infrastructure.Tests --filter AiControllerTests: PASS (17 passed)
  • EF migration generated: AddPendingClarifications
  • Manual smoke (post-merge to local): chat "Create a meditation habit" → ClarificationCard renders; tap Daily → habit created with frequency_unit=Day,frequency_quantity=1
  • Regression: "Create a one-time task to call the dentist Friday" → creates immediately, no card
  • Regression: "Create a daily meditation habit" → creates immediately, no card

Deferred follow-ups

  • ClarificationFlowTests.cs integration test: existing chat integration tests hit the live OpenAI API, making deterministic clarification testing impractical. A focused integration test (seed PendingClarification → POST resolve) is a follow-up PR.
  • Background sweep of expired PendingClarification rows (TTL already enforces logical expiry; physical cleanup deferred).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Interactive clarification flow: assistant asks follow-ups when a habit-like title lacks schedule details and offers four quick-action choices (daily, weekly, “3 per week”, one‑time).
    • Users can submit a structured choice to resolve a pending clarification and continue the original action; resolution is validated against offered options.
  • Chores

    • Server-backed persistence for pending clarifications with a 30‑minute expiry and DB migration applied.
  • Tests

    • New and updated tests covering clarification generation, validation, resolution, and prompt guidance.

Review Change Stack

Two-layer fix for AI silently creating one-time tasks:

- Prompt: tighten CreateHabitTool.Description; new structuring rule
  for habit/rotina/hábito titles without a schedule
- Tool: returns ClarificationRequest payload when frequency_unit is
  absent on a habit-keyword title
- Infra: new ActionStatus.NeedsClarification, PendingClarification
  entity + store, POST /api/ai/clarifications/{id}/resolve endpoint
- Prompt: new ClarificationGuidanceSection teaching the model not
  to over-ask after a clarification round-trip
- Tests: 14 new clarification-trigger unit tests

Refs thomasluizon/orbit-ui-mobile#98 thomasluizon/orbit-ui-mobile#99

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

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
📝 Walkthrough

Walkthrough

This PR implements an AI tool clarification feature allowing tools to return a ClarificationRequest, stash partial arguments server-side, present quick-action options to users, and resume the original operation via POST /api/ai/clarifications/{operationId}/resolve which merges the chosen JSON patch and executes the operation.

Changes

AI Tool Clarification Request System

Layer / File(s) Summary
Domain Models and Storage Contract
src/Orbit.Application/Chat/Models/ClarificationRequest.cs, src/Orbit.Application/Chat/Models/QuickAction.cs, src/Orbit.Domain/Entities/PendingClarification.cs, src/Orbit.Domain/Models/PendingClarificationData.cs, src/Orbit.Domain/Interfaces/IAgentPlatformServices.cs
Adds ClarificationRequest and QuickAction payloads, PendingClarification entity with TTL/resolution tracking, PendingClarificationData (with AllowedValues), and IPendingClarificationStore interface.
Chat Execution Clarification Handling
src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.cs
Adds ActionStatus.NeedsClarification, extends ActionResult with ClarificationRequest, injects IPendingClarificationStore, and stashes NeedsClarification results returning an operation id.
CreateHabitTool Clarification Logic
src/Orbit.Application/Chat/Tools/Implementations/CreateHabitTool.cs
Detects habit-flavored titles and returns a NeedsClarificationPayload when frequency_unit is missing; provides four quick-action JSON patches for common frequency choices.
Database and Store Implementation
src/Orbit.Infrastructure/Persistence/OrbitDbContext.cs, src/Orbit.Infrastructure/Services/PendingClarificationStore.cs, src/Orbit.Infrastructure/Migrations/20260519165526_AddPendingClarifications.*, src/Orbit.Infrastructure/Migrations/OrbitDbContextModelSnapshot.cs
Adds PendingClarifications table, DbSet mapping and indexes, implements PendingClarificationStore with TTL-backed creation, expiry-aware retrieval, quick-action extraction, and atomic MarkResolved via ExecuteUpdateAsync.
Clarification Resolution API Endpoint
src/Orbit.Api/Controllers/AiController.cs, src/Orbit.Application/Chat/Models/ResolveClarificationRequest.cs
Adds POST api/ai/clarifications/{operationId:guid}/resolve that validates the JSON patch, checks allowed values, deep-merges patch into stored partial arguments, atomically claims resolution, executes the operation with merged args, and records an audit.
Dependency Injection Wiring
src/Orbit.Api/Extensions/ServiceCollectionExtensions.cs
Registers PendingClarificationStore as scoped and wires IPendingClarificationStore into ChatExecutionDependencies.
Validation & Constants
src/Orbit.Application/Chat/Validators/ResolveClarificationRequestValidator.cs, src/Orbit.Application/Common/AppConstants.cs, src/Orbit.Application/Common/ErrorMessages.cs
Adds validator ensuring Value is a non-empty JSON object within length limits; adds MaxClarificationValueLength and PendingClarificationTtlMinutes and related error messages.
AI Prompt Guidance
src/Orbit.Infrastructure/Services/Prompts/Sections/Static/ClarificationGuidanceSection.cs, src/Orbit.Infrastructure/Services/Prompts/Sections/Static/StructuringStrategySection.cs, src/Orbit.Infrastructure/Services/SystemPromptBuilder.cs
Adds guidance instructing the assistant to prefer NeedsClarification cards (e.g., for create_habit missing frequency) and registers the new section.
Tests & Test Wiring
tests/Orbit.Application.Tests/Chat/Tools/CreateHabitToolClarificationTests.cs, tests/Orbit.Application.Tests/Chat/Tools/CreateHabitToolTests.cs, tests/Orbit.Application.Tests/Commands/Chat/ProcessUserChatCommandHandlerTests.cs, tests/Orbit.Infrastructure.Tests/Controllers/AiControllerTests.cs, tests/Orbit.Application.Tests/Chat/Validators/ResolveClarificationRequestValidatorTests.cs
Adds CreateHabitTool clarification tests and validator tests, and updates controller/handler tests to include IPendingClarificationStore and validator mocks; tweaks a couple of test payload titles.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Client
    participant Chat as Chat Handler
    participant Tool as CreateHabitTool
    participant Store as PendingClarificationStore
    participant API as ResolveClarification API
    User->>Chat: Send message with habit mention, no frequency
    Chat->>Tool: Execute create_habit with partial args
    Tool->>Chat: Return ToolResult with NeedsClarificationPayload
    Chat->>Store: CreateAsync(pending clarification, 30min TTL)
    Store->>Chat: Return OperationId
    Chat->>User: Display clarification card with quick actions
    User->>API: POST /api/ai/clarifications/{id}/resolve with JSON patch
    API->>Store: GetForResolutionAsync(id, userId)
    Store->>API: Return partial args + allowed values
    API->>API: Merge patch into partial args
    API->>Store: MarkResolvedAsync(id, userId)
    API->>Chat: Execute create_habit with merged args (ConfirmationToken: null)
    Chat->>User: Create habit with selected frequency
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Poem

🐰 I smelled a habit missing its time,
I hid the half-formed args in a clovered rhyme.
Four quick hops later, a patch came through,
I stitched the JSON and the routine sprouted new.
Hooray — routines now hop into view!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a clarification flow (NeedsClarification) for habit-flavored titles without frequency specification.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/chat-clarification

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

Comment thread src/Orbit.Infrastructure/Services/PendingClarificationStore.cs
Comment thread src/Orbit.Api/Controllers/AiController.cs Outdated
Comment thread src/Orbit.Api/Controllers/AiController.cs
Comment thread src/Orbit.Api/Controllers/AiController.cs
Comment thread src/Orbit.Api/Controllers/AiController.cs Outdated
Comment thread tests/Orbit.Application.Tests/Chat/Tools/CreateHabitToolClarificationTests.cs Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 056f335b35

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Orbit.Infrastructure/Services/PendingClarificationStore.cs Outdated
Comment thread src/Orbit.Api/Controllers/AiController.cs Outdated
@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Review summary

The architecture is clean — server-side stash + dedicated resolve endpoint is the right call over a synthetic chat turn. The two-layer fix (prompt + tool heuristic) is well-reasoned and test coverage is solid. A few issues need addressing before merge.

Must-fix

  • TOCTOU race causes duplicate habit creation. The store reads with AsNoTracking and marks-resolved in a separate DB round-trip. Two concurrent resolves for the same operationId can both pass the IsResolved guard and both invoke the tool. Fix: make MarkResolvedAsync return bool via an atomic ExecuteUpdateAsync WHERE ResolvedAtUtc IS NULL, and return Conflict() from the controller when 0 rows are updated. (See inline comments on PendingClarificationStore.cs:65 and AiController.cs:322.)

  • No audit trail on ResolveClarification. Every other endpoint on AiController calls auditService.RecordAsync. This one silently invokes a tool that creates habits without any audit record — a traceability and security gap.

Should-fix

  • No rate limiting. The two step-up endpoints carry DistributedRateLimit; the resolve endpoint is equally state-mutating and has none.

  • Missing FluentValidation for ResolveClarificationRequest. AGENTS.md mandates validators for all new features. The Value field needs format + size validation beyond JsonRequired.

Nit

  • Test assertions check i18n key label substrings (e.g. Contains("daily")) — fragile if key naming changes; better to assert on the JSON patch Value instead.

@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: 7

🧹 Nitpick comments (5)
tests/Orbit.Application.Tests/Chat/Tools/CreateHabitToolClarificationTests.cs (1)

111-122: ⚡ Quick win

Consider validating QuickAction.Value structure beyond JSON parseability.

The test confirms each QuickAction.Value is valid JSON but doesn't verify the structure or content. Consider adding assertions to ensure the parsed JSON contains the expected fields (e.g., frequency_unit, frequency_quantity) with correct values for each action type.

🧪 Example enhancement
 [Fact]
 public async Task ClarificationQuickActions_ContainValidJsonPatches()
 {
     var result = await Execute("""{"title": "Morning habit"}""");

     var clarification = (ClarificationRequest)result.Payload!;
     foreach (var action in clarification.QuickActions)
     {
         var parsed = () => JsonDocument.Parse(action.Value);
         parsed.Should().NotThrow($"QuickAction '{action.Label}' value should be valid JSON");
+        
+        using var doc = JsonDocument.Parse(action.Value);
+        doc.RootElement.TryGetProperty("frequency_unit", out _)
+            .Should().BeTrue($"QuickAction '{action.Label}' should contain frequency_unit");
     }
 }
🤖 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
`@tests/Orbit.Application.Tests/Chat/Tools/CreateHabitToolClarificationTests.cs`
around lines 111 - 122, Update the test
ClarificationQuickActions_ContainValidJsonPatches to not only parse
QuickAction.Value but also validate the JSON shape and expected fields: after
parsing (using JsonDocument.Parse(action.Value)) assert the root contains
expected properties like "frequency_unit" and "frequency_quantity" (and any
other action-specific keys) and verify their types/values match the expected
values for each QuickAction.Label; use the existing Execute(...) result cast to
ClarificationRequest and iterate clarification.QuickActions to perform these
assertions so malformed or semantically incorrect patches fail the test.
src/Orbit.Infrastructure/Persistence/OrbitDbContext.cs (1)

183-185: ⚡ Quick win

Replace new inline limits with shared constants.

The new max-length values are hardcoded in this mapping. Please route these through shared constants to keep limits centralized.

As per coding guidelines, Use AppConstants for magic numbers (MaxSubHabits, MaxUserFacts, etc.), never inline limits.

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

In `@src/Orbit.Infrastructure/Persistence/OrbitDbContext.cs` around lines 183 -
185, Replace the hardcoded max-length integers in the EF mapping with
centralized constants: change the calls that set HasMaxLength on ToolName,
MissingArgumentKey, and Question inside OrbitDbContext's model configuration to
use existing AppConstants (or add new constants like
AppConstants.MaxToolNameLength, AppConstants.MaxMissingArgumentKeyLength,
AppConstants.MaxQuestionLength) instead of 100/500; update or add those
constants in the AppConstants class so the mapping uses the named values and
remove magic numbers from the entity.Property(...) calls.
src/Orbit.Infrastructure/Services/PendingClarificationStore.cs (1)

11-11: ⚡ Quick win

Use shared constants for clarification TTL.

The 30-minute TTL is introduced as an inline numeric constant in this service. Move it to shared constants to keep policy values centralized.

As per coding guidelines, Use AppConstants for magic numbers (MaxSubHabits, MaxUserFacts, etc.), never inline limits.

Also applies to: 29-29

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

In `@src/Orbit.Infrastructure/Services/PendingClarificationStore.cs` at line 11,
The TTL value (private const int TtlMinutes = 30) in PendingClarificationStore
should be moved to the centralized AppConstants to avoid inline magic numbers;
add a suitably named constant (e.g. AppConstants.PendingClarificationTtlMinutes
or AppConstants.ClarificationTtlMinutes), replace the local TtlMinutes usage in
the PendingClarificationStore class with that AppConstants member, and remove
the local constant declaration so all TTL policy values are sourced from
AppConstants.
src/Orbit.Api/Controllers/AiController.cs (2)

16-23: ⚡ Quick win

Add structured business-event logging for the clarification resolve path.

The controller still lacks ILogger<AiController> injection and the new resolve operation does not log business events.

As per coding guidelines, All controllers must inject ILogger<T> and log business events in format: logger.LogInformation("Action {Property}", value) with structured properties in PascalCase.

Also applies to: 301-336

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

In `@src/Orbit.Api/Controllers/AiController.cs` around lines 16 - 23, The
AiController constructor must inject ILogger<AiController> and the clarification
resolve handler must emit structured business-event logs using PascalCase
property names; add ILogger<AiController> to the AiController parameter list and
assign to a private readonly field, then in the clarification resolve path (the
resolve method referenced around lines 301-336) add logger.LogInformation calls
such as logger.LogInformation("ClarificationResolved {ClarificationId} {AgentId}
{UserId}", clarificationId, agentId, userId) and any additional context
(operation status, timestamp) using PascalCase property keys to follow the
project's logging guideline.

310-310: ⚡ Quick win

Use shared API error constants instead of hardcoded strings.

Please replace the new literal error messages with ErrorMessages constants.

As per coding guidelines, Use ErrorMessages constants from Common/ErrorMessages.cs, never hardcode error strings.

Also applies to: 319-319

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

In `@src/Orbit.Api/Controllers/AiController.cs` at line 310, Replace the hardcoded
NotFound error strings in AiController (the NotFound(...) calls that return
"Clarification not found or expired." and the similar literal at the other
occurrence) with the shared ErrorMessages constants from Common/ErrorMessages.cs
(e.g., ErrorMessages.ClarificationNotFound or the appropriate constant name);
ensure the controller imports the Common namespace (or use the fully-qualified
ErrorMessages) and update both NotFound(...) calls to pass the constant instead
of the literal string.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Orbit.Api/Controllers/AiController.cs`:
- Around line 340-350: MergeClarificationValue currently overwrites top-level
keys (baseNode[kvp.Key] = kvp.Value) which drops nested fields in
PartialArgumentsJson; change it to perform a recursive deep-merge: when
iterating patchNode entries, if both baseNode[kvp.Key] and kvp.Value are
JsonObject instances, merge their children recursively (e.g., via a helper
MergeJsonObjects(JsonObject baseObj, JsonObject patchObj)), otherwise replace as
now; ensure you clone nodes (DeepClone) when assigning to avoid shared
references and keep the existing function name MergeClarificationValue and
variables baseNode/patchNode for locating the logic.
- Line 335: The controller is returning Ok(result) directly; replace that with
the PayGate-aware adapter by calling result.ToPayGateAwareResult(v => Ok(v)) so
the endpoint uses Extensions/ResultActionResultExtensions.cs for PAY_GATE
handling; locate the return in AiController (the action that currently returns
Ok(result)) and change it to use ToPayGateAwareResult, and remove any manual
403/PAY_GATE response logic to comply with the guideline.
- Around line 308-335: The current flow (GetForResolutionAsync +
MarkResolvedAsync) can race and allow double execution; replace it with a single
atomic claim operation on pendingClarificationStore (e.g.,
ClaimForResolutionAsync or ClaimAndGetForResolutionAsync) that performs an
UPDATE setting ResolvedAtUtc (with a WHERE ResolvedAtUtc IS NULL AND
ExpiresAtUtc > now AND OperationId = operationId AND UserId = userId) and
returns the claimed record or null; call this new Claim method instead of
GetForResolutionAsync/MarkResolvedAsync, validate the returned claimed item
(return NotFound if null), run MergeClarificationValue on the claimed payload,
and only then call operationExecutor.ExecuteAsync so execution occurs only when
the claim succeeded.
- Around line 343-346: MergeClarificationValue currently calls
JsonNode.Parse(...)? .AsObject() which throws InvalidOperationException for
valid JSON that is not an object (array/primitive); update both occurrences (the
one around line 337 and the block creating patchNode at the AsObject call) to
validate the parsed JsonNode's Kind or use TryGetProperty-style checks instead
of calling AsObject directly, and if the parsed node is not a JsonObject return
a 400 Bad Request (or wrap the parse in a try/catch that catches
InvalidOperationException and responds 400) so non-object JSON patches are
rejected explicitly rather than causing a 500; reference MergeClarificationValue
and the patchNode creation where JsonNode.Parse(...)? .AsObject() is used.

In `@src/Orbit.Application/Chat/Tools/Implementations/CreateHabitTool.cs`:
- Around line 323-328: The IsHabitFlavoredTitle method currently checks for
"hábito" but misses the non-accented Portuguese form; update the OR conditions
in IsHabitFlavoredTitle to also check title.Contains("habito",
StringComparison.OrdinalIgnoreCase) (and optionally other variants like plural
if desired) so inputs like "meu habito matinal" are recognized as
habit-flavored.

In `@src/Orbit.Domain/Entities/PendingClarification.cs`:
- Around line 53-56: MarkResolved currently overwrites ResolvedAtUtc and uses
DateTime.UtcNow; change it to preserve one-shot semantics by only setting
ResolvedAtUtc when it's not already set (i.e., if ResolvedAtUtc is default/null)
and remove direct DateTime.UtcNow usage — instead accept the timestamp via a
parameter or obtain it from the shared time abstraction (e.g., an injected
IClock/IDateTimeProvider) so MarkResolved (and the ResolvedAtUtc property) are
set once and use a testable time source.
- Around line 27-46: The Create factory on PendingClarification must validate
inputs before constructing the instance: inside PendingClarification.Create,
check that userId != Guid.Empty (throw ArgumentException), toolName,
missingArgumentKey and question are not null/empty/whitespace (throw
ArgumentNullException or ArgumentException), and that expiresAtUtc is in the
future (expiresAtUtc > DateTime.UtcNow; throw ArgumentException if not). Keep
the existing defaulting for PartialArgumentsJson and QuickActionsJson but
perform these invariant checks at the top of the Create method and throw clear
exceptions if any validation fails.

---

Nitpick comments:
In `@src/Orbit.Api/Controllers/AiController.cs`:
- Around line 16-23: The AiController constructor must inject
ILogger<AiController> and the clarification resolve handler must emit structured
business-event logs using PascalCase property names; add ILogger<AiController>
to the AiController parameter list and assign to a private readonly field, then
in the clarification resolve path (the resolve method referenced around lines
301-336) add logger.LogInformation calls such as
logger.LogInformation("ClarificationResolved {ClarificationId} {AgentId}
{UserId}", clarificationId, agentId, userId) and any additional context
(operation status, timestamp) using PascalCase property keys to follow the
project's logging guideline.
- Line 310: Replace the hardcoded NotFound error strings in AiController (the
NotFound(...) calls that return "Clarification not found or expired." and the
similar literal at the other occurrence) with the shared ErrorMessages constants
from Common/ErrorMessages.cs (e.g., ErrorMessages.ClarificationNotFound or the
appropriate constant name); ensure the controller imports the Common namespace
(or use the fully-qualified ErrorMessages) and update both NotFound(...) calls
to pass the constant instead of the literal string.

In `@src/Orbit.Infrastructure/Persistence/OrbitDbContext.cs`:
- Around line 183-185: Replace the hardcoded max-length integers in the EF
mapping with centralized constants: change the calls that set HasMaxLength on
ToolName, MissingArgumentKey, and Question inside OrbitDbContext's model
configuration to use existing AppConstants (or add new constants like
AppConstants.MaxToolNameLength, AppConstants.MaxMissingArgumentKeyLength,
AppConstants.MaxQuestionLength) instead of 100/500; update or add those
constants in the AppConstants class so the mapping uses the named values and
remove magic numbers from the entity.Property(...) calls.

In `@src/Orbit.Infrastructure/Services/PendingClarificationStore.cs`:
- Line 11: The TTL value (private const int TtlMinutes = 30) in
PendingClarificationStore should be moved to the centralized AppConstants to
avoid inline magic numbers; add a suitably named constant (e.g.
AppConstants.PendingClarificationTtlMinutes or
AppConstants.ClarificationTtlMinutes), replace the local TtlMinutes usage in the
PendingClarificationStore class with that AppConstants member, and remove the
local constant declaration so all TTL policy values are sourced from
AppConstants.

In
`@tests/Orbit.Application.Tests/Chat/Tools/CreateHabitToolClarificationTests.cs`:
- Around line 111-122: Update the test
ClarificationQuickActions_ContainValidJsonPatches to not only parse
QuickAction.Value but also validate the JSON shape and expected fields: after
parsing (using JsonDocument.Parse(action.Value)) assert the root contains
expected properties like "frequency_unit" and "frequency_quantity" (and any
other action-specific keys) and verify their types/values match the expected
values for each QuickAction.Label; use the existing Execute(...) result cast to
ClarificationRequest and iterate clarification.QuickActions to perform these
assertions so malformed or semantically incorrect patches fail the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 08e43719-784d-4fa6-9f46-5936669c21fb

📥 Commits

Reviewing files that changed from the base of the PR and between 8c6a816 and 056f335.

📒 Files selected for processing (21)
  • src/Orbit.Api/Controllers/AiController.cs
  • src/Orbit.Api/Extensions/ServiceCollectionExtensions.cs
  • src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.cs
  • src/Orbit.Application/Chat/Models/ClarificationRequest.cs
  • src/Orbit.Application/Chat/Models/QuickAction.cs
  • src/Orbit.Application/Chat/Tools/Implementations/CreateHabitTool.cs
  • src/Orbit.Domain/Entities/PendingClarification.cs
  • src/Orbit.Domain/Interfaces/IAgentPlatformServices.cs
  • src/Orbit.Domain/Models/PendingClarificationData.cs
  • src/Orbit.Infrastructure/Migrations/20260519165526_AddPendingClarifications.Designer.cs
  • src/Orbit.Infrastructure/Migrations/20260519165526_AddPendingClarifications.cs
  • src/Orbit.Infrastructure/Migrations/OrbitDbContextModelSnapshot.cs
  • src/Orbit.Infrastructure/Persistence/OrbitDbContext.cs
  • src/Orbit.Infrastructure/Services/PendingClarificationStore.cs
  • src/Orbit.Infrastructure/Services/Prompts/Sections/Static/ClarificationGuidanceSection.cs
  • src/Orbit.Infrastructure/Services/Prompts/Sections/Static/StructuringStrategySection.cs
  • src/Orbit.Infrastructure/Services/SystemPromptBuilder.cs
  • tests/Orbit.Application.Tests/Chat/Tools/CreateHabitToolClarificationTests.cs
  • tests/Orbit.Application.Tests/Chat/Tools/CreateHabitToolTests.cs
  • tests/Orbit.Application.Tests/Commands/Chat/ProcessUserChatCommandHandlerTests.cs
  • tests/Orbit.Infrastructure.Tests/Controllers/AiControllerTests.cs

Comment thread src/Orbit.Api/Controllers/AiController.cs
Comment thread src/Orbit.Api/Controllers/AiController.cs
Comment thread src/Orbit.Api/Controllers/AiController.cs
Comment thread src/Orbit.Api/Controllers/AiController.cs Outdated
Comment thread src/Orbit.Application/Chat/Tools/Implementations/CreateHabitTool.cs
Comment thread src/Orbit.Domain/Entities/PendingClarification.cs
Comment thread src/Orbit.Domain/Entities/PendingClarification.cs Outdated
Backend hardening from review:

- Atomic resolve: MarkResolvedAsync now uses ExecuteUpdateAsync with a
  WHERE ResolvedAtUtc IS NULL guard, returning bool. Closes the TOCTOU
  window where two concurrent resolves could both invoke the tool. The
  controller returns 409 Conflict if it didn't claim the row.
- Audit trail: every branch of ResolveClarification now records an
  AgentAuditEntry (not-found, invalid value, already-resolved, success,
  tool-failure) — matches the ExecutePendingOperation pattern.
- Rate limiting: [DistributedRateLimit("chat")] on the resolve endpoint.
- FluentValidation: new ResolveClarificationRequestValidator
  enforcing non-empty + 2048-char max on Value. Moved
  ResolveClarificationRequest to Orbit.Application.Chat.Models so the
  validator can target it. Auto-discovered via
  AddValidatorsFromAssemblyContaining.
- Defensive merge: MergeClarificationValue now throws JsonException
  (not InvalidOperationException → 500) when the patch is valid JSON
  but not an object (e.g. "[]", "1").
- Tests: replaced fragile i18n-key label assertions with JSON-patch
  Value assertions (the load-bearing contract); added validator tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@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 (4)
src/Orbit.Application/Chat/Validators/ResolveClarificationRequestValidator.cs (2)

8-8: 💤 Low value

Move MaxValueLength to AppConstants.

This limit should be centralized alongside other max-length constants. As per coding guidelines, "Use AppConstants for magic numbers (MaxSubHabits, MaxUserFacts, etc.), never inline limits."

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

In
`@src/Orbit.Application/Chat/Validators/ResolveClarificationRequestValidator.cs`
at line 8, Remove the inline constant MaxValueLength from
ResolveClarificationRequestValidator and replace its usages with a centralized
constant on AppConstants (e.g., AppConstants.MaxValueLength); add the new
MaxValueLength constant to AppConstants alongside other limits (matching value
2048), update ResolveClarificationRequestValidator to reference
AppConstants.MaxValueLength, and delete the now-redundant MaxValueLength
declaration from the validator class.

12-16: 💤 Low value

Use ErrorMessages constants for validation messages.

The hardcoded error strings should use constants from Common/ErrorMessages.cs for consistency. As per coding guidelines, "Use ErrorMessages constants from Common/ErrorMessages.cs, never hardcode error strings."

♻️ Proposed fix

Add to ErrorMessages.cs:

public const string ClarificationValueEmpty = "Clarification value cannot be empty.";
public const string ClarificationValueTooLong = "Clarification value cannot exceed {0} characters.";

Then update the validator:

 RuleFor(x => x.Value)
     .NotEmpty()
-    .WithMessage("Clarification value cannot be empty.")
+    .WithMessage(ErrorMessages.ClarificationValueEmpty)
     .MaximumLength(MaxValueLength)
-    .WithMessage($"Clarification value cannot exceed {MaxValueLength} characters.");
+    .WithMessage(string.Format(ErrorMessages.ClarificationValueTooLong, MaxValueLength));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/Orbit.Application/Chat/Validators/ResolveClarificationRequestValidator.cs`
around lines 12 - 16, The validator currently hardcodes messages in
ResolveClarificationRequestValidator's RuleFor(x => x.Value) chain; replace
those strings with ErrorMessages constants from Common/ErrorMessages.cs (add
ClarificationValueEmpty and ClarificationValueTooLong constants as suggested)
and use the ClarificationValueTooLong with string formatting to inject
MaxValueLength so the MaximumLength rule uses the constant message; update the
RuleFor call to reference ErrorMessages.ClarificationValueEmpty and
string.Format(ErrorMessages.ClarificationValueTooLong, MaxValueLength) for the
respective WithMessage invocations.
src/Orbit.Infrastructure/Services/PendingClarificationStore.cs (1)

11-11: 💤 Low value

Move TtlMinutes to AppConstants.

The TTL value should be centralized in AppConstants rather than inlined here. As per coding guidelines, "Use AppConstants for magic numbers (MaxSubHabits, MaxUserFacts, etc.), never inline limits."

♻️ Proposed fix
-    private const int TtlMinutes = 30;

Add to AppConstants:

public const int PendingClarificationTtlMinutes = 30;

Then reference it here:

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

In `@src/Orbit.Infrastructure/Services/PendingClarificationStore.cs` at line 11,
Move the inline TTL constant out of PendingClarificationStore by adding a new
constant PendingClarificationTtlMinutes = 30 to AppConstants, replace references
to the local TtlMinutes in PendingClarificationStore (e.g., the
DateTime.UtcNow.AddMinutes(...) call and any other uses of TtlMinutes) with
AppConstants.PendingClarificationTtlMinutes, and remove the private const int
TtlMinutes from the PendingClarificationStore class.
tests/Orbit.Application.Tests/Chat/Validators/ResolveClarificationRequestValidatorTests.cs (1)

26-31: ⚡ Quick win

Consider verifying error details for consistency.

The other failure tests (Empty_Value_Fails and TooLong_Value_Fails) verify error details (property name or message content), but this test only checks IsValid. Adding an assertion like result.Errors.Should().Contain(e => e.PropertyName == "Value") would improve robustness—if the validator later fails whitespace for an unintended reason, the test would catch it.

✨ Suggested assertion for consistency
     [Fact]
     public void Whitespace_Value_Fails()
     {
         var result = _validator.Validate(new ResolveClarificationRequest("   "));
         result.IsValid.Should().BeFalse();
+        result.Errors.Should().Contain(e => e.PropertyName == "Value");
     }
🤖 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
`@tests/Orbit.Application.Tests/Chat/Validators/ResolveClarificationRequestValidatorTests.cs`
around lines 26 - 31, Update the Whitespace_Value_Fails test to assert the
validation error targets the expected property: after calling
_validator.Validate(new ResolveClarificationRequest("   ")) and assigning to
result, add an assertion like result.Errors.Should().Contain(e => e.PropertyName
== "Value") (or match the same message/content used in
Empty_Value_Fails/TooLong_Value_Fails) so the test verifies the failure is for
the Value property specifically; reference the test method
Whitespace_Value_Fails, the ResolveClarificationRequest instantiation, the
_validator variable and the result.Errors collection when adding the assertion.
🤖 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
`@src/Orbit.Application/Chat/Validators/ResolveClarificationRequestValidator.cs`:
- Line 8: Remove the inline constant MaxValueLength from
ResolveClarificationRequestValidator and replace its usages with a centralized
constant on AppConstants (e.g., AppConstants.MaxValueLength); add the new
MaxValueLength constant to AppConstants alongside other limits (matching value
2048), update ResolveClarificationRequestValidator to reference
AppConstants.MaxValueLength, and delete the now-redundant MaxValueLength
declaration from the validator class.
- Around line 12-16: The validator currently hardcodes messages in
ResolveClarificationRequestValidator's RuleFor(x => x.Value) chain; replace
those strings with ErrorMessages constants from Common/ErrorMessages.cs (add
ClarificationValueEmpty and ClarificationValueTooLong constants as suggested)
and use the ClarificationValueTooLong with string formatting to inject
MaxValueLength so the MaximumLength rule uses the constant message; update the
RuleFor call to reference ErrorMessages.ClarificationValueEmpty and
string.Format(ErrorMessages.ClarificationValueTooLong, MaxValueLength) for the
respective WithMessage invocations.

In `@src/Orbit.Infrastructure/Services/PendingClarificationStore.cs`:
- Line 11: Move the inline TTL constant out of PendingClarificationStore by
adding a new constant PendingClarificationTtlMinutes = 30 to AppConstants,
replace references to the local TtlMinutes in PendingClarificationStore (e.g.,
the DateTime.UtcNow.AddMinutes(...) call and any other uses of TtlMinutes) with
AppConstants.PendingClarificationTtlMinutes, and remove the private const int
TtlMinutes from the PendingClarificationStore class.

In
`@tests/Orbit.Application.Tests/Chat/Validators/ResolveClarificationRequestValidatorTests.cs`:
- Around line 26-31: Update the Whitespace_Value_Fails test to assert the
validation error targets the expected property: after calling
_validator.Validate(new ResolveClarificationRequest("   ")) and assigning to
result, add an assertion like result.Errors.Should().Contain(e => e.PropertyName
== "Value") (or match the same message/content used in
Empty_Value_Fails/TooLong_Value_Fails) so the test verifies the failure is for
the Value property specifically; reference the test method
Whitespace_Value_Fails, the ResolveClarificationRequest instantiation, the
_validator variable and the result.Errors collection when adding the assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cca93890-5642-4b1c-a6f9-58dc108a32b5

📥 Commits

Reviewing files that changed from the base of the PR and between 056f335 and 9965381.

📒 Files selected for processing (8)
  • src/Orbit.Api/Controllers/AiController.cs
  • src/Orbit.Application/Chat/Models/ResolveClarificationRequest.cs
  • src/Orbit.Application/Chat/Validators/ResolveClarificationRequestValidator.cs
  • src/Orbit.Domain/Interfaces/IAgentPlatformServices.cs
  • src/Orbit.Infrastructure/Services/PendingClarificationStore.cs
  • tests/Orbit.Application.Tests/Chat/Tools/CreateHabitToolClarificationTests.cs
  • tests/Orbit.Application.Tests/Chat/Validators/ResolveClarificationRequestValidatorTests.cs
  • tests/Orbit.Infrastructure.Tests/Controllers/AiControllerTests.cs
✅ Files skipped from review due to trivial changes (1)
  • src/Orbit.Application/Chat/Models/ResolveClarificationRequest.cs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/Orbit.Domain/Interfaces/IAgentPlatformServices.cs
  • tests/Orbit.Infrastructure.Tests/Controllers/AiControllerTests.cs
  • tests/Orbit.Application.Tests/Chat/Tools/CreateHabitToolClarificationTests.cs
  • src/Orbit.Api/Controllers/AiController.cs

Comment thread src/Orbit.Api/Controllers/AiController.cs
Comment thread src/Orbit.Infrastructure/Services/PendingClarificationStore.cs
Comment thread src/Orbit.Api/Controllers/AiController.cs Outdated
Comment thread src/Orbit.Application/Chat/Validators/ResolveClarificationRequestValidator.cs Outdated
@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Review summary

Well-designed feature. The two-layer defence (prompt + tool heuristic) is the right approach, the atomic one-shot claim via ExecuteUpdateAsync correctly closes the double-resolve race, the migration/indexes are appropriate, and the 14 new unit tests cover the load-bearing contract (JSON patch values) rather than the i18n labels.

Four issues flagged inline:

Severity File Issue
🔴 AiController.cs:306 ResolveClarification is missing the ApiKey auth guard that every other interactive agent endpoint has — API-key clients can't render clarification cards
🟠 PendingClarificationStore.cs:57 MarkResolvedAsync WHERE clause checks ResolvedAtUtc == null but not ExpiresAtUtc, so a claim can succeed on a logically-expired row if the TTL window is crossed between Get and MarkResolved
🟡 AiController.cs:439 MergeClarificationValue does an unrestricted shallow merge — MissingArgumentKey is fetched but never used to limit which keys the client may patch
🟡 ResolveClarificationRequestValidator.cs:14 Value is not validated as a JSON object; invalid JSON passes validation and only fails later inside MergeClarificationValue

The red and orange items should be fixed before merge; the two yellow items are worth addressing but not blocking.

Tightening the resolve endpoint per review:

- API-key auth guard on ResolveClarification (mirrors
  ExecutePendingOperation and the step-up endpoints — clarification
  cards are a UI-only flow).
- Expiry-aware atomic claim: MarkResolvedAsync's WHERE now also
  requires ExpiresAtUtc > now, so a clarification that ticks over the
  30-min TTL between get and claim can't be executed.
- Server-offered value allowlist: PendingClarificationData now exposes
  AllowedValues (extracted from the stored QuickActionsJson). The
  controller rejects any incoming Value that isn't one of the offered
  options, closing the "client crafts an arbitrary merge patch" hole.
- Validator: Value must parse as a JSON object — unifies the error
  surface (400 with a clear message) rather than failing later inside
  MergeClarificationValue.
- Centralized constants: MaxClarificationValueLength and
  PendingClarificationTtlMinutes moved to AppConstants; validation
  messages added to ErrorMessages. Removes the inline magic numbers
  and string literals per coding guidelines.
- Tests: validator now covers JSON-object rejection (string/array/
  number/null/bool), property-name assertion on the whitespace case,
  and a max-length-valid-JSON happy-path case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread src/Orbit.Api/Controllers/AiController.cs Outdated
Comment thread src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.cs Outdated
Comment thread src/Orbit.Domain/Entities/PendingClarification.cs Outdated
Comment thread src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.cs Outdated
Trivial round-3 cleanup — store the auth method once and reuse it
for both the ApiKey guard and the rest of the handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@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

Caution

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

⚠️ Outside diff range comments (1)
src/Orbit.Api/Controllers/AiController.cs (1)

19-27: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Inject ILogger<AiController> and log this flow.

The new clarification endpoint adds a business operation, but this controller still doesn't inject ILogger<AiController> or emit the required structured business-event logs for validation failures, claims, and execution outcomes.

As per coding guidelines, All controllers must inject ILogger<T> and log business events in format: logger.LogInformation("Action {Property}", value) with structured properties in PascalCase.

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

In `@src/Orbit.Api/Controllers/AiController.cs` around lines 19 - 27, Add an
ILogger<AiController> parameter to the AiController constructor (alongside
IAgentCatalogService, IAgentPolicyEvaluator, etc.) and store it in a private
readonly field; then update the new clarification endpoint (the action that uses
ResolveClarificationRequest and resolveClarificationValidator) to emit
structured LogInformation calls for the key business events: validation failures
(e.g., logger.LogInformation("ValidationFailed {Request}", request)), claims
extraction (e.g., logger.LogInformation("Claims {UserId}", userId)), and
execution outcomes (e.g., logger.LogInformation("ClarificationResult {Result}",
result)); ensure all logged property names use PascalCase and reuse the injected
ILogger<AiController> instance.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Orbit.Api/Controllers/AiController.cs`:
- Around line 452-455: MergeClarificationValue currently coerces any non-object
parsed JsonNode into an empty JsonObject, which silently drops stored args
(e.g., when pending.PartialArgumentsJson is an array or primitive). Change
MergeClarificationValue to parse baseJson into a JsonNode, check if the result
is a JsonObject, and if it is not then fail closed (throw a clear exception like
InvalidOperationException or return the original baseJson as a JsonElement)
instead of creating new JsonObject; only perform the merge logic when the parsed
node is a JsonObject. Reference the MergeClarificationValue method and use the
JsonNode/JsonObject type check to locate and change the behavior.

---

Outside diff comments:
In `@src/Orbit.Api/Controllers/AiController.cs`:
- Around line 19-27: Add an ILogger<AiController> parameter to the AiController
constructor (alongside IAgentCatalogService, IAgentPolicyEvaluator, etc.) and
store it in a private readonly field; then update the new clarification endpoint
(the action that uses ResolveClarificationRequest and
resolveClarificationValidator) to emit structured LogInformation calls for the
key business events: validation failures (e.g.,
logger.LogInformation("ValidationFailed {Request}", request)), claims extraction
(e.g., logger.LogInformation("Claims {UserId}", userId)), and execution outcomes
(e.g., logger.LogInformation("ClarificationResult {Result}", result)); ensure
all logged property names use PascalCase and reuse the injected
ILogger<AiController> instance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3332695f-3645-444f-b6e6-8de6e8938e66

📥 Commits

Reviewing files that changed from the base of the PR and between 9965381 and ca5fece.

📒 Files selected for processing (7)
  • src/Orbit.Api/Controllers/AiController.cs
  • src/Orbit.Application/Chat/Validators/ResolveClarificationRequestValidator.cs
  • src/Orbit.Application/Common/AppConstants.cs
  • src/Orbit.Application/Common/ErrorMessages.cs
  • src/Orbit.Domain/Models/PendingClarificationData.cs
  • src/Orbit.Infrastructure/Services/PendingClarificationStore.cs
  • tests/Orbit.Application.Tests/Chat/Validators/ResolveClarificationRequestValidatorTests.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/Orbit.Application.Tests/Chat/Validators/ResolveClarificationRequestValidatorTests.cs

Comment thread src/Orbit.Api/Controllers/AiController.cs
Comment thread src/Orbit.Api/Controllers/AiController.cs
Comment thread src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.cs Outdated
Comment thread src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.cs
Comment thread tests/Orbit.Infrastructure.Tests/Controllers/AiControllerTests.cs
@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Review Summary

The design is solid. Two-layer defence (prompt + tool-layer heuristic), atomic one-shot resolve, TOCTOU handled correctly via ExecuteUpdateAsync, and the AllowedValues allowlist prevents clients from overriding arbitrary fields. The PayGate-before-clarification ordering is a nice touch.

Blocking

  • ClarificationRequest.QuickActions non-nullable / NeedsClarificationPayload.QuickActions? nullable mismatch — if any future tool passes null QuickActions, the clarification stores "[]" and the resolve endpoint will always reject with 400 (AllowedValues empty). Suggestion left inline.

Non-blocking

  • Multi-line XML doc on DeepMerge violates AGENTS.md one-line comment rule (inline suggestion).
  • NeedsClarificationPayload returned with Success = false silently drops the clarification — implicit invariant worth documenting (inline comment).
  • AiControllerTests has no tests for ResolveClarification; the 6 meaningful paths are straightforward with the existing mock infra. Please file the follow-up PR before this merges so it doesn't get lost.

Everything else — entity factory validation, migration indexes, rate-limit guard, audit trail, deep-merge correctness (null patch value → one-time task confirmed via GetOptionalString returning null on JsonValueKind.Null), validator registration (same assembly as CreateHabitCommandValidator) — looks correct.

…, AiController test coverage

- ProcessUserChatCommand: ClarificationRequest construction now
  null-coalesces payload.QuickActions to Array.Empty<QuickAction>().
  Aligns the nullable upstream type with the non-nullable downstream
  contract so a future tool emitting a null list can't NPE.
- New LogClarificationDroppedOnFailedTool (EventId 25): warns when a
  tool returns Failed/Denied with a NeedsClarificationPayload — the
  payload is dropped (we only stash for Succeeded), so the warning
  surfaces the bug class in production logs.
- DeepMerge XML doc-block collapsed to a single-line comment per
  AGENTS.md "one short line max" convention.
- AiControllerTests: 6 new tests covering the ResolveClarification
  endpoint — ApiKey 403, validator rejection 400, store-miss 404,
  value-not-offered 400, claim-race 409, and the happy path through
  to operationExecutor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.cs Outdated
Comment thread src/Orbit.Application/Chat/Tools/Implementations/CreateHabitTool.cs
Comment thread src/Orbit.Api/Controllers/AiController.cs
Comment thread src/Orbit.Domain/Entities/PendingClarification.cs
Comment thread src/Orbit.Infrastructure/Services/PendingClarificationStore.cs
Five clarification-related comment improvements:

- ProcessUserChatCommand: moved EventIds 24 and 25 to the end of the
  LoggerMessage declarations (after EventId 23) so the sequence reads
  monotonically. They were sandwiched between 8 and 9 and broke a
  visual scan.
- CreateHabitTool: inline comment near the TryGetProperty check
  explaining the null-vs-missing semantic that lets the "one-time
  task" quick action bypass the guard on re-invocation. Easy to
  misregress if someone moves to a value-based check.
- QuickAction docstring: corrected "applied" wording to "deep-merged"
  to match the actual DeepMerge behaviour in the controller.
- PendingClarification.ResolvedAtUtc: comment documents why there's
  no domain Resolve() method — the store uses ExecuteUpdateAsync to
  keep the claim atomic and intentionally bypasses EF change tracking
  for that transition.
- PendingClarificationStore.CreateAsync: comment explains the eager
  SaveChangesAsync trade-off (orphans an unused row on chat-command
  failure; TTL reclaims it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread src/Orbit.Application/Chat/Models/ResolveClarificationRequest.cs Outdated
Comment thread src/Orbit.Application/Chat/Tools/Implementations/CreateHabitTool.cs Outdated
Comment thread src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.cs Outdated
Comment thread src/Orbit.Infrastructure/Services/PendingClarificationStore.cs Outdated
@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Review summary

The clarification-card flow is well-architected: two-layer defence (prompt + tool-layer heuristic), atomic one-shot claim via ExecuteUpdateAsync, allowlist-only patch values, correct API-key guard, and solid test coverage for all the happy/error paths. The migration, indexes, and TTL design are clean.

Four items to address before merge:

  1. Shallow vs deep merge (ResolveClarificationRequest.cs:8) — the doc says "shallow-merged" but MergeClarificationValue calls DeepMerge. Minor now, but will mislead the next tool author who adds a nested-object argument.

  2. "habito" false-positive (CreateHabitTool.cs:341) — unaccented habito is a common Portuguese verb conjugation (habitar); consider a word-boundary regex to match the treatment already applied to \bhabit\b.

  3. No size cap on PartialArgumentsJson (ProcessUserChatCommand.cs:393) — call.Args.GetRawText() is stored verbatim with no length guard and the DB column has no MaxLength. An LLM call with many sub-habits/checklist items could stash a large blob that lives for 30 minutes per attempt within the chat rate limit.

  4. Post-load filtering in GetForResolutionAsync (PendingClarificationStore.cs:54–57) — expiry and resolved-state are checked in C# rather than in the EF WHERE clause; see inline for a suggested push-down (minor, no correctness impact given the PK lookup).

- ResolveClarificationRequest XML doc: corrected "shallow-merged" to
  "deep-merged" so it matches the actual DeepMerge implementation.
- CreateHabitTool.IsHabitFlavoredTitle: removed unaccented "habito"
  match. It collides with the PT verb habitar ("Eu habito em Lisboa"
  = "I live in Lisbon"), which would wrongly trigger the clarification
  card. Users typing PT properly use the accent.
- ProcessUserChatCommand: cap call.Args.GetRawText() at
  AppConstants.MaxClarificationArgsLength (16 KB) before stashing.
  Runaway tool arguments can't bloat PendingClarifications. Returns
  ActionStatus.Failed when exceeded.
- PendingClarificationStore.GetForResolutionAsync: expiry +
  resolved-state filters are now in the SQL WHERE clause instead of
  C# post-load. Single round-trip stays the same but unusable rows
  no longer cross the wire.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread src/Orbit.Application/Chat/Commands/ProcessUserChatCommand.cs Outdated
Comment thread src/Orbit.Api/Controllers/AiController.cs Outdated
@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Review summary

Overall this is a well-designed feature — the two-layer defence (prompt rule + tool-layer heuristic), atomic one-shot claim, user-scoped lookup, AllowedValues allowlist, and 30-minute TTL are all solid. The test coverage is thorough.

Three issues flagged inline:

  1. Misleading log message (ProcessUserChatCommand.cs:396) — LogClarificationDroppedOnFailedTool is reused for the args_too_large branch, but the tool result there was Succeeded, not Failed/Denied. Needs a separate log method.

  2. Spurious validator error (ResolveClarificationRequestValidator.cs:18) — Must(BeJsonObject) runs even when NotEmpty() already failed, producing a second spurious error. Add .When(v => !string.IsNullOrWhiteSpace(v)) to suppress it.

  3. Rate-limit bucket shared with chat (AiController.cs:306) — Resolving a clarification card burns from the user's chat quota. A user who hits the limit mid-clarification-flow can't complete the card. Either use a dedicated bucket or exempt the endpoint (it is already one-shot) and leave a comment if the current behaviour is intentional.

Everything else — auth, migration, indexes, TOCTOU handling, user scoping, DRY patterns — looks correct.

…te rate-limit bucket

- New LogClarificationArgsTooLarge (EventId 26): the args-too-large
  path was reusing LogClarificationDroppedOnFailedTool, whose
  message claimed the tool was Failed/Denied. Misleading because the
  tool actually Succeeded but we dropped the clarification because
  the payload was too big. Two distinct log lines now.
- ResolveClarificationRequestValidator: Cascade(CascadeMode.Stop) on
  the Value rule so an empty value emits one error ("cannot be
  empty") instead of also tripping the JSON-object check. Controller
  only surfaces Errors[0] anyway; redundant failures were noise.
- New "ai-resolve" rate-limit bucket (30/min) separate from "chat".
  Resolving a clarification card no longer counts against the user's
  chat quota — a user mid-flow can't get blocked from tapping a
  quick action because they hit the chat throttle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- User describes a SINGLE weekly occurrence (e.g., "weekly study routine", "every week I want to clean") with no specific day named -> ask which day of the week
- User says a vague time like "morning" or "evening" without a specific hour
- Structure is genuinely ambiguous between checklist and sub-habits -> ask "do you want these as a single checklist or individually trackable steps?"
- User calls something a "habit" / "rotina" / "hábito" without stating daily / weekly / X times per week / a specific schedule -> ASK before calling create_habit. Offer: daily, weekly with specific days, X times per week, or one-time task. Prefer returning a NeedsClarification clarification card from create_habit (with the four quick actions) over a plain-text question.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This line contradicts ClarificationGuidanceSection (Order=260), which says:

"Prefer to ask the schedule INLINE in your text BEFORE calling create_habit… The clarification card is the safety net; the ideal flow is for you to ask first."

But this line says:

"Prefer returning a NeedsClarification clarification card from create_habit (with the four quick actions) over a plain-text question."

The two sections give opposing guidance on which flow is preferred. The model will get contradictory instructions: StructuringStrategySection (lower order ≈ rendered first) says "prefer the card", then ClarificationGuidanceSection says "prefer inline text". Whichever the model anchors on, the other section will seem wrong.

Suggest aligning both to the same preferred flow. Based on the PR description ("the clarification card is the safety net"), the intent appears to be:

  • Preferred: Ask inline in text before calling the tool.
  • Safety net: If the model still calls the tool without a frequency, the tool returns the card.
Suggested change
- User calls something a "habit" / "rotina" / "hábito" without stating daily / weekly / X times per week / a specific schedule -> ASK before calling create_habit. Offer: daily, weekly with specific days, X times per week, or one-time task. Prefer returning a NeedsClarification clarification card from create_habit (with the four quick actions) over a plain-text question.
- User calls something a "habit" / "rotina" / "hábito" without stating daily / weekly / X times per week / a specific schedule -> ASK before calling create_habit. Offer: daily, weekly with specific days, X times per week, or one-time task. Prefer asking via plain text first; the NeedsClarification clarification card is the automatic safety net if you call the tool without a frequency.

[LoggerMessage(EventId = 26, Level = LogLevel.Warning, Message = "Tool {Name} requested clarification with oversized partial args ({Length} chars) — dropped without stashing")]
private static partial void LogClarificationArgsTooLarge(ILogger logger, string name, int length);

[LoggerMessage(EventId = 23, Level = LogLevel.Warning, Message = "Background post-response work failed")]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

EventId 23 is declared here but EventIds 24, 25, and 26 are declared on lines 825–831 above it. The numeric sequence in the file reads 22 → 24 → 25 → 26 → 23, which makes it look like EventId 23 is missing when scanning the file.

Swap the declaration order so the sequence reads 22 → 23 → 24 → 25 → 26, or at minimum add a comment explaining the ordering.

Suggested change
[LoggerMessage(EventId = 23, Level = LogLevel.Warning, Message = "Background post-response work failed")]
[LoggerMessage(EventId = 23, Level = LogLevel.Warning, Message = "Background post-response work failed")]
private static partial void LogBackgroundPostResponseFailed(ILogger logger, Exception ex);
[LoggerMessage(EventId = 24, Level = LogLevel.Information, Message = "Tool {Name} requested clarification (operationId={OperationId}, missing={MissingKey})")]
private static partial void LogClarificationRequested(ILogger logger, string name, Guid operationId, string missingKey);
[LoggerMessage(EventId = 25, Level = LogLevel.Warning, Message = "Tool {Name} emitted a clarification payload on a Failed/Denied result and it was dropped: {Reason}")]
private static partial void LogClarificationDroppedOnFailedTool(ILogger logger, string name, string? reason);
[LoggerMessage(EventId = 26, Level = LogLevel.Warning, Message = "Tool {Name} requested clarification with oversized partial args ({Length} chars) — dropped without stashing")]
private static partial void LogClarificationArgsTooLarge(ILogger logger, string name, int length);

private readonly IServiceScopeFactory _scopeFactory = Substitute.For<IServiceScopeFactory>();
private readonly IAgentCatalogService _catalogService = Substitute.For<IAgentCatalogService>();
private readonly IAgentOperationExecutor _operationExecutor = Substitute.For<IAgentOperationExecutor>();
private readonly IPendingClarificationStore _pendingClarificationStore = Substitute.For<IPendingClarificationStore>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

_pendingClarificationStore is wired into the handler but no test in this file exercises the NeedsClarification code path through ProcessUserChatCommand. The unit tests for CreateHabitTool confirm the tool emits the right payload, and AiControllerTests covers the resolve endpoint — but the critical in-between logic in ProcessUserChatCommandHandler (args-size cap, CreateAsync call, ActionResult.NeedsClarification construction) is untested here.

Minimum coverage gap:

  • Tool returns NeedsClarificationPayload with Success=true → handler calls pendingClarificationStore.CreateAsync and returns ActionStatus.NeedsClarification + ClarificationRequest in the action result.
  • Tool returns NeedsClarificationPayload with oversized args → handler logs and returns ActionStatus.Failed without calling CreateAsync.

_pendingClarificationStore.Received(1).CreateAsync(...) / DidNotReceive() patterns already work for the other stores in this file.

var claimed = await pendingClarificationStore.MarkResolvedAsync(operationId, userId, cancellationToken);
if (!claimed)
{
var auditError = pending.ExpiresAtUtc <= DateTime.UtcNow

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

pending.ExpiresAtUtc here is the value fetched earlier by GetForResolutionAsync, not a fresh DB read. If MarkResolvedAsync returns false because a concurrent request won the claim race (not because the row expired), but the row's TTL then elapses in the sub-millisecond window between the two calls, this check mis-classifies the audit event as "clarification_expired_mid_flight" when the real cause was "clarification_already_resolved".

In practice this race is negligibly rare (two requests resolve the same clarification within 1 ms of the TTL boundary), so it doesn't affect correctness — only the audit label. Worth noting if audit data is ever used for metrics.

@claude

claude Bot commented May 19, 2026

Copy link
Copy Markdown

Review summary

Solid, well-scoped implementation. The two-layer approach (prompt rule + tool-layer heuristic), server-side stash, atomic one-shot claim, and PayGate-before-clarification ordering are all correct. Security story is clean: ToolName is server-stored, AllowedValues is server-extracted, and user scoping is enforced in both GetForResolutionAsync and MarkResolvedAsync.

Issues (4 inline comments)

Severity Location Issue
Medium StructuringStrategySection.cs:41 Contradicts ClarificationGuidanceSection: one says prefer inline text first, the other says prefer the card over plain text. The model gets opposite guidance depending on which section it anchors on.
Minor ProcessUserChatCommand.cs:834 LoggerMessage EventId 23 is declared after 24/25/26 — sequence looks like a missing ID when scanning the file.
Minor ProcessUserChatCommandHandlerTests.cs:36 _pendingClarificationStore is wired in but no test covers the NeedsClarification code path through the command handler (args-size cap, CreateAsync call, ActionStatus.NeedsClarification construction).
Nit AiController.cs:397 Mid-flight expiry vs. race-lost audit classification uses stale pending.ExpiresAtUtc; can mislabel in a pathological race. No correctness impact.

What looks good

  • TryGetProperty used for the absent-key check — correctly passes an explicit null after the one-time patch is merged
  • ExecuteUpdateAsync for atomic claim closes the TOCTOU window properly
  • Rate-limit bucket separated from chat so card taps are not throttled by message quota
  • MaxClarificationArgsLength cap in code; 16 KB is a sensible ceiling for create_habit payloads
  • Test suite covers validator edge cases, tool heuristics (including {"frequency_unit":null} re-invocation), and all controller error paths

@thomasluizon
thomasluizon merged commit af109d5 into main May 19, 2026
3 checks passed
@thomasluizon
thomasluizon deleted the fix/chat-clarification branch May 19, 2026 20:12
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