Skip to content

feat(server): skip DB write for empty turns via TurnSubstance predicate - #592

Merged
chuks-qua merged 1 commit into
mainfrom
fix/skip-assistant-row
Jun 8, 2026
Merged

chuks-qua merged 1 commit into
mainfrom
fix/skip-assistant-row

Conversation

@chuks-qua

@chuks-qua chuks-qua commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

What

Gate assistant-row persistence on a TurnSubstance predicate so empty turns leave no DB row.

Why

A turn that produces no tool call, body, narration, or hook previously still wrote a hollow assistant row and broadcast turn.persisted. #578 makes "no recordable activity" a first-class contract: such turns persist nothing and broadcast nothing, keeping threads uncluttered.

Key changes

  • turn-finalizer.ts — bufferAssistantBody buffers the provider body (returning the deterministic row id synchronously) instead of writing eagerly. hasRecordableActivity decides whether the turn earns a row. finalize materializes exactly one row via materializeAssistantRow only when the predicate holds; otherwise it clears the turn with no row and no broadcast.
  • agent-service.ts — the provider Message handler now buffers the body. The plan-output path eagerly materializes the row first because plans.message_id is a NOT NULL FK; materializeAssistantRow is idempotent and the materializedThreads flag keeps the predicate true after the buffered body is consumed.
  • narrative-store.ts — new hasBufferedNarrative feeding the predicate (tool call / narration / hook).
  • Late-hook fix (bundled) — lastPersistedMessageIdByThread is now cleared on the empty-turn and materialize-failure paths, so a late Stop/SessionEnd hook for a turn that persisted no row is discarded rather than mis-attached to the previous turn's message.

Behavioral contract changes

  1. A turn that buffered a tool call but never emitted a Message now materializes an assistant row (any buffered tool call is recordable), rather than being discarded.
  2. A fully empty turn leaves no row, so a late hook for it is dropped.

Both are covered by updated/added tests.

Test plan

  • bun run verify — typecheck + lint + tests, 5/5 packages, 1221 server tests green
  • New TurnFinalizer.hasRecordableActivity predicate tests (per-contributor + empty + whitespace body)
  • New regression test: empty turn N+1 resets getLastPersistedMessageId so a late hook can't attach to turn N
  • No user-facing UI changes (epic confirmed) — no Playwright run

Closes #578


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

Summary by CodeRabbit

  • Tests

    • Expanded test coverage for turn finalization and narrative persistence workflows to ensure proper handling of edge cases and improve reliability.
  • Refactor

    • Internal improvements to assistant message buffering and turn materialization logic to streamline state management.

Buffer the assistant message body behind TurnFinalizer instead of writing
it eagerly on the provider Message event. finalize materializes exactly one
assistant row only when hasRecordableActivity holds (a buffered tool call,
non-empty body, narration segment, or hook); a fully empty turn leaves no
hollow row and broadcasts nothing.

The plan-output path eagerly materializes the row first because
plans.message_id is a NOT NULL FK; materializeAssistantRow is idempotent and
the materializedThreads flag keeps the predicate true after the buffered body
is consumed.

Also clear lastPersistedMessageIdByThread on the empty-turn and
materialize-failure paths so a late hook for a turn that persisted no row is
discarded rather than mis-attached to the previous turn's message.

Closes #578
@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c61e9bee-a928-4290-a4c7-135e34c157c6

📥 Commits

Reviewing files that changed from the base of the PR and between 06577a3 and d960389.

📒 Files selected for processing (5)
  • apps/server/src/services/__tests__/agent-service-narrative-persist.test.ts
  • apps/server/src/services/__tests__/turn-finalizer.test.ts
  • apps/server/src/services/agent-service.ts
  • apps/server/src/services/narrative-store.ts
  • apps/server/src/services/turn-finalizer.ts

📝 Walkthrough

Walkthrough

This PR defers assistant message row creation from event handling to turn finalization. The TurnFinalizer now buffers provider message bodies and uses a hasRecordableActivity predicate to avoid materializing hollow assistant rows for turns with no substance. AgentService adapts to buffer assistant bodies instead of eagerly creating rows, while plan output persistence materializes rows on-demand for foreign-key consistency.

Changes

Deferred Assistant-Row Materialization

Layer / File(s) Summary
Buffer state & data contracts
apps/server/src/services/turn-finalizer.ts
Adds BufferedBody type and per-thread state maps for buffered provider bodies and materialization tracking.
Buffering & recoverability APIs
apps/server/src/services/turn-finalizer.ts, apps/server/src/services/narrative-store.ts
Introduces bufferAssistantBody, hasBufferedNarrative, and hasRecordableActivity methods to enable deferred materialization and empty-turn detection.
Turn finalization & materialization restructure
apps/server/src/services/turn-finalizer.ts
Refactors finalize to guard with recoverability predicate, unifies materialization via materializeAssistantRow, and updates clearTurn to remove buffered state.
AgentService event handler integration
apps/server/src/services/agent-service.ts
Updates Message event handler to buffer assistant bodies instead of eagerly creating rows; materializes rows eagerly when plan output is pending.
Test coverage & validation
apps/server/src/services/__tests__/turn-finalizer.test.ts, apps/server/src/services/__tests__/agent-service-narrative-persist.test.ts
Adds tests for buffering behavior, empty-turn handling, recoverability predicates, late-hook discard, and turn-state isolation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • Mzeey-Empire/mcode#578: This PR directly implements the TurnSubstance change described in the issue: buffering assistant bodies, adding hasRecordableActivity, and deferring assistant-row materialization to the finalization seam.

Possibly related PRs

  • Mzeey-Empire/mcode#590: Both PRs change TurnFinalizer's synthesized assistant-row creation to use a stable/deterministic assistant message id (and idempotent insert behavior) so the assistant row can be materialized consistently across replay/late-hook scenarios.
  • Mzeey-Empire/mcode#430: Both PRs modify apps/server/src/services/agent-service.ts to buffer streaming assistant text per thread and then conditionally materialize/broadcast an assistant row with a deterministic messageId during the stop/finalization seam.
  • Mzeey-Empire/mcode#587: Both PRs modify the same end-of-turn seam around TurnFinalizer—adjusting how AgentService buffers/materializes the assistant row and how NarrativeStore ties narrative/tool persistence to the turn-finalization flow.

Poem

🐰 A turn arrives with words to say,
We buffer them, not rush away—
At finalization's gentle call,
The substance decides: create or stall!
No hollow rows, just truth we've earned,
Each narrative precisely turned. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: introducing a TurnSubstance predicate to skip DB writes for empty turns, which is the core objective of this PR.
Description check ✅ Passed The description comprehensively covers all required sections: What (gate assistant-row persistence), Why (prevent hollow rows and broadcasts), and Key Changes (detailed file-by-file breakdown with behavioral contracts and test plan).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@chuks-qua
chuks-qua merged commit f223a5b into main Jun 8, 2026
7 checks passed
@chuks-qua
chuks-qua deleted the fix/skip-assistant-row branch June 8, 2026 15:13
@mze-bot mze-bot Bot mentioned this pull request Jun 9, 2026
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.

fix: skip assistant row for empty turns (TurnSubstance)

1 participant