fix: compute conversation iteration count live from events instead of a stale stored column#325
Conversation
There was a problem hiding this comment.
Reviewed changes — computes iteration_count live from agent_conversation_events instead of trusting a stored column that was never incremented by any runtime path, fixing #314.
- Replace stored
iteration_countwith live subquery inconversationCols— countsActionEventrows per conversation via the existing(conversation_id, event_type)index - Remove
iteration_countfrom INSERT, UPDATE, and record mapping inagent_repository.go - Drop the column via migration
000026using idempotentDROP COLUMN IF EXISTS - Add e2e test seeding 3 ActionEvents among 5 total events and asserting
iteration_count=3on both list and get endpoints
Stale reference: docs/architecture/database-schema.md and docs/ai-agent/database-schema.md still describe iteration_count as a stored column — worth updating in a follow-up, but not blocking.
✅ No new issues found.
DeepSeek Pro (free via Pullfrog for OSS) (Kimi K2 not used — the program covers this model; add its provider key to run your pick) | 𝕏
There was a problem hiding this comment.
Reviewed changes — incremental delta since the prior review at 3e6fda5: fixes a COUNT(*) vs int type mismatch in the scan struct that would fail on the Postgres bigint return, and extends e2e coverage to the remaining conversationCols call sites plus the zero-events edge case.
- Fix
IterationCounttype frominttoint64inagentConversationRecord—COUNT(*)returns Postgresbigint, and the priorinttype would cause a scan failure. - Add
FindLatestConversationByChatSessionsubtest — exercises the thirdconversationColscall site, which usesWHERE chat_session_id = ...rather thanWHERE id = .... - Add zero-ActionEvents subtest — confirms
iteration_countis0when a conversation has only non-ActionEventrows.
✅ No new issues found.
DeepSeek Pro (free via Pullfrog for OSS) (Kimi K2 not used — the program covers this model; add its provider key to run your pick) | 𝕏

Summary
Fixes #314 — the "N iterations" pill on conversation rows always showed 0.
Root cause:
agent_conversations.iteration_countwas a stored counter that no runtime path ever incremented. The only writer of the full row,AgentRepository.UpdateConversation, is called exclusively from tests — the real runtime paths (executor, ACP dispatch, bridge, realtime) all go throughUpdateConversationStatus, which never touchesiteration_count. So the column stayed at its default0forever, regardless of how many steps a conversation actually took.Fix: instead of wiring up a new writer in the ai-agent service (which risks drifting out of sync again, and needs a backfill for historical conversations), the count is now computed live in the API layer from data that's already authoritative:
agent_conversation_events. One persistedActionEventcorresponds to one agent reasoning step/action, soiteration_countis a correlatedCOUNT(*)subquery over that table, scoped toevent_type = 'ActionEvent'.Changes
services/api/migrations/000026_drop_conversation_iteration_count.sql— drops the now-unused stored column.services/api/internal/repository/postgres/agent_repository.go—conversationCols(used byListConversations,FindConversationByID,FindLatestConversationByChatSession) computesiteration_countvia a correlated subquery instead of reading the stored column;CreateConversation/UpdateConversationno longer write to it.services/api/test/e2e/conversation_pagination_test.go— addsTestE2EConversationIterationCount, seeding a mix of event types and asserting both the list and single-conversation endpoints return the correctActionEventcount.Why compute-on-read instead of increment-on-write
A stored counter needs a writer, and that writer needs to run on every event-persistence path (in-process SDK executor and the ACP bridge daemon) to stay correct — which is exactly the kind of drift that caused this bug in the first place. Computing it from
agent_conversation_eventsat read time means there's a single source of truth, no new write path to keep in sync, and no backfill migration needed for conversations that already ran.Test plan
go build ./...passesgo test ./...passes (all Go unit tests)PACA_E2E=1 go test ./test/e2e/...passes against real Postgres/Redis/MinIO via testcontainers, including the newTestE2EConversationIterationCount(seedsSystemPromptEvent/ActionEvent/ObservationEventrows and confirmsiteration_countreflects only theActionEventcount, on both the list and get-by-id endpoints)python -m pytestpasses inservices/ai-agent(untouched — no changes needed there)