fix(llm): preserve prompt cache stability with 2-part system message layout#37
Conversation
Each scheduled task now has its own list of MCP servers that can be toggled on or off independently in the web UI editor dialog. Default is all off. When enabled servers are set, only those MCP tools are available to the task's runs. When null/empty (existing tasks), all MCP servers remain available for backward compatibility. - Add mcp_servers JSON column to cron_task table - Add mcpServers field to Cron Task/CreateInput/UpdateInput schemas - Pass enabled MCP servers to session metadata in the cron scheduler - Filter MCP tools in prompt.ts based on session metadata - Add MCP server toggle list with switches in cron editor dialog - Add i18n translations for new MCP servers section Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…layout Separate the provider/agent header (message 0) and the first static context block (message 1) so LLM providers can cache prompt prefixes across turns. Dynamic blocks (environment, tool availability, user custom system) sit in position 2+ and get compacted safely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: codeplane-agent[bot] <287208015+codeplane-agent[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors how LLM system messages are assembled to improve prompt-prefix cache stability (by keeping stable “header + static block” segments intact), and introduces cron-task-level configuration for restricting which MCP servers/tools are available during scheduled runs.
Changes:
- Restructures
systemprompt assembly/compaction in the LLM streaming path to preserve cacheable prefixes across turns. - Adds
mcpServersto cron task schemas, routes, persistence, and the app client/types/UI. - Threads cron-task MCP server selection into session metadata and uses it to filter MCP tools during prompt tool resolution.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/codeplane/src/session/session.ts | Extends session creation interface to accept metadata (used by cron runs). |
| packages/codeplane/src/session/prompt.ts | Filters MCP tools based on session.metadata.enabledMcpServers. |
| packages/codeplane/src/session/llm.ts | Splits/compacts system messages to keep cacheable prompt prefix segments stable. |
| packages/codeplane/src/server/routes/cron.ts | Adds mcpServers to cron create route input schema. |
| packages/codeplane/src/cron/scheduler.ts | Passes cron task MCP server selection into created session metadata. |
| packages/codeplane/src/cron/cron.ts | Adds mcpServers to cron task schema, create/update inputs, and DB mapping. |
| packages/codeplane/src/cron/cron.sql.ts | Adds mcp_servers JSON-text column to the cron task table schema. |
| packages/codeplane/migration/20260601130626_add_mcp_servers_to_cron_tasks/snapshot.json | Drizzle snapshot reflecting the new cron task column. |
| packages/codeplane/migration/20260601130626_add_mcp_servers_to_cron_tasks/migration.sql | Migration adding mcp_servers column to cron_task. |
| packages/app/src/utils/cron-client.ts | Adds mcpServers to client task and create/update input types. |
| packages/app/src/pages/cron.tsx | Adds MCP server selection UI and submits mcpServers in create/update flows. |
| packages/app/src/i18n/en.ts | Adds UI strings for the MCP servers cron field. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const sanitizeMcp = (s: string) => s.replace(/[^a-zA-Z0-9_-]/g, "_") | ||
| const enabledMcpServers = input.session.metadata?.enabledMcpServers as string[] | undefined | ||
| const mcpTools = enabledMcpServers?.length | ||
| ? Object.fromEntries( | ||
| Object.entries(allMcpTools).filter(([key]) => | ||
| enabledMcpServers.some((name) => key.startsWith(sanitizeMcp(name) + "_")), | ||
| ), | ||
| ) | ||
| : allMcpTools |
| metadata: task.mcpServers?.length | ||
| ? { enabledMcpServers: task.mcpServers } | ||
| : undefined, |
| if (props.existing) { | ||
| const body: CronUpdateInput = { | ||
| name: store.name.trim(), | ||
| description: store.description.trim() || null, | ||
| prompt: store.prompt, | ||
| schedule, | ||
| timeoutMs: timeoutMs ?? null, | ||
| agent: store.agent.trim() || null, | ||
| model: store.model.trim() || null, | ||
| status: store.status, | ||
| mcpServers: store.mcpServers.length > 0 ? store.mcpServers : null, | ||
| } | ||
| await CronClient.update(conn, props.existing.id, body) | ||
| } else { | ||
| const body: CronCreateInput = { | ||
| ...(props.project.id ? { projectID: props.project.id } : {}), | ||
| directory: props.project.worktree, | ||
| name: store.name.trim(), | ||
| description: store.description.trim() || undefined, | ||
| prompt: store.prompt, | ||
| schedule, | ||
| timeoutMs, | ||
| agent: store.agent.trim() || undefined, | ||
| model: store.model.trim() || undefined, | ||
| status: store.status, | ||
| mcpServers: store.mcpServers.length > 0 ? store.mcpServers : undefined, | ||
| } |
| agent: e.agent ?? "", | ||
| model: e.model ?? "", | ||
| status: e.status, | ||
| mcpServers: e.mcpServers ?? [], | ||
| errors: {}, |
| "cron.field.mcpServers": "MCP servers", | ||
| "cron.field.mcpServers.empty": "No MCP servers configured. Add servers in Settings > MCP.", | ||
| "cron.field.mcpServers.help": "Only enabled MCP servers will be available to this task. By default all servers are off.", |
| const CreateInput = z.object({ | ||
| projectID: ProjectID.zod.optional(), | ||
| directory: z.string().optional(), | ||
| name: z.string(), | ||
| description: z.string().optional(), | ||
| prompt: z.string(), | ||
| agent: z.string().optional(), | ||
| model: z.string().optional(), | ||
| schedule: ScheduleInput, | ||
| timezone: z.string().optional(), | ||
| status: Cron.Status.zod.optional(), | ||
| timeoutMs: z.number().optional(), | ||
| maxRetries: z.number().optional(), | ||
| mcpServers: z.array(z.string()).optional(), | ||
| }) |
Review: LLM prompt cache stability + MCP toggleStatus: Merge candidate All CI checks are green. The 2-part system message layout in llm.ts correctly preserves the provider/agent header and first static block for caching while compacting the dynamic tail. The MCP server toggle feature (identical to PR #36) is well-implemented with proper schema, migration, scheduler, and UI changes. Critical dependency: This PR contains all changes from PR #36 plus the LLM cache fix. I'll be closing #36 as superseded and merging this one as the complete implementation. |
Separates LLM system messages: header + static block intact for caching, dynamic tail compacted. 63/63 tests pass, 0 lint errors.