MoonshotAI / MoonshotAI/kimi-code
[RFC] make system-prompt renders deterministic
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 7.5k
- Forks
- 1.2k
- Avg merge
- 11h 53m
- Merged PRs (30d)
- 350
Description
What feature would you like to see?
The system prompt embeds new Date().toISOString() — millisecond precision. Every re-render therefore produces a byte-different prompt even when nothing the model cares about changed. Server-side prefix caching is a longest-common-prefix match, so one differing token ends the match: everything after it is recomputed, including the byte-identical remainder, the tools array that follows the system prompt on the wire, and the whole accumulated history of that session.
Proposal
- Short term — make an unchanged render produce an unchanged string: day-granular
${now}, plus a dirty check and two related fixes. - Longer term — stop carrying dynamic content in the system prompt at all; deliver it as appended history messages, the way codex does.
Short term
Five paths re-render the whole prompt mid-session:
| trigger | source |
|---|---|
| session tool-policy change | profileService.ts:179 |
| AGENTS.md watcher | profileService.ts:184 |
tools config section change |
profileService.ts:190 |
| builtin skill catalog change | profileService.ts:197 |
| after every full compaction | fullCompactionService.ts:570 |
All five reach refreshSystemPrompt() (profileService.ts:425), which rebuilds the context — reading the clock again at profileService.ts:865 — and writes the result unconditionally:
context = await this.buildSystemPromptContext(profile); // fresh this.clock.now()
const rendered = profile.renderSystemPrompt(context);
this.update({ systemPrompt: rendered.text, ... }); // no comparison
The next request picks the new text up at llmRequesterService.ts:600.
There is a guard one layer down, but it cannot fire (profileOps.ts:124-128) — environmentDisclosure is supplied on every render, so the || makes the equality test irrelevant:
if (e.systemPrompt !== undefined &&
(e.systemPrompt !== s.systemPrompt ||
e.environmentDisclosure !== undefined || // always defined since #2564
e.renderGeneration !== undefined)) { ... }
The compaction path is the clearest case: compaction changes neither cwd, nor the directory listing, nor AGENTS.md, nor the skills list. The only observable effect of that refreshSystemPrompt() call is a new timestamp — on the very request that already pays for a rebuilt history.
Proposed changes:
- Render
${now}at day granularity. - Give
refreshSystemPrompt()a dirty check — skip the update when the rendered text is unchanged. - Repair the
profileOps.tsguard. - Drop or condition the unconditional refresh after compaction.
- Fix
system.md:83, which describes behavior the code does not implement (below).
Day granularity
dateChangeService already works at day granularity: it stays silent unless the local calendar date advanced (dateChangeService.ts:56-66), and the reminder it appends tells the model that the timestamp in the system prompt is stale and to rely on the reminder instead. Sub-day precision in the prompt has no consumer — it only guarantees that every render differs.
The helper already exists in the same file (localDateKey, profile-shared.ts:164) and AgentProfileContext already carries timeZone (agentProfileCatalog.ts:27). Both are module-private, so no new exports:
// profile-shared.ts:79
- now: context.now ?? new Date().toISOString(),
+ now: localDateKey(context.now, context.timeZone ?? localTimeZone()),
This also keeps the prompt's date and the date_change reminder's date coming from one function, so the two cannot drift in format or timezone handling. The ## Date and Time section stays; only the value changes, and the instruction to fetch the real time from the environment when it matters is untouched.
Longer term
Everything after ${now} in the template is dynamic: ${cwd}, ${cwd_listing}, ${additional_dirs_section}, ${agents_md}, ${skills_section}, ${plugin_sections}. Today a change to any of them is delivered by re-rendering the prompt in place, which discards the prefix from that point on.
codex takes the opposite approach: base instructions are completely static — codex-rs/core/gpt_5_codex_prompt.md contains zero template variables — and every dynamic value lives in the conversation history as a user message. cwd, AGENTS.md and skills are all "contextual user fragments" (context/contextual_user_message.rs:18-31), each backed by a world-state section implementing render_diff(previous) -> Option<...> (context/world_state/mod.rs:212-245) that returns None when nothing changed. The turn loop records only when the result is non-empty (session/mod.rs:3091-3095):
let items = merge_contextual_fragments(world_state.render_diff(&previous_snapshot));
if !items.is_empty() {
self.record_conversation_items(turn_context, &items).await;
}
The AGENTS.md section is the closest analogue to what is proposed here — on change it appends a fresh fragment carrying an explicit supersede notice rather than editing the earlier one (context/world_state/agents_md.rs:52-78):
const REPLACEMENT_NOTICE: &str =
"These AGENTS.md instructions replace all previously provided AGENTS.md instructions.";
const REMOVAL_NOTICE: &str =
"The previously provided AGENTS.md instructions no longer apply.";
The machinery for this already exists here. contextInjectorService.appendResult() has only two exits, appendSystemReminder() and context.append(); nothing splices or rewrites. Seven variants already ride it: swarm_mode, plan_mode, date_change, plugin_session_start, plugin_change, permission_mode, background_task_status. agentsMdReminderService.ts:147 already appends for AGENTS.md discovery. And #2316 already implemented a skill_list reminder before being closed.
Additional information
No response
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with refreshSystemPrompt() in profileService.ts and trace the five callers, then read profile-shared.ts, profileOps.ts, fullCompactionService.ts, and system.md. Compare the proposed day-granular value and dirty-check behavior with the existing dateChangeService flow. Done means the chosen short-term scope is implemented without rewriting unchanged prompt content, and system.md matches the resulting behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- cli, developer-experience
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100