Skip to content

feat(context): persist system prompt in context.jsonl#1417

Merged
RealKai42 merged 5 commits intomainfrom
kaiyi/session-sysprompt-sync
Mar 12, 2026
Merged

feat(context): persist system prompt in context.jsonl#1417
RealKai42 merged 5 commits intomainfrom
kaiyi/session-sysprompt-sync

Conversation

@RealKai42
Copy link
Copy Markdown
Collaborator

@RealKai42 RealKai42 commented Mar 12, 2026

Summary

  • Persist the system prompt as a _system_prompt special record (first line) in context.jsonl, so frontends/visualization tools can read the full conversation context
  • On session restore, reuse the frozen system prompt instead of regenerating it
  • On new sessions or legacy sessions without a system prompt, write/prepend it to context.jsonl
  • Re-write system prompt after /clear and /compact to maintain the invariant

Changes

  • context.py: add _system_prompt field, system_prompt property, write_system_prompt() method; handle in restore(), clear(), revert_to()
  • app.py: freeze or write system prompt after context restore
  • kimisoul.py: re-write system prompt after compaction clear
  • slash.py: re-write system prompt after /clear
  • New tests/core/test_context.py with 14 tests covering write, restore, clear, revert, round-trip, and file format

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked the related issue, if any.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have run make gen-changelog to update the changelog.
  • I have run make gen-docs to update the user documentation.

Open with Devin

Copilot AI review requested due to automatic review settings March 12, 2026 08:05
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Context to track/read/write a _system_prompt record 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.

Comment on lines +82 to +93
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)

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +89 to +90
async with aiofiles.open(self._file_backend, encoding="utf-8") as f:
existing_content = await f.read()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +168 to +169
if context.system_prompt is not None:
agent = dataclasses.replace(agent, system_prompt=context.system_prompt)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@RealKai42 RealKai42 merged commit 7a47524 into main Mar 12, 2026
11 of 14 checks passed
@RealKai42 RealKai42 deleted the kaiyi/session-sysprompt-sync branch March 12, 2026 08:33
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants