Skip to content

fix(astra): stop dropping and collapsing habits when listing#236

Merged
thomasluizon merged 2 commits into
mainfrom
fix/astra-habit-listing
Jun 24, 2026
Merged

fix(astra): stop dropping and collapsing habits when listing#236
thomasluizon merged 2 commits into
mainfrom
fix/astra-habit-listing

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

The bug

Asked "quais meus habitos de hoje" (what are my habits today), Astra dropped scattered habits and collapsed identical siblings — e.g. a user with six identical Water - 710ml sub-habits saw Water listed once, and mid-list items (Dinner, an overdue posthog, etc.) vanished entirely.

Root cause

The prompt has gpt-4.1-mini free-text reproduce the full nested habit index. Two well-documented small-LLM failure modes bite:

  • Duplicate-collapse — identical adjacent lines collapse under induction (the six Water - 710ml lines are byte-identical except for a GUID the model is told never to emit).
  • Lost-in-the-middle — items in the middle of a long list get dropped.

The index data itself is complete and uncapped; this is an enumeration/relay failure, not missing data or a token cap (MaxOutputTokenCount = 8192, the answer ended cleanly).

What this PR does (Stage 1 — backend hardening)

Server-side only, so it improves every client on the next deploy with no app update:

  1. SiblingTitleDisambiguator numbers exact-duplicate-title siblings (n of N) in both the prompt index (ActiveHabitsSection) and query_habits output. The six waters become Water - 710ml (1 of 6)(6 of 6) — distinct, countable lines the model cannot silently merge.
  2. One index instruction line: repeated habits are marked (n of N); list every numbered instance.
  3. finish_reason == length guard in both AI round paths (buffered + streaming): a truncated response is logged (Warning, EventId 8), never silently rendered as a complete list.

Scope / what's deferred

This kills the duplicate-collapse half fully and hardens against silent truncation. The lost-in-the-middle half is only fully solved by having the app render the habit list so the model stops relaying it as free text — that's the next stage (cross-platform, needs the web + mobile chat surfaces). Deliberately not routing listing through a forced query_habits call here: it would add a round-trip per "what's today" without fixing enumeration (the model still relays), and that routing only pays off once the app renders the payload.

Compatibility

Purely additive prompt/text formatting — no DTO or shared-contract change, no client change required. Safe to deploy any time.

Tests

SiblingTitleDisambiguator unit tests, index + tool duplicate-numbering tests, and buffered/streaming truncation-guard tests. Full suite green: 3564 passed, 0 failed.

🤖 Generated with Claude Code

Astra dropped scattered habits and collapsed identical siblings (e.g. six
"Water - 710ml" sub-habits rendered as one) when answering "what are my
habits today". Root cause: the prompt has gpt-4.1-mini free-text reproduce
the full nested habit index, where identical adjacent lines collapse under
induction and mid-list items get lost.

Stage 1 (backend hardening, server-deploys to help all clients today):
- SiblingTitleDisambiguator numbers exact-duplicate-title siblings
  " (n of N)" in both the prompt index and query_habits output, so
  repeated habits become distinct, countable lines the model cannot
  silently merge.
- One index instruction line to list every numbered instance.
- Guard finish_reason == length in both AI round paths so a truncated
  list is logged, never silently rendered as complete.

A later stage will render the habit list in-app so the model stops
relaying the list as free text entirely.

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

@claude claude 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.

Solid fix. The SiblingTitleDisambiguator correctly scopes numbering per-sibling-group (applied at every recursion level in both ActiveHabitsSection and QueryHabitsTool), the ordinal case-sensitive matching is appropriate and tested, and the early-exit on no duplicates keeps the hot path clean. The finish_reason == length guard is a sensible observability improvement — logs at Warning with a stable EventId rather than silently swallowing truncated output. Test coverage is thorough: unit tests for the disambiguator (empty, no-dups, case-sensitive, multi-group), integration-level section/tool tests verifying the numbered suffix in rendered output, and buffered + streaming truncation tests with a recording logger. No DTO or client contract change, safe to deploy immediately.

@thomasluizon
thomasluizon merged commit 2e1f5df into main Jun 24, 2026
6 checks passed
@thomasluizon
thomasluizon deleted the fix/astra-habit-listing branch June 24, 2026 16:06
@sonarqubecloud

Copy link
Copy Markdown

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