feat(context): persist system prompt in context.jsonl#1417
Conversation
There was a problem hiding this comment.
Pull request overview
Adds persistence of the session’s system prompt into context.jsonl (as a _system_prompt first-line record) so restores can reuse the exact frozen prompt and downstream tools can access the full context.
Changes:
- Extend
Contextto track/read/write a_system_promptrecord and exclude it from in-memory message history. - Update session startup and context lifecycle actions (
/clear, compaction) to (re)write the system prompt record to maintain the “first line” invariant. - Add a dedicated test suite covering system prompt persistence, migration/prepend behavior, and restore/clear/revert semantics.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/kimi_cli/soul/context.py |
Adds _system_prompt state, restoration handling, and write_system_prompt() that prepends/writes prompt record. |
src/kimi_cli/app.py |
Freezes agent system prompt from restored context, or writes it on new/legacy sessions. |
src/kimi_cli/soul/slash.py |
Re-writes system prompt record after /clear. |
src/kimi_cli/soul/kimisoul.py |
Re-writes system prompt record after compaction clears the context. |
tests/core/test_context.py |
New tests validating file format invariants and behavior across restore/clear/revert and legacy migration. |
docs/en/configuration/data-locations.md |
Documents _system_prompt as the first record in context.jsonl and prompt freezing semantics. |
docs/zh/configuration/data-locations.md |
Same documentation update (Chinese). |
docs/en/release-notes/changelog.md |
Adds unreleased entry for system prompt persistence. |
docs/zh/release-notes/changelog.md |
Same release note update (Chinese). |
CHANGELOG.md |
Adds unreleased entry for system prompt persistence. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/kimi_cli/soul/context.py
Outdated
| self._system_prompt = prompt | ||
| prompt_line = json.dumps({"role": "_system_prompt", "content": prompt}) + "\n" | ||
|
|
||
| if not self._file_backend.exists() or self._file_backend.stat().st_size == 0: | ||
| async with aiofiles.open(self._file_backend, "w", encoding="utf-8") as f: | ||
| await f.write(prompt_line) | ||
| else: | ||
| async with aiofiles.open(self._file_backend, encoding="utf-8") as f: | ||
| existing_content = await f.read() | ||
| async with aiofiles.open(self._file_backend, "w", encoding="utf-8") as f: | ||
| await f.write(prompt_line + existing_content) | ||
|
|
There was a problem hiding this comment.
write_system_prompt() prepends by reading the entire existing context file into memory and rewriting it in-place. For large/long-running sessions this can be very memory-heavy, and the non-atomic rewrite risks losing/corrupting the context if the process crashes between truncation and the final write. Consider doing an atomic prepend (write prompt + stream-copy old contents into a temp file in the same directory, then os.replace), and only updating self._system_prompt after the file operation succeeds.
| self._system_prompt = prompt | |
| prompt_line = json.dumps({"role": "_system_prompt", "content": prompt}) + "\n" | |
| if not self._file_backend.exists() or self._file_backend.stat().st_size == 0: | |
| async with aiofiles.open(self._file_backend, "w", encoding="utf-8") as f: | |
| await f.write(prompt_line) | |
| else: | |
| async with aiofiles.open(self._file_backend, encoding="utf-8") as f: | |
| existing_content = await f.read() | |
| async with aiofiles.open(self._file_backend, "w", encoding="utf-8") as f: | |
| await f.write(prompt_line + existing_content) | |
| prompt_line = json.dumps({"role": "_system_prompt", "content": prompt}) + "\n" | |
| # Fast path: file does not exist or is empty; just write the prompt line. | |
| if not self._file_backend.exists() or self._file_backend.stat().st_size == 0: | |
| async with aiofiles.open(self._file_backend, "w", encoding="utf-8") as f: | |
| await f.write(prompt_line) | |
| # Only update in-memory state after the write succeeds. | |
| self._system_prompt = prompt | |
| return | |
| # General case: prepend atomically by writing to a temporary file and replacing. | |
| tmp_path = self._file_backend.with_suffix(self._file_backend.suffix + ".tmp") | |
| # Write prompt line, then stream-copy existing content to the temporary file. | |
| async with aiofiles.open(tmp_path, "w", encoding="utf-8") as tmp_f: | |
| await tmp_f.write(prompt_line) | |
| async with aiofiles.open(self._file_backend, "r", encoding="utf-8") as src_f: | |
| while True: | |
| chunk = await src_f.read(64 * 1024) | |
| if not chunk: | |
| break | |
| await tmp_f.write(chunk) | |
| # Atomically replace the original file with the new one. | |
| await aiofiles.os.replace(tmp_path, self._file_backend) | |
| # Update in-memory state only after the file has been safely replaced. | |
| self._system_prompt = prompt |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d377df93ad
ℹ️ 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".
src/kimi_cli/soul/context.py
Outdated
| async with aiofiles.open(self._file_backend, encoding="utf-8") as f: | ||
| existing_content = await f.read() |
There was a problem hiding this comment.
Stream legacy prompt migration instead of loading full context
write_system_prompt() currently does existing_content = await f.read() and then rewrites the whole file, and this path is exercised on startup for legacy sessions that lack _system_prompt. For large historical contexts (which can include bulky embedded payloads), this can cause a large transient memory spike or OOM during restore, and because it rewrites in place it also risks leaving a truncated context file if the process is interrupted mid-write; prepend should be implemented via streamed copy into a temp file and atomic replace.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5c2e4dde2c
ℹ️ 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".
| if context.system_prompt is not None: | ||
| agent = dataclasses.replace(agent, system_prompt=context.system_prompt) |
There was a problem hiding this comment.
Honor explicit agent selection on session restore
KimiCLI.create() loads the agent selected by --agent/--agent-file, but when a persisted _system_prompt exists it unconditionally overwrites that prompt (dataclasses.replace(...)). Since the CLI advertises these flags as “agent specification to use” (src/kimi_cli/cli/__init__.py), resuming a session with an explicit agent now silently keeps the old prompt while keeping the newly loaded toolset, which can produce prompt/tool mismatches and prevents intended agent switching.
Useful? React with 👍 / 👎.
Signed-off-by: Kai <me@kaiyi.cool>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0a12a679bc
ℹ️ 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".
| line = line.strip() | ||
| if not line: | ||
| continue | ||
| role = json.loads(line).get("role") |
There was a problem hiding this comment.
Guard role lookup against non-object context lines
Session.is_empty() now parses each context.jsonl line and immediately calls .get("role"), but valid JSON lines that are not objects (for example [] or null from manual edits/recovery tooling) raise AttributeError, which is not covered by the current except (OSError, ValueError, TypeError) block. This exception can bubble out of Session.list()/Session.continue_() and break session discovery instead of safely treating the session as unreadable.
Useful? React with 👍 / 👎.
Summary
_system_promptspecial record (first line) incontext.jsonl, so frontends/visualization tools can read the full conversation context/clearand/compactto maintain the invariantChanges
context.py: add_system_promptfield,system_promptproperty,write_system_prompt()method; handle inrestore(),clear(),revert_to()app.py: freeze or write system prompt after context restorekimisoul.py: re-write system prompt after compaction clearslash.py: re-write system prompt after/cleartests/core/test_context.pywith 14 tests covering write, restore, clear, revert, round-trip, and file formatChecklist
make gen-changelogto update the changelog.make gen-docsto update the user documentation.