Repository navigation
feat(ai): sliding-window context — compaction, prompt caching, tool-result eliding, agent memory - #1637
Conversation
Implements PR 1 of the Sliding-Window Context epic. Moves volatile per-turn data (timestamp/mention/command) out of the system prompt and onto the last user message so the stable system prefix stays byte-identical across turns, enabling provider prefix caching (~75–85% input-cost reduction on multi-step agent runs). Applies explicit OpenRouter/Anthropic cache breakpoints at the message level via providerOptions, and re-applies them per step via prepareStep so mid-loop tool results are cached. Sorts dynamically merged MCP and integration tool keys for deterministic tool-array order. - New `prompt-assembly.ts`: buildVolatileTurnContext, appendTurnContextToLastUserMessage, withCacheBreakpoints - Chat route: stable system = base+pageTree+toolDiscovery; volatile block on last user message; prepareStep breakpoints - Global route: same surgery; stable order base→TOOL_DISCOVERY→instructions→drive→awareness→pageTree→nonCore - integration-tool-resolver.ts: sorted keys on both page-agent and global-assistant resolvers - Chat/global routes: Object.keys(mcpToolSchemas).sort() before merge - complete-request-builder.ts (admin viewer): mirrors new assembly order, removes volatile sections - 28 new/updated tests; typecheck + lint green; pre-existing v1-route AbortSignal failures unrelated prepareStep finding: ai@^5.0.54 fully supports messages?: Array<ModelMessage> in PrepareStepResult — re-applying withCacheBreakpoints(stepMessages, 0) per step is solid. @openrouter/ai-sdk-provider@1.5.4 reads providerOptions.openrouter.cacheControl at message-level; no-op for non-Anthropic models and local providers (ollama/lmstudio/azure/glm). v1 completions route has no volatile sections — no changes needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `conversation_compactions` table (no FK; covers all 4 AI entry
points including row-less conversationIds). Migration 0159.
- `packages/lib/src/ai/context-window.ts`: pure buildModelContext
(soft/hard threshold, pointer cut, emergency truncation, summary-over-
cap), plus exported internals applyPointerCut, findUserTurnCut,
validateTailIntegrity, formatSummaryMessage, stripNonTextForSummarizer.
- `packages/lib/src/ai/summarization-prompt.ts`: buildSummarizationPrompt
with 9 mandatory sections, VERBATIM rule preservation, progressive
framing on previousSummary, explicit token-cap instruction.
- Extend USAGE_SOURCES with 'compaction' + USAGE_SOURCE_LABELS entry.
- Service glue in apps/web/src/lib/ai/core/compaction/:
- compaction-gating.ts: canUseCompaction (admin-only, mirroring
canUseCommands — widen by changing this predicate only)
- compaction-repository.ts: getState/upsertState/invalidate with
optimistic versioning (onConflictDoNothing on first insert, version-
guarded UPDATE for subsequent writes)
- compaction-service.ts: runCompaction — 60s gap check, generateText
with COMPACTION_MODEL env fallback, one re-condense pass if over
cap, version-guarded upsert, loser discards; never throws; tracks
usage with source='compaction'
- prepare-context.ts: prepareConversationContext — gate check,
buildModelContext, scheduleCompaction wrapping after()
- 40 new tests: 32 pure-core (zero mocks), 8 service (mocked repo +
generateText). All green; 4 pre-existing failures unchanged.
- New @pagespace/lib subpath exports: ai/context-window,
ai/summarization-prompt (+ typesVersions entries).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n hooks Wire prepareConversationContext into all four AI entry points so admin users get sliding-window compaction invisibly. Non-admin users get exact legacy behavior (early gate in canUseCompaction). Summary message is never persisted. Entry points wired (Task 7–8): - api/ai/chat/route.ts: originalMessages = full sanitized history for UI, baseMessages = compacted tail for model; stableBoundaryIndex = hasSummary ? 1 : 0 (was hardcoded 0); scheduleCompaction() called in onFinish - api/ai/global/[id]/messages/route.ts: same pattern; injectVisualContent helper factored out; compaction inserted after finalTools+finalSystemPrompt are known - api/v1/chat/completions/route.ts: isThreadMode && !clientManagesHistory branch only; v1ScheduleCompaction() called after settle() on clean exit - agent-communication-tools.ts (ask_agent): user role looked up best-effort (try-catch); void runCompaction(pendingCompaction) fire-and-forget because after() from next/server is unavailable inside tool execution mid-stream PreparedContext extended (Task 7): - Added pendingCompaction field for callers where after() is unavailable - Non-admin early-return now includes pendingCompaction: null Invalidation hooks (Task 9): - api/ai/chat/messages/[messageId]/route.ts: PATCH + DELETE check message.createdAt <= compactedUpToCreatedAt before invalidating (precise, no false positives) - api/ai/page-agents/.../messages/[messageId]/route.ts: same createdAt check - api/ai/global/[id]/messages/[messageId]/route.ts: always invalidates (Message type lacks createdAt; conservative invalidation is safe) - conversation-repository.softDeleteConversation: fire-and-forget invalidate - global-conversation-repository.softDeleteConversation: same DM messages route (api/messages/[conversationId]/[messageId]) intentionally skipped: DM conversations are not AI chat — compaction never applies there. Tests: agent-communication-tools test file mocks prepareConversationContext and runCompaction; try-catch on the role DB query handles test mock chain shape. All compaction unit tests pass; 2 pre-existing AbortSignal v1 test failures are environment-specific (Bun/Request incompatibility), not regression. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both this branch and master independently generated migration 0159. Removing ours so it can be regenerated as 0160 on top of master's snapshot lineage (migrations must never be hand-edited). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds chunk-aligned tool-output elision that cuts 40–70% of replayed history tokens in long conversations with no orphan-pair risk. - packages/lib/src/ai/tool-result-eliding.ts: pure module with computeElisionBoundary (chunk-aligned, compactionPointer override) and elideStaleToolOutputs (replaces large read-tool outputs with deterministic stubs; never touches write-tool results or small outputs) - packages/lib/src/ai/__tests__/tool-result-eliding.test.ts: 19 tests covering boundary determinism, stub shape, chunk-alignment invariant, non-elidable/write-tool guards - apps/web/src/lib/ai/core/context-assembly.ts: unified seam (prepareHistoryForModel) composing sanitize → compaction → elide; both chat and global routes now use it instead of bare prepareConversationContext; stableBoundaryIndex and elision boundary coincide by construction - Recall polish: regex_search, list_conversations, read_conversation descriptions updated to mention recovering condensed/elided history Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ng (PR 4) - New agent-memory.ts: getAgentMemoryContext() finds the non-trashed "Agent Memory" child page of an AI_CHAT agent, caps content at ~2k tokens, and returns '' on any error or permission denial. buildAgentMemorySection() wraps content + standing instruction into the stable system-prompt block. - chat route: injects agentMemoryPrompt into the STABLE system section (AI_CHAT only, positioned between pageTree and toolDiscovery) — cache invalidates once per write, not per turn. - provider-factory: adds optional UserProviderRow + ProviderFactoryOptions so callers can supply a pre-loaded user row and skip redundant DB selects. chat route threads its already-loaded user through createAIProvider and updateUserProviderSettings, eliminating two extra round-trips per request. - 14 new agent-memory tests + 6 new provider-factory threading tests, all green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rge (round 2) Master gained its own 0160 (calendar multi-drive sharing). Regenerating ours as 0161 on master's lineage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 16 minutes. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (11)
📝 WalkthroughWalkthroughThis PR implements a production-grade sliding-window conversation compaction system for AI chats. It adds persisted compaction state tracking, summarization orchestration, unified history preparation with cache-aware streaming, deterministic tool ordering, and invalidation flows when edited or deleted messages affect compacted history. ChangesAI Conversation Compaction
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0397ab88ec
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ute_tool elision, await invalidation, accumulate usage
- summarization-prompt.ts: renderTranscript now handles UIMessage-format parts
(type='tool-{name}', input/output fields) so real persisted conversations
don't silently drop tool invocations from compaction summaries (P1)
- tool-result-eliding.ts: remove execute_tool from DEFAULT_ELIDABLE_TOOLS;
in search-exposure mode it dispatches write operations whose results must
not be replaced with a re-call stub (P1)
- chat, global, page-agent message routes: await compaction invalidation on
edit/delete instead of fire-and-forget so stale summaries can't be read
by a concurrent request before the DELETE/PATCH returns (P2)
- compaction-service.ts: accumulate inputTokens/outputTokens across both
summarize() calls when a re-condense pass runs; previously the first
call's usage was silently discarded (P2)
- new test coverage: UIMessage tool part rendering, execute_tool exclusion,
output truncation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…njection CodeQL flagged console.debug at line 149 where user-provided conversationId was logged verbatim. Add maskIdentifier import and wrap the id so it is truncated to first4...last4 format before reaching the log sink. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (3)
apps/web/src/lib/ai/core/compaction/compaction-repository.ts (1)
5-18: ⚡ Quick winReuse the schema-inferred row type instead of duplicating it.
CompactionStateRowis already drifting from the table contract: Line 7 widenssourcetostring, whileconversation_compactions.sourceis constrained to'page' | 'global'. Keeping a hand-maintained copy here makes future schema changes easy to miss; importSelectConversationCompactionfrom@pagespace/db/schema/ai-compactionand aliasCompactionStateRowto it.Suggested refactor
-import { conversationCompactions } from '`@pagespace/db/schema/ai-compaction`'; +import { + conversationCompactions, + type SelectConversationCompaction, +} from '`@pagespace/db/schema/ai-compaction`'; -export interface CompactionStateRow { - conversationId: string; - source: string; - pageId: string | null; - summary: string; - summaryTokens: number; - compactedUpToMessageId: string | null; - compactedUpToCreatedAt: Date | null; - summaryVersion: number; - summarizerModel: string | null; - lastCompactedAt: Date | null; - createdAt: Date; - updatedAt: Date; -} +export type CompactionStateRow = SelectConversationCompaction;As per coding guidelines, TypeScript code should use proper types rather than a widened duplicate contract here.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/lib/ai/core/compaction/compaction-repository.ts` around lines 5 - 18, Replace the hand-maintained CompactionStateRow with the schema-inferred type: import SelectConversationCompaction from the `@pagespace/db/schema/ai-compaction` module and alias or re-export it as CompactionStateRow (i.e., make CompactionStateRow = SelectConversationCompaction) so the in-code type exactly matches the DB table contract and avoids widening fields like source; update any local references to CompactionStateRow to use the imported/aliased type.Source: Coding guidelines
apps/web/src/lib/ai/core/context-assembly.ts (1)
37-49: ⚡ Quick winStop mirroring the write-tool allowlist in two places.
WRITE_TOOLS_FOR_ELISIONis explicitly copied fromtool-filtering.ts. The next write tool added there can be elided here by accident, which hides a side-effectful tool result from the model and breaks follow-up reasoning. Export the canonical set once and consume it from both paths.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/lib/ai/core/context-assembly.ts` around lines 37 - 49, The duplicated WRITE_TOOLS_FOR_ELISION set causes drift; stop mirroring it by exporting the canonical write-tool allowlist from its original module (the symbol that currently defines the list in tool-filtering) and import/use that exported constant here instead of defining WRITE_TOOLS_FOR_ELISION locally; update references in context-assembly.ts to consume the imported symbol (remove the local new Set([...]) and any duplicate entries) so both consumers share the single source of truth.apps/web/src/lib/ai/tools/__tests__/agent-communication-tools.test.ts (1)
99-106: ⚡ Quick winAdd one regression test that actually exercises the compaction path.
The new mock always passes
messagesthrough unchanged and forcespendingCompaction: null, so this suite never covers the new{ synthetic summary + tail }branch or the follow-uprunCompaction()dispatch. That leaves the compaction wiring effectively untested even thoughask_agentnow depends on it. Add a case whereprepareConversationContext()returns a synthetic first message pluspendingCompaction, then assert the exactgenerateText({ messages })payload and that compaction is triggered.Based on PR objectives,
ask_agentnow "build[s] compaction-aware model messages and trigger[s] follow-up compaction."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/lib/ai/tools/__tests__/agent-communication-tools.test.ts` around lines 99 - 106, Add a regression test that mocks prepareConversationContext to return a synthetic summary message followed by the tail messages and a non-null pendingCompaction, then call ask_agent and assert that generateText was invoked with the exact messages array (synthetic summary first + tail) and that runCompaction was subsequently dispatched; specifically override vi.mock for prepareConversationContext to return { messages: [syntheticSummary, ...tail], pendingCompaction: { /* id or payload */ }, scheduleCompaction: vi.fn() }, spy on generateText and runCompaction, call ask_agent with the conversation that triggers compaction, and assert generateText received the composed messages and runCompaction was called with the pendingCompaction payload.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/src/app/api/ai/chat/route.ts`:
- Around line 915-923: The call to prepareHistoryForModel is using pre-factory
preference variables currentProvider/currentModel; change it to use the actual
computed runtime pair (resolvedProvider and resolvedModelName) so compaction
budgets reflect the real provider/model. Locate the prepareHistoryForModel
invocation and replace currentProvider with resolvedProvider and currentModel
with resolvedModelName (or the variable that holds the resolved model name)
while keeping the rest of the arguments (systemPrompt, tools, conversationId,
etc.) unchanged.
In `@apps/web/src/app/api/ai/global/`[id]/messages/route.ts:
- Around line 885-891: prepared.messages is being truncated by using
slice(-MAX_MESSAGES_WITH_IMAGES) so earlier tail messages chosen by
prepareHistoryForModel() are dropped; instead preserve the full
prepared.messages array and only apply injectVisualContent to its last
MAX_MESSAGES_WITH_IMAGES items. Change the logic around prepared.messages,
calling injectVisualContent on
prepared.messages.slice(-MAX_MESSAGES_WITH_IMAGES) to produce processedTail,
then merge that processedTail back into the full messages sequence before
calling convertToModelMessages (and keep using prepared.summaryText when
constructing modelMessages) so only visual-content injection is limited while
the rest of the context remains intact.
In `@apps/web/src/lib/ai/core/compaction/__tests__/compaction-service.test.ts`:
- Around line 65-68: The mocks for mockGenerateText currently return
usage.promptTokens/completionTokens while runCompaction reads usage.inputTokens
and usage.outputTokens; update the mock fixtures (the mockGenerateText responses
at the three locations) to include usage.inputTokens and usage.outputTokens with
the same numeric values used previously, and then change the test assertions
that verify recorded token totals to assert on the aggregated input/output token
sums produced by runCompaction (referencing runCompaction, mockGenerateText,
usage.inputTokens and usage.outputTokens to locate the relevant code).
In `@apps/web/src/lib/ai/core/compaction/compaction-gating.ts`:
- Around line 7-10: The canUseCompaction function currently does a raw role
check (user?.role === 'admin'); replace that with the centralized permission
helper from the shared permissions module so compaction uses the canonical
authorization logic: import the appropriate exported function from the shared
permissions package (for example isAdmin or hasPermission) and call it inside
canUseCompaction to determine access instead of comparing role strings directly;
keep the function signature and return a boolean based on that helper.
In `@apps/web/src/lib/ai/core/complete-request-builder.ts`:
- Around line 164-171: buildCompleteRequest() currently builds systemPrompt by
concatenating baseSystemPrompt, TOOL_DISCOVERY_PROMPT, inlineInstructions and
nonCoreNamesSection but omits the volatile suffix (timestamp/mention/command)
that production appends to the last user message; update buildCompleteRequest()
so the admin preview mirrors production by taking the volatile suffix (the same
logic used on the route side) and appending it to the final user message instead
of dropping it, referencing symbols systemPrompt, baseSystemPrompt,
TOOL_DISCOVERY_PROMPT, inlineInstructions, nonCoreNamesSection and the
buildCompleteRequest() assembly path; alternatively, if you decide not to append
volatile data, split or rename this output to make it explicit that it is a
stable-prefix-only preview (e.g., stableSystemPromptPreview) so callers and
token estimates aren’t misled.
In `@apps/web/src/lib/ai/core/context-assembly.ts`:
- Around line 116-119: The code currently infers a synthetic summary by checking
compacted[0].id (variables hasSummary, summaryText, tailUIMessages), which
misclassifies client-sent UIMessage objects that legitimately lack DB ids;
change prepareConversationContext to return an explicit flag or summary message
alongside the compacted array (e.g., { compacted, syntheticSummary } or
syntheticSummary?: UIMessage), then update this file to stop using
!compacted[0].id — compute hasSummary from the explicit return value and set
summaryText and tailUIMessages based on that flag (use compacted.slice(1) only
when the explicit syntheticSummary flag/message is present).
In `@apps/web/src/lib/ai/core/prompt-assembly.ts`:
- Around line 143-154: The addCacheBreakpoint function currently only sets
providerOptions.openrouter.cacheControl; update it to also set
providerOptions.anthropic with the Vercel AI SDK shape (e.g.,
providerOptions.anthropic = { cacheControl: { type: 'ephemeral' } } and
optionally include ttl) while preserving any existing providerOptions and
existing.openrouter values (keep EPHEMERAL_CACHE for openrouter). Modify
addCacheBreakpoint to merge both openrouter and anthropic entries (or branch by
provider if preferred) and add/adjust tests to assert that both
providerOptions.openrouter.cacheControl and
providerOptions.anthropic.cacheControl are set correctly.
In `@apps/web/src/lib/ai/tools/agent-communication-tools.ts`:
- Around line 598-603: The summary branch currently flattens
prepared.messages[0] to a single string via agentSummaryText, losing additional
parts and tool-rendered content; instead keep the full parts payload by
converting prepared.messages[0] with the same convertToModelMessages flow used
for the tail. Update the logic around
agentHasSummary/agentSummaryText/agentTailUIMessages so that when
agentHasSummary is true you call convertToModelMessages on prepared.messages[0]
(preserving its parts array) and prepend those resulting ModelMessage(s) to
agentTailModelMessages when building agentModelMessages, ensuring generateText()
receives the full parts structure rather than only parts[0].text.
In `@apps/web/src/lib/repositories/conversation-repository.ts`:
- Around line 279-280: Change both softDeleteConversation implementations to
await the compaction invalidation instead of fire-and-forget: ensure the
conversation-state write and the call to invalidateCompaction(conversationId)
are performed as a single awaited flow (ideally in the same helper/transaction)
so the delete does not return until invalidateCompaction completes, and
propagate any errors up to the caller so the route layer can log or handle
failures; update the helper used by
apps/web/src/lib/repositories/conversation-repository.ts and
apps/web/src/lib/repositories/global-conversation-repository.ts to perform and
await both the state delete and invalidateCompaction and surface exceptions
rather than swallowing them.
In `@packages/lib/src/ai/__tests__/context-window.test.ts`:
- Around line 14-20: The default timestamp generation in makeUser (and the other
fixture helpers on lines referenced) uses id.slice(-1) which wraps multi-digit
IDs like "m10" back to 0; change it to extract and parse the full trailing
numeric suffix (e.g. const n = Number((id.match(/\d+$/) || ['0'])[0]) || 0) and
use that n in the Date construction so createdAt increases monotonically for
TEN_MSGS; update the same logic in the other fixture functions referenced (lines
23-29 and 32-40) to use the parsed numeric suffix instead of slice(-1).
In `@packages/lib/src/ai/context-window.ts`:
- Around line 218-223: The code currently uses compaction.summaryTokens which
undercounts because prepare-context actually prepends
formatSummaryMessage(compaction.summary); update the budget calculation to count
the tokens of the formatted summary string instead: if summaryMessage (from
formatSummaryMessage(compaction.summary)) is non-null, call the token estimator
used elsewhere (e.g., the existing estimateTokens/estimateTailTokens utility) to
compute formattedSummaryTokens and replace compaction.summaryTokens with that
value when computing totalTokens/usageRatio (and mirror this change in the other
occurrences referenced); keep using compaction.summaryTokens only as a fallback
if no formatted summary string is produced.
- Around line 255-274: The soft-threshold compaction branch can choose cutIdx ==
tail.length and thus summarize the entire tail; change the guard so the plan
never covers the whole tail. Update the condition around creating compactionPlan
(the block using findUserTurnCut, hasFullUserTurnPreCut, cutIdx, tail,
tailStart, estimateTailTokens, compaction) to require cutIdx < tail.length
(e.g., if (hasFullUserTurnPreCut && cutIdx > 0 && cutIdx < tail.length)) so at
least one live tail message remains verbatim after soft compaction.
In `@packages/lib/src/ai/tool-result-eliding.ts`:
- Around line 195-201: The code only uses part.toolName and falls back to '' so
messages with part.type like 'tool-<name>' are missed; update the logic where
toolName is computed (the toolName variable in tool-result-eliding.ts / the code
handling part) to derive the canonical tool identifier when part.toolName is
undefined by parsing part.type for the 'tool-' prefix (e.g., if typeof part.type
=== 'string' and part.type.startsWith('tool-') then set toolName to the
substring after 'tool-'), then continue using writeTools.has(toolName) and
elidableTools.has(toolName) as before so both write-tools and elidable-tools are
correctly detected.
---
Nitpick comments:
In `@apps/web/src/lib/ai/core/compaction/compaction-repository.ts`:
- Around line 5-18: Replace the hand-maintained CompactionStateRow with the
schema-inferred type: import SelectConversationCompaction from the
`@pagespace/db/schema/ai-compaction` module and alias or re-export it as
CompactionStateRow (i.e., make CompactionStateRow =
SelectConversationCompaction) so the in-code type exactly matches the DB table
contract and avoids widening fields like source; update any local references to
CompactionStateRow to use the imported/aliased type.
In `@apps/web/src/lib/ai/core/context-assembly.ts`:
- Around line 37-49: The duplicated WRITE_TOOLS_FOR_ELISION set causes drift;
stop mirroring it by exporting the canonical write-tool allowlist from its
original module (the symbol that currently defines the list in tool-filtering)
and import/use that exported constant here instead of defining
WRITE_TOOLS_FOR_ELISION locally; update references in context-assembly.ts to
consume the imported symbol (remove the local new Set([...]) and any duplicate
entries) so both consumers share the single source of truth.
In `@apps/web/src/lib/ai/tools/__tests__/agent-communication-tools.test.ts`:
- Around line 99-106: Add a regression test that mocks
prepareConversationContext to return a synthetic summary message followed by the
tail messages and a non-null pendingCompaction, then call ask_agent and assert
that generateText was invoked with the exact messages array (synthetic summary
first + tail) and that runCompaction was subsequently dispatched; specifically
override vi.mock for prepareConversationContext to return { messages:
[syntheticSummary, ...tail], pendingCompaction: { /* id or payload */ },
scheduleCompaction: vi.fn() }, spy on generateText and runCompaction, call
ask_agent with the conversation that triggers compaction, and assert
generateText received the composed messages and runCompaction was called with
the pendingCompaction payload.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 96f7815c-e006-4137-a76f-10fb4b50fd33
📒 Files selected for processing (41)
apps/web/src/app/api/ai/chat/messages/[messageId]/route.tsapps/web/src/app/api/ai/chat/route.tsapps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.tsapps/web/src/app/api/ai/global/[id]/messages/route.tsapps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/messages/[messageId]/route.tsapps/web/src/app/api/v1/chat/completions/route.tsapps/web/src/lib/ai/core/__tests__/agent-memory.test.tsapps/web/src/lib/ai/core/__tests__/integration-tool-resolver.test.tsapps/web/src/lib/ai/core/__tests__/prompt-assembly.test.tsapps/web/src/lib/ai/core/__tests__/provider-factory.test.tsapps/web/src/lib/ai/core/agent-memory.tsapps/web/src/lib/ai/core/compaction/__tests__/compaction-service.test.tsapps/web/src/lib/ai/core/compaction/compaction-gating.tsapps/web/src/lib/ai/core/compaction/compaction-repository.tsapps/web/src/lib/ai/core/compaction/compaction-service.tsapps/web/src/lib/ai/core/compaction/prepare-context.tsapps/web/src/lib/ai/core/complete-request-builder.tsapps/web/src/lib/ai/core/context-assembly.tsapps/web/src/lib/ai/core/integration-tool-resolver.tsapps/web/src/lib/ai/core/prompt-assembly.tsapps/web/src/lib/ai/core/provider-factory.tsapps/web/src/lib/ai/tools/__tests__/agent-communication-tools.test.tsapps/web/src/lib/ai/tools/agent-communication-tools.tsapps/web/src/lib/ai/tools/page-read-tools.tsapps/web/src/lib/ai/tools/search-tools.tsapps/web/src/lib/repositories/conversation-repository.tsapps/web/src/lib/repositories/global-conversation-repository.tspackages/db/drizzle/0161_easy_mockingbird.sqlpackages/db/drizzle/meta/0161_snapshot.jsonpackages/db/drizzle/meta/_journal.jsonpackages/db/package.jsonpackages/db/src/schema.tspackages/db/src/schema/ai-compaction.tspackages/lib/package.jsonpackages/lib/src/ai/__tests__/context-window.test.tspackages/lib/src/ai/__tests__/summarization-prompt.test.tspackages/lib/src/ai/__tests__/tool-result-eliding.test.tspackages/lib/src/ai/context-window.tspackages/lib/src/ai/summarization-prompt.tspackages/lib/src/ai/tool-result-eliding.tspackages/lib/src/monitoring/usage-source.ts
Adds a test that verifies both summarize() calls' inputTokens/outputTokens are summed when a re-condense pass runs, not just the second call's counts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ion toolName, context fixes
- await invalidateCompaction in conversation-repository & global-conversation-repository
so soft-deletes don't leave stale summaries visible to concurrent requests
- fix elideStaleToolOutputs to derive toolName from type field ('tool-{name}')
when toolName is absent (UIMessage SDK format)
- fix context-assembly hasSummary guard to also require <conversation_summary> prefix
preventing v1 client-supplied id-less messages from being misclassified
- fix global/messages/route visual content injection to preserve full prepared tail
(was silently dropping earlier messages, keeping only last MAX_MESSAGES_WITH_IMAGES)
- fix chat/route to pass resolved provider/model to prepareHistoryForModel
- fix context-window soft compaction guard: cutIdx < tail.length prevents absorbing
entire tail into messagesToSummarize with nothing left as the new tail
- fix compaction-service test mocks to use inputTokens/outputTokens (SDK field names)
- fix context-window test makeUser/makeAssistant timestamps for IDs >9 (m10 → :10 not :00)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…en budget accuracy - agent-communication-tools: use convertToModelMessages for the full prepared.messages array (including synthetic summary) instead of manually splitting out parts[0].text, which would silently drop multi-part content if the summary format ever changes - context-window: estimate formatted summary tokens (post-wrapper) rather than stored raw summaryTokens, fixing ~30-token undercount from the <conversation_summary> wrapper and footer text added by formatSummaryMessage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nitpicks) - Export WRITE_TOOLS from tool-filtering.ts; context-assembly.ts now imports it directly instead of maintaining a stale copy — prevents newly added write tools from having their results elided by accident (find_and_replace, insert_content, trigger/role/command tools were all missing from the old WRITE_TOOLS_FOR_ELISION) - compaction-repository: replace hand-maintained CompactionStateRow interface with SelectConversationCompaction from the DB schema so the source field stays 'page' | 'global' and schema changes propagate automatically Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ask_agent - Verifies that when prepareConversationContext returns a synthetic summary message, convertToModelMessages is called with the full prepared.messages array (summary + tail) rather than just the tail after manual splitting - Verifies that runCompaction is fired when pendingCompaction is non-null (covers the fire-and-forget compaction dispatch path in ask_agent) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
All bot review items addressed. Summary of changes across commits The two nitpick items from the main review are also fixed in
The third nitpick (agent-communication-tools regression test) is in |
…mary detection
Consistent with context-assembly.ts: require firstText.startsWith('<conversation_summary>')
so that id-less client-supplied UIMessages in thread mode cannot be misclassified
as synthetic summaries and silently stripped from the model context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2witstudios
left a comment
There was a problem hiding this comment.
🔬 Code Review
Thorough review of the sliding-window compaction, tool-result eliding, stable system prompts / prompt caching, agent memory, and find_and_replace removal (~21k lines, 63 files).
Inline comments are attached to specific lines below. Summary of findings by priority:
High
context-assembly.tshas no unit tests (summary detection, stableBoundaryIndex, boundary coincidence)compaction-repository.tshas no unit tests (version-guarded CAS upsert, first-insert race)hasSummarysentinel check duplicated in 3 places — needs a shared helper;as nevercasts need a real adapter
Medium
contextCalculationin global route uses fullsanitizedMessagesnot the compacted tail — wrong token count in admin context vieweragent-communication-tools.tsskips elision — intentional? If so, document whyprepare-context.tsandcompaction-gating.tsneed minimal unit tests
Low
validateTailIntegrityexported but never called in production — call it or unexportOmit<RunCompactionParams, never>is a no-op typeallAgentToolsnot sorted in agent-communication-tools (cache stability inconsistency)- Refactor mutable-
letblock in global route - Comment explaining why
JSON.stringify(tools)token estimate is intentionally function-free - Drop self-describing comment in compaction-service.ts
Review findingsThe patch introduces context-sizing, billing, persistent-memory security, and consistency failures. In particular, common AI SDK tool outputs are omitted from threshold calculations and concurrent compaction calls can incur untracked provider spend. P1
P2
|
…el, viewer parity, sub-agent elision, compaction test coverage - context-window: buildModelContext now calls validateTailIntegrity and advances mid-turn tails to the next user turn (S1); new shared isSyntheticSummaryMessage sentinel helper (S3/C1) - context-assembly + v1 route: use the shared sentinel; replace 'as never' with an explicit CompactionMessage[] cast (C1) - complete-request-builder: mirror production by appending the volatile turn context to the example last user message; tokenEstimates gains volatileTurnContext (CodeRabbit viewer-parity) - global route: placeholder lets → const IIFE destructure (C6); contextCalculation now measures the post-compaction message set (A1) - ask_agent: switched to the full prepareHistoryForModel seam so sub-agent histories get chunk-aligned elision too (A2); tool-merge determinism documented (C5) - prepare-context: drop no-op Omit type (S2); stringify-estimation comment (C4) - compaction-service: prompt-injection accepted-risk surface documented - chat route: stronger intentional-double-sanitize comment (C2) - tests: new suites for context-assembly (summary split, boundary coincidence, false-positive guard), compaction-repository (CAS upsert, insert race, invalidate), prepare-context (admin gate passthrough, summary prepend, after() scheduling), compaction-gating; lib tests for the integrity-gate recovery paths Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rge (round 3) Master gained its own 0161 (contact_resolved_at). Regenerating ours as 0162 on master's lineage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… race loss, untrusted memory framing, scoped + tombstone invalidation
P1:
- ai-context-calculator: estimateMessageTokens now counts AI SDK v5
'tool-{name}' parts (input/output) — large persisted tool results were
previously counted as overhead only, so compaction/emergency truncation
could fail to fire before the provider rejected the context
- compaction-service: provider usage is tracked immediately after
generation, so a lost version race or failed validation can no longer
incur untracked spend
- agent-memory: memory content is framed as untrusted quoted data inside
an <agent_memory_data> envelope with an explicit non-instruction
preamble — page/web-derived text cannot gain system-tier authority
P2:
- compaction-service: empty/whitespace summaries are never persisted (the
pointer must not advance over silently-lost history); generateText is
bounded with maxOutputTokens and the final result is clamped at the cap
so a paid summary-over-cap loop cannot recur
- compaction-repository: getState is scoped by source (and pageId for
page-source) so reused/colliding conversation IDs can never attach
another conversation's summary; invalidate() now writes an empty-state
TOMBSTONE (insert + onConflictDoUpdate with version bump) instead of
deleting, closing the edit-during-first-compaction race where a pending
null-version insert could land a stale summary
- edit/delete hooks tombstone when no row exists yet; global conversation
delete only invalidates when the user-scoped delete matched a row
Tests: estimator SDK-part coverage, untrusted-framing assertion, race-loss
billing, empty-summary refusal, maxOutputTokens bound, scoped reads,
tombstone semantics (page + global scopes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review findings — all 8 addressed in
|
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/web/src/lib/ai/core/complete-request-builder.ts (1)
269-315:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winOnly count volatile turn context when the example message is actually present.
volatileTurnContextis always added intotokenEstimates.total, butrequest.messagesis empty whenincludeExampleMessageisfalse. In that mode the builder over-reports the payload size and the message-format label advertises tokens that are not in the serialized request.💡 Suggested fix
- const volatileTurnContext = buildVolatileTurnContext({ - timestampPrompt: buildTimestampSystemPrompt(), - mentionPrompt: buildMentionSystemPrompt([ - { id: 'example-page-id', label: 'Example Document', type: 'page' }, - ]), - commandPrompt: '', - }); + const volatileTurnContext = includeExampleMessage + ? buildVolatileTurnContext({ + timestampPrompt: buildTimestampSystemPrompt(), + mentionPrompt: buildMentionSystemPrompt([ + { id: 'example-page-id', label: 'Example Document', type: 'page' }, + ]), + commandPrompt: '', + }) + : ''; const messages: CompleteAIRequest['messages'] = includeExampleMessage ? [ { role: 'user' as const, content: 'What documents are in this drive?' + (volatileTurnContext ? `\n\n${volatileTurnContext}` : ''), }, ] : []; - const volatileTokens = estimateSystemPromptTokens(volatileTurnContext); + const volatileTokens = + includeExampleMessage && volatileTurnContext + ? estimateSystemPromptTokens(volatileTurnContext) + : 0;Based on PR objectives, this builder is supposed to show the payload “exactly as it would be sent to the LLM.”
Also applies to: 378-378
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/lib/ai/core/complete-request-builder.ts` around lines 269 - 315, The token estimates always include volatileTurnContext even when request.messages is empty; update the calculation so volatileTurnContext (volatileTokens) is only added to tokenEstimates and tokenEstimates.total when includeExampleMessage is true (i.e., when the example user message containing volatileTurnContext is actually included in request.messages). Locate the volatileTurnContext / volatileTokens variables and the tokenEstimates object in complete-request-builder (look for volatileTurnContext, includeExampleMessage, volatileTokens, and tokenEstimates.total) and conditionally add volatileTokens to the token breakdown and total only when includeExampleMessage is true.apps/web/src/lib/ai/tools/agent-communication-tools.ts (1)
643-645:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHandle rejected background compactions explicitly.
void runCompaction(prepared.pendingCompaction)drops the promise on the floor. If compaction throws, this path produces an unhandled rejection even though the comment describes it as detached/best-effort.💡 Minimal fix
if (prepared.pendingCompaction) { - void runCompaction(prepared.pendingCompaction); + void runCompaction(prepared.pendingCompaction).catch((error) => { + loggers.ai.error('ask_agent: deferred compaction failed', error as Error, { + agentId, + conversationId: activeConversationId, + }); + }); }Based on PR objectives, this post-generation compaction path is intended to be detached and non-fatal.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/lib/ai/tools/agent-communication-tools.ts` around lines 643 - 645, The detached call to runCompaction(prepared.pendingCompaction) currently drops the promise and can cause unhandled rejections; update that invocation to explicitly handle rejections (e.g., runCompaction(prepared.pendingCompaction).catch(err => { /* swallow or log */ })) so the best-effort background compaction remains non-fatal but any thrown error is caught; reference the runCompaction(...) call and prepared.pendingCompaction variable when making this change.
🧹 Nitpick comments (1)
packages/lib/src/monitoring/ai-context-calculator.ts (1)
124-145: ⚡ Quick winTeach the exported message-part type about
input/outputinstead of casting around them.This new
tool-*branch only works because it locally casts each part to{ input?: unknown; output?: unknown }. That leavesestimateMessageTokens()supporting shapes its own exportedUIMessagetype cannot represent, which is why the new test needs an unsafe assertion too.♻️ Suggested refactor
export interface UIMessage { id?: string; role: 'user' | 'assistant' | 'system'; parts?: Array<{ type: string; text?: string; toolCallId?: string; toolName?: string; args?: unknown; result?: unknown; + input?: unknown; + output?: unknown; }>; }- const sdkPart = part as { input?: unknown; output?: unknown }; - if (sdkPart.input !== undefined && sdkPart.input !== null) { - tokens += estimateTokens(JSON.stringify(sdkPart.input)); + if (part.input !== undefined && part.input !== null) { + tokens += estimateTokens(JSON.stringify(part.input)); } - if (sdkPart.output !== undefined && sdkPart.output !== null) { - const outStr = typeof sdkPart.output === 'string' - ? sdkPart.output - : JSON.stringify(sdkPart.output); + if (part.output !== undefined && part.output !== null) { + const outStr = typeof part.output === 'string' + ? part.output + : JSON.stringify(part.output); tokens += estimateTokens(outStr); }As per coding guidelines,
**/*.{ts,tsx}: Always use proper TypeScript types. Never useanytypes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/lib/src/monitoring/ai-context-calculator.ts` around lines 124 - 145, The branch handling 'tool-*' message parts is using a local cast (sdkPart as { input?: unknown; output?: unknown }) instead of the library's exported message-part type, so update the shared exported UIMessage/message-part type to include optional input?: unknown and output?: unknown for tool-style messages (or add a discriminated union variant for types starting with "tool-"); then remove the unsafe cast in estimateMessageTokens (the branch in ai-context-calculator.ts that reads sdkPart.input/sdkPart.output) and rely on the strengthened type so tests no longer need unsafe assertions, and ensure any call sites or tests importing the message type are updated to the new union variant.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/src/app/api/ai/global/`[id]/messages/[messageId]/route.ts:
- Around line 70-73: Only tombstone the global summary when the edited/deleted
message falls inside the compacted prefix: fetch state via
getState(conversationId) and call invalidate(conversationId, { source: 'global'
}) only if !state OR message.createdAt <= state.compactedUpToCreatedAt; replace
the unconditional invalidate(...) at the shown try block and apply the same
guard to the other occurrence around lines 184-187 so we only clear compaction
when the touched message is within the compacted range.
In `@apps/web/src/lib/ai/core/compaction/compaction-repository.ts`:
- Around line 31-45: The storage key lacks scope fields so reads in getState
(and related functions) can diverge from writes; add a composite key/unique
constraint in the ai-compaction schema for (conversation_id, source, page_id)
and then update all write and conflict/update predicates to include source and
pageId (e.g., in the insert/update/CAS code paths referenced near getState, the
invalidate function, and the other blocks you flagged at 63-81, 84-101, 118-147)
so that inserts, upserts, updates, and tombstones use the same scoped WHERE
(conversationId + source [+ pageId when source==='page']) and the same insert
columns; ensure conversationCompactions table definition and all db.where/eq
calls are changed to match the new composite uniqueness to prevent cross-scope
collisions.
In `@apps/web/src/lib/ai/tools/agent-communication-tools.ts`:
- Around line 595-605: prepareHistoryForModel is currently passed allAgentTools
but generateText may send { ...allAgentTools, ...finishTool }, so the compaction
budget undercounts schema bytes and can overflow at generation time; fix by
making the history prep use the exact toolset that will be sent to generateText
(e.g., compute a toolsForRequest variable combining allAgentTools and finishTool
and pass that to prepareHistoryForModel) and ensure the same toolsForRequest is
used when calling generateText so schema sizing matches; update references to
allAgentTools in the prepareHistoryForModel call to use the combined tools
variable.
---
Outside diff comments:
In `@apps/web/src/lib/ai/core/complete-request-builder.ts`:
- Around line 269-315: The token estimates always include volatileTurnContext
even when request.messages is empty; update the calculation so
volatileTurnContext (volatileTokens) is only added to tokenEstimates and
tokenEstimates.total when includeExampleMessage is true (i.e., when the example
user message containing volatileTurnContext is actually included in
request.messages). Locate the volatileTurnContext / volatileTokens variables and
the tokenEstimates object in complete-request-builder (look for
volatileTurnContext, includeExampleMessage, volatileTokens, and
tokenEstimates.total) and conditionally add volatileTokens to the token
breakdown and total only when includeExampleMessage is true.
In `@apps/web/src/lib/ai/tools/agent-communication-tools.ts`:
- Around line 643-645: The detached call to
runCompaction(prepared.pendingCompaction) currently drops the promise and can
cause unhandled rejections; update that invocation to explicitly handle
rejections (e.g., runCompaction(prepared.pendingCompaction).catch(err => { /*
swallow or log */ })) so the best-effort background compaction remains non-fatal
but any thrown error is caught; reference the runCompaction(...) call and
prepared.pendingCompaction variable when making this change.
---
Nitpick comments:
In `@packages/lib/src/monitoring/ai-context-calculator.ts`:
- Around line 124-145: The branch handling 'tool-*' message parts is using a
local cast (sdkPart as { input?: unknown; output?: unknown }) instead of the
library's exported message-part type, so update the shared exported
UIMessage/message-part type to include optional input?: unknown and output?:
unknown for tool-style messages (or add a discriminated union variant for types
starting with "tool-"); then remove the unsafe cast in estimateMessageTokens
(the branch in ai-context-calculator.ts that reads sdkPart.input/sdkPart.output)
and rely on the strengthened type so tests no longer need unsafe assertions, and
ensure any call sites or tests importing the message type are updated to the new
union variant.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: df2572d1-78a8-4e57-9124-fade197f6ba3
📒 Files selected for processing (31)
apps/web/src/app/api/ai/chat/messages/[messageId]/route.tsapps/web/src/app/api/ai/chat/route.tsapps/web/src/app/api/ai/global/[id]/messages/[messageId]/route.tsapps/web/src/app/api/ai/global/[id]/messages/route.tsapps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/messages/[messageId]/route.tsapps/web/src/app/api/v1/chat/completions/route.tsapps/web/src/lib/ai/core/__tests__/agent-memory.test.tsapps/web/src/lib/ai/core/__tests__/context-assembly.test.tsapps/web/src/lib/ai/core/agent-memory.tsapps/web/src/lib/ai/core/compaction/__tests__/compaction-gating.test.tsapps/web/src/lib/ai/core/compaction/__tests__/compaction-repository.test.tsapps/web/src/lib/ai/core/compaction/__tests__/compaction-service.test.tsapps/web/src/lib/ai/core/compaction/__tests__/prepare-context.test.tsapps/web/src/lib/ai/core/compaction/compaction-repository.tsapps/web/src/lib/ai/core/compaction/compaction-service.tsapps/web/src/lib/ai/core/compaction/prepare-context.tsapps/web/src/lib/ai/core/complete-request-builder.tsapps/web/src/lib/ai/core/context-assembly.tsapps/web/src/lib/ai/core/tool-filtering.tsapps/web/src/lib/ai/tools/__tests__/agent-communication-tools.test.tsapps/web/src/lib/ai/tools/agent-communication-tools.tsapps/web/src/lib/repositories/conversation-repository.tsapps/web/src/lib/repositories/global-conversation-repository.tspackages/db/drizzle/0162_watery_jean_grey.sqlpackages/db/drizzle/meta/0162_snapshot.jsonpackages/db/drizzle/meta/_journal.jsonpackages/lib/src/ai/__tests__/context-window.test.tspackages/lib/src/ai/context-window.tspackages/lib/src/ai/tool-result-eliding.tspackages/lib/src/monitoring/__tests__/ai-context-calculator.test.tspackages/lib/src/monitoring/ai-context-calculator.ts
💤 Files with no reviewable changes (1)
- packages/db/drizzle/0162_watery_jean_grey.sql
✅ Files skipped from review due to trivial changes (2)
- apps/web/src/lib/ai/core/compaction/tests/compaction-gating.test.ts
- packages/db/drizzle/meta/_journal.json
🚧 Files skipped from review as they are similar to previous changes (10)
- apps/web/src/app/api/v1/chat/completions/route.ts
- apps/web/src/app/api/ai/chat/messages/[messageId]/route.ts
- packages/lib/src/ai/tool-result-eliding.ts
- apps/web/src/lib/ai/core/tests/agent-memory.test.ts
- apps/web/src/lib/ai/core/compaction/prepare-context.ts
- apps/web/src/lib/ai/core/context-assembly.ts
- apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/messages/[messageId]/route.ts
- apps/web/src/lib/ai/core/agent-memory.ts
- apps/web/src/app/api/ai/global/[id]/messages/route.ts
- apps/web/src/lib/ai/core/compaction/compaction-service.ts
…al invalidation, scoped writes, accurate sub-agent tool budget - maskIdentifier now strips non-id characters BEFORE masking (CodeQL log injection: slicing alone keeps the taint, and <=8-char ids passed through raw) — fixes every call site at once - global message edit/delete now mirror the page-chat guard: tombstone when no compaction row exists, otherwise invalidate only when the touched message is inside the compacted range (Message interface gains createdAt — the select already returned it) - compaction writes are scope-guarded at the storage layer: the CAS update and the tombstone clear include source/pageId predicates so a colliding conversationId can never overwrite or clear another scope's row; tombstone is now a two-step PK-claim insert + scoped clear+bump (all interleavings with a concurrent compaction stay safe; same-id cross-scope coexistence is intentionally blocked, not corrupted) - ask_agent budgets compaction against the exact tool set generateText receives (finishTool included) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rge (round 4) Master gained its own 0162 (Home drive). Regenerating ours as 0163. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Agents can never run out of context, and users never experience compaction. This PR ships a sliding-window context system plus the cache-aware message assembly it builds on, in four phases (one commit each, built by parallel worktree agents and verified independently before each merge).
PR 1 — Cache-aware prompt assembly (`apps/web/src/lib/ai/core/prompt-assembly.ts`)
PR 2 — Core sliding-window compaction
PR 3 — Cross-request tool-result eliding (`packages/lib/src/ai/tool-result-eliding.ts`)
PR 4 — Agent memory + efficiency
Post-review fixes (bot review — all addressed)
Test plan
🤖 Generated with Claude Code
Review rounds (post-open)
Two full review cycles are incorporated on top of the original four phases:
Round 1 — bot reviews + 16 human threads (commits
0cf8e2ca2…f4109e4d5,b46361169): summarizer handles AI SDKtool-{name}parts;execute_toolexcluded from elision (write-dispatch risk); invalidation awaited in all edit/delete hooks; usage accumulated across re-condense; CodeQL log-injection masked; admin payload viewer mirrors production volatile-context placement;validateTailIntegritywired intobuildModelContext; sharedisSyntheticSummaryMessagesentinel; ask_agent upgraded to the full sanitize→compact→elide seam; global-route const refactor + post-compaction context tracking; four new test suites (context-assembly, compaction-repository, prepare-context, compaction-gating). All 22 threads resolved with replies.Round 2 — context-sizing/billing/security findings (commit
852aabef5): token estimator counts AI SDK v5 tool parts (compaction thresholds now see real sizes); compaction spend tracked even on lost version races; agent memory framed as untrusted quoted data (<agent_memory_data>envelope); empty summaries never persisted; compaction reads scoped by source/pageId; invalidation is now a version-bumping tombstone (closes the edit-during-first-compaction stale-summary race);maxOutputTokens+ final clamp prevent paid summary-over-cap loops; global conversation delete only invalidates rows the caller actually deleted.Summary by CodeRabbit
Release Notes
New Features
Improvements