From e99c00a9eeeb379ba6c07cd12f632f8d53ba3638 Mon Sep 17 00:00:00 2001 From: claude code agent 227 Date: Fri, 26 Jun 2026 17:19:14 +0300 Subject: [PATCH] test(review): pin full-transcript history past 50 rows + changelog (PR #202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address the PR #202 review (approve-with-comments). The only actionable non-blocking item was the test-coverage suggestion: the source switch in AiChatService.handle from findRecent(chatId, ws, 50) to findAllByChat(chatId, ws) was not pinned by a test. handle() is a streaming method the project marks as not unit-testable, so cover the behavioral guarantee it now relies on at the repo/integration level — seed a chat of 60 messages and assert the default findAllByChat (exactly how handle calls it) returns the FULL transcript in chronological order, including the first turn the old 50-window would have dropped. Also document the behavior change under CHANGELOG [Unreleased] -> Changed. The two stability items (token-budget trim before streamText; O(N) history rebuild per turn) are deferred: the reviewer flagged both as non-blocking conscious trade-offs aligned with the PR's stated goal, and the trim is a larger architecture change out of scope for this follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 9 ++++++ .../ai-chat-message-status.int-spec.ts | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77fe9718..b8e28530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,15 @@ per-workspace rolling-day token budget. ### Changed +- **AI chat now feeds the model the full stored transcript.** The per-turn model + conversation was rebuilt from a sliding window of the 50 most recent stored + rows, which silently dropped the beginning of any longer chat. It is now + rebuilt from the complete non-deleted transcript in chronological order, so + the model sees every turn (a 5000-row backstop guards process memory — a + safety net far above any realistic chat, not a conversational limit). On a + very long chat this can eventually reach the model's context window; the + client already surfaces that as "start a new chat". (#202) + - **AI chat default provider is now `openai-compatible` (reasoning surfaced).** For the `openai` driver the chat provider defaults to the openai-compatible implementation, so a workspace pointing at z.ai/GLM/DeepSeek now streams the diff --git a/apps/server/test/integration/ai-chat-message-status.int-spec.ts b/apps/server/test/integration/ai-chat-message-status.int-spec.ts index 5e7eba1b..b73a815d 100644 --- a/apps/server/test/integration/ai-chat-message-status.int-spec.ts +++ b/apps/server/test/integration/ai-chat-message-status.int-spec.ts @@ -267,4 +267,36 @@ describe('AiChatMessageRepo.update + sweepStreaming [integration]', () => { const all = await repo.findAllByChat(cappedChat, workspaceId, 100); expect(all.map((r) => r.content)).toEqual(['m1-oldest', 'm2', 'm3-newest']); }); + + it('default findAllByChat returns the FULL transcript past 50 rows — no recent-tail window (#202)', async () => { + // PR #202 swapped the model-history rebuild in AiChatService.handle from + // findRecent(chatId, ws, 50) to findAllByChat(chatId, ws) WITHOUT a limit + // arg. This pins the behavioral guarantee that switch relies on: a chat + // longer than the old 50-msg window comes back in FULL (oldest -> newest), + // so no early turns are silently dropped from what the model sees. The old + // 50-cap would have returned only the last 50 of these 60 rows. + const longChat = ( + await createChat(db, { workspaceId, creatorId: userId }) + ).id; + const base = Date.now(); + const total = 60; + for (let i = 0; i < total; i++) { + await createMessage(db, { + workspaceId, + chatId: longChat, + content: `msg-${i}`, + // Strictly increasing timestamps so ordering is deterministic. + createdAt: new Date(base + i * 1000), + }); + } + + // Default args == exactly how handle() calls it now. + const history = await repo.findAllByChat(longChat, workspaceId); + expect(history).toHaveLength(total); + expect(history.map((r) => r.content)).toEqual( + Array.from({ length: total }, (_, i) => `msg-${i}`), + ); + // The very first turn (which the old 50-window would have dropped) is present. + expect(history[0]!.content).toBe('msg-0'); + }); });