Skip to content

perf(daemon): redesign spaceTaskMessages.byTask.compact on conversation turns [#2338] - #2361

Open
lsm wants to merge 2 commits into
devfrom
space/2338-redesign-spacetaskmessages-bytask-compact-materialize
Open

perf(daemon): redesign spaceTaskMessages.byTask.compact on conversation turns [#2338]#2361
lsm wants to merge 2 commits into
devfrom
space/2338-redesign-spacetaskmessages-bytask-compact-materialize

Conversation

@lsm

@lsm lsm commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Closes #2338.

What & why

Opening a heavy task thread (~42k messages) cost ~633 ms warm, CPU-bound: the compact feed ran 6 window passes over every task message to compute a result-bounded turn model, plus recomputed it on every 250 ms-debounced re-eval. It also swallowed user messages sent mid-turn (only the first per result-turn was pinned).

This replaces it with a materialized, global-per-task conversation_turn_index and a conversation-turn model.

  • Migration 170 adds conversation_turn_index (stored, backfilled once) + idx_sdk_messages_task_turn. Maintained at insert — an anchor (message_type='user' AND is_renderable=1) opens a new turn; rewind-safe via the append-only invariant.
  • Compact feed + active roster rewritten on a new base CTE that reads the column and pushes a recent-100-turn cap into the scan (deliberately no load-more param — older history is reachable via the unbounded byTask/full feed). Per segment the feed keeps anchor + summary (assistant text → thinking → last 3 tool_use) + result; GitHub activity rows (PR/review/CI) are kept alongside the thread, outside the turn model (no turnIndex, sparse + state-filtered); the roster is the conversation turn holding the session's most recent agent row.
  • Fixes the swallow bug: every renderable user message is now an always-visible anchor.

Design + benchmark evidence: docs/design/2338-compact-conversation-turns.md.

Measurement (46.2k-message task, reproduces the issue's 633 ms base-CTE cost)

query before after
compact ~1.3 s 23 ms
active-turn ~0.8 s 20 ms

Both under the 100 ms target. Legacy byTask/full is left untouched.

Notes for review

  • Result-bounded turns (turnIndex via SUM(isTerminal) window) and the dead turnUserMessageId forward-fill are removed from the compact + active-turn paths (the id-vs-rowid tiebreak inconsistency and the forward-fill-over-tool_result bug go with them). byTask/full keeps the legacy base CTE.
  • Compact/active-turn tests rewritten to the new semantics; the key new test is "mid-turn user messages are never swallowed".

…on turns [#2338]

Replace the result-bounded turn model (6 window passes over every task
message + a dead turnUserMessageId forward-fill; ~1.3s warm on a 46k-message
task) with a materialized, global-per-task conversation_turn_index.

- Migration 170: add conversation_turn_index (stored, backfilled once via
  temp table + UPDATE…FROM) and idx_sdk_messages_task_turn. Maintained at
  insert — an anchor (message_type='user' AND is_renderable=1) opens a new
  turn (MAX+1), others inherit the current turn. Rewind-safe: rewind
  deletes-the-future then appends, so MAX-over-survivors self-corrects.
- Rewrite the compact feed + active roster on a new conversation-turn base
  CTE that reads the column and pushes a recent-100-turn cap into the
  sdk_messages scan. Per segment the compact feed keeps the anchor + a
  summary (assistant text → thinking → last 3 tool_use) + the result; the
  active roster is the conversation turn holding the session's most recent
  agent row (running iff that row is not a result).
- Fixes the mid-turn-user-message swallow bug: every renderable user
  message is now an always-visible conversation anchor.

Measured on a 46.2k-message task (reproduces the issue's 633ms base-CTE
cost): compact 1.3s → 23ms, active-turn 0.8s → 20ms — both well under the
100ms target. Legacy byTask/full + its base CTE are left untouched.

Design + benchmark evidence: docs/design/2338-compact-conversation-turns.md.
Compact/active-turn tests rewritten to the new semantics.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@greptile-apps greptile-apps 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.

lsm has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🤖 Review by glm-5.1 (GLM)

Model: glm-5.1 | Client: NeoKai | Provider: GLM

Recommendation: REQUEST_CHANGES — CI is red (P0, blocking). Two P2 behavior/doc items below.


P0 — Red CI: 5 runtime test files have an inline sdk_messages schema missing conversation_turn_index

This PR adds conversation_turn_index to the INSERT in SDKMessageRepository.saveSDKMessage / saveUserMessage / saveHyperNeoActionMessage, but five test files build their sdk_messages table with a hand-written inline CREATE TABLE that omits the new column:

  • tests/unit/5-space/runtime/prompt-too-long-recovery.test.ts:46
  • tests/unit/5-space/runtime/prompt-too-long-replay.test.ts
  • tests/unit/5-space/runtime/space-runtime-stalled-recovery.test.ts
  • tests/unit/5-space/runtime/space-runtime-tick-loop.test.ts
  • tests/unit/5-space/runtime/space-runtime.test.ts

Each calls runMigrations(db) before CREATE TABLE IF NOT EXISTS sdk_messages (...). Migration 170's tableExists('sdk_messages') guard therefore no-ops (the table doesn't exist yet at migration time), so the column is never added — and the inline CREATE doesn't include it either. Every repository INSERT then throws SqliteError: table sdk_messages has no column named conversation_turn_index, the overflow messages never persist, and /compact is never injected → 21+ assertions fail across the five files. Both Daemon Unit Tests (5-space-runtime-a) and the All Tests Pass gate are red on this PR's head (74225e43).

The PR updated space-test-db.ts, hidden-subtype-idle-detection.test.ts, and sdk-message-repository.test.ts but missed these five. Same inline-schema pattern, latent (didn't fail only because consumers mock the repo): tests/unit/helpers/space-agent-schema.ts:142.

Fix: add conversation_turn_index INTEGER to each inline sdk_messages schema — or, better, point these fixtures at createTables/createSpaceTables so they can't drift on the next schema change.

P2 — GitHub PR-activity rows are dropped from the compact feed (behavior change)

The new compact selected_ids selects only user-anchors / terminal results / non-hook system rows / per-segment assistant summaries. GitHub-event rows (messageType='github_pr_activity', kind='github') match none of these branches, so they are now excluded from spaceTaskMessages.byTask.compact entirely. Legacy compact could surface them via the non-terminal tail. If github activity should appear inline in the compact task thread, add a selected_ids branch for kind='github'; otherwise confirm it's intended (it still appears in the activity panel + the full byTask feed). See anchored comment.

P2 — "load-more param" is documented but not implemented (doc/code mismatch)

The design doc and the SQL doc-comments (here + line 2205) promise "older turns load via the load-more param," but the compact query is registered paramCount: 1 (taskId only) and the recent-turn window is hardcoded to SPACE_TASK_MESSAGES_COMPACT_RECENT_TURNS = 100. There is no load-more parameter, so on tasks with >100 conversation turns (e.g. the 42k-message task in the issue) older turns — including early system:init rows — are silently unreachable in compact (the full view still returns them). Either implement the param or correct the doc to state the cap.

What looks good

  • Anchor definition is consistent across the migration backfill, insert-time resolveConversationTurnIndex, and the query predicate (message_type='user' AND is_renderable=1). Verified computeIsRenderable returns 0 for any user row containing a tool_result block, so tool_result echoes correctly do not start turns.
  • Dropping turnUserMessageId/turnHiddenMessageCount from compact is frontend-safe. useTurnBlocks groups by session-switch + result-boundary (turn block id = firstMsgUuid ?? sessionId-startTime, not turnId); MinimalThreadFeed matches the active roster on (sessionId, turnIndex) — still emitted; and turnHiddenMessageCount is not rendered anywhere in non-test frontend code.
  • Migration 170 backfill is correct: turn 0 for pre-anchor rows, per-task partitioning, NULL for no-task rows, idempotent, and the sargable index is asserted by its own test.

Once the P0 schema drift is fixed and CI is green, the two P2s can be addressed or consciously deferred.

Comment thread packages/daemon/src/lib/rpc-handlers/live-query-handlers.ts
Comment thread packages/daemon/src/lib/rpc-handlers/live-query-handlers.ts Outdated
… load-more doc

Review feedback on PR #2361:

- P0 (red CI): add conversation_turn_index to the hand-written inline
  sdk_messages schemas in the 5 runtime test files + helpers
  (space-agent-schema). They run runMigrations before a CREATE TABLE IF
  NOT EXISTS sdk_messages, so migration 170's column was never added and
  every repo INSERT threw "no column named conversation_turn_index" —
  overflow messages never persisted, /compact never injected (21+ failures).
- P2: surface GitHub activity rows in byTask.compact again. selected_ids
  had no branch matching kind='github', so PR/review/CI events vanished
  from the compact feed (legacy compact surfaced them via the non-terminal
  tail, and mapRow + the frontend still classify kind='github'). Add the
  branch; rows sit outside the turn model (turnIndex NULL) but are sparse
  and state-filtered.
- P2: the load-more param was documented (constant docblock, compact
  docblock, design doc §Performance) but never implemented (paramCount
  stays 1, recent window hardcoded to 100). Correct the docs to state the
  cap; older history is reachable via the unbounded byTask (full) feed.

Tests: new github-in-compact case; full runtime + migration-170 suites green.

@greptile-apps greptile-apps 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.

lsm has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@lsm lsm left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🤖 Review by glm-5.1 (GLM)

Model: glm-5.1 | Client: NeoKai | Provider: GLM

Recommendation: APPROVE — all three prior findings addressed and verified; CI fully green (0 failures).

Round-1 findings — resolved

  • P0 (red CI — missing conversation_turn_index in inline test schemas): fixed. The column was added to all 14 test files that hand-write a sdk_messages schema + helpers/space-agent-schema.ts (not just the 5 that were failing — the latent-risk files too). Daemon Unit Tests (5-space-runtime-a) now passes; the previously-throwing repo inserts no longer break overflow recovery.
  • P2 (GitHub events dropped from compact): fixed. SELECT id FROM joined WHERE kind = 'github' added to selected_ids — github activity rows sit outside the turn model (turnIndex NULL, sparse/state-filtered) but are surfaced again, matching legacy behavior. New test asserts a routed PR event appears in compact with kind='github' / messageType='github_pr_activity' in chronological position.
  • P2 (load-more documented but unimplemented): fixed honestly — all three doc sites (constant docblock, compact SQL docblock, design doc §Performance) now state the 100-turn cap explicitly and point to the unbounded byTask (full) feed as the drill-in surface, rather than claiming a param that was never wired.

CI status (head 5f7a9d953)

All required checks pass: 5-space-runtime-a, Lint/Knip/Format/Typecheck (so bun run check incl. db-schema-parity is clean), Web Tests, both migration shards, and the rest. Both inline review threads are resolved (0 unresolved).

One transient: 4-space-migrations-a initially failed on a single migration-34 test timing out (5000ms ceiling, ran 13s) — an unrelated older migration (status CHECK constraints) this PR doesn't touch, and the shard passed on the prior commit. Confirmed a flake: migration-34 + migration-170 pass locally (17/17, ~6s), and a rerun of the job went green (2m36s).

Still sound (re-verified this round)

Anchor definition consistent across backfill/insert/query; dropping turnUserMessageId/turnHiddenMessageCount is frontend-safe (useTurnBlocks groups by session/result-boundary, MinimalThreadFeed matches on turnIndex, hidden-count not rendered); migration 170 backfill correct + idempotent + sargable.

Ship it.

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.

perf(daemon): redesign spaceTaskMessages.byTask.compact (materialize turn_index / LIMIT pushdown) — 633ms CPU sort

1 participant