Make chat compaction resumable and secret-safe

Preserve authorization, evidence, pending work, loaded skills, and secret references in a fixed resumable-state summary. Clear the stale context-window cache after compaction and cover the prompt and persistence contracts with focused tests.

Alessandro committed Jul 18, 2026 at 23:15 UTC 425cfc283b3c16e254a41f8d03d1f4cf7dd9a472
4 files changed +66 -16
plugins/_chat_compaction/AGENTS.md
+3
@@ -18,7 +18,10 @@
18 - Preserve chat history integrity and persistence after compaction.
19 - Backup JSON and transcript artifacts must remain UTF-8 writable when chat content contains malformed Unicode such as lone surrogates.
20 - Keep generated summaries bounded by configured model and token limits.
21 +- Compacted summaries must be resumable task state: preserve the latest request, authorization boundaries, decisions, evidence, modified artifacts, pending jobs and their IDs, the next executable step, blockers, and checks not run.
22 - Preserve loaded skill names from `skill_instructions` metadata without copying full skill bodies into compacted summaries.
23 +- Preserve only secret references such as names, aliases, purposes, or storage locations; never preserve secret values.
24 +- Clear the cached context window after replacing history so stale transcript content is not persisted as active resumable state; the cache rebuilds on the next model turn.
25 - After replacing local history, clear the active Responses provider continuation while preserving stored response IDs for cleanup.
26 - Do not discard original context data unless the compaction flow explicitly owns that behavior.
27
plugins/_chat_compaction/helpers/compactor.py
+1
@@ -146,6 +146,7 @@ async def run_compaction(
146 agent.history = History(agent=agent)
147 agent.history.add_message(ai=True, content=compacted_content)
148 clear_responses_provider_state(agent)
149 + agent.data.pop(Agent.DATA_NAME_CTX_WINDOW, None)
150
151 # Clear subordinate chain
152 agent.data.pop(Agent.DATA_NAME_SUBORDINATE, None)
plugins/_chat_compaction/prompts/compact.sys.md
+32 -16
@@ -1,18 +1,34 @@
1 -You are a conversation compactor. Produce the most concise summary possible while preserving critical information.
1 +You are a conversation compactor. Preserve the minimum state another agent needs to resume the task correctly without rereading the original conversation.
2
3 Rules:
4 -- Extract only: key decisions, final outcomes, actionable facts, unresolved items
5 -- Discard: intermediate reasoning, failed attempts, redundant exchanges, pleasantries
6 -- Use terse bullet points, not prose
7 -- Collapse related items into single lines
8 -- Keep exact values: file paths, config values, code identifiers, credentials, URLs
9 -- Preserve loaded skill names from skill_instructions metadata, but do not copy full skill bodies
10 -- Omit anything that can be re-derived from context
11 -- Group by topic, not chronology
12 -- No meta-commentary about the summarization
13 -- Target: 10-20% of original length
14 -
15 -Output format:
16 -- Markdown with short section headers
17 -- Bullet lists, no paragraphs
18 -- Code/paths in backticks inline, not fenced blocks unless multi-line
4 +- Capture the latest active user request, including anything that superseded an earlier request.
5 +- Preserve only explicit or clearly implied authorization and prohibitions; never broaden scope.
6 +- Separate decisions from assumptions and mark unverified assumptions.
7 +- Record completed work only with available evidence such as results, commands, tests, or artifact paths.
8 +- Preserve exact file paths, URLs, config values, code identifiers, job IDs, context IDs, and verification status.
9 +- Record pending workers or jobs with their IDs, state, and the next executable step.
10 +- Record blockers and every relevant check that was not run.
11 +- Preserve loaded skill names from `skill_instructions` metadata, but never copy skill bodies.
12 +- Never include passwords, API keys, tokens, credentials, private keys, session secrets, or other secret values. Preserve only a secret's name, purpose, storage location, or reference alias when needed.
13 +- Discard intermediate reasoning, redundant exchanges, pleasantries, and facts that can be safely re-derived.
14 +- Use terse bullets, no prose or meta-commentary. Target 10-20% of the original length when possible.
15 +
16 +Use exactly these sections in this order. Include `- None recorded.` for an empty section.
17 +
18 +## Current objective and latest user request
19 +
20 +## Authorized scope and prohibited actions
21 +
22 +## Decisions and assumptions
23 +
24 +## Completed work with evidence
25 +
26 +## Modified files and artifacts
27 +
28 +## Pending jobs and next executable step
29 +
30 +## Blockers and checks not run
31 +
32 +## Loaded skill names
33 +
34 +## Secret references
tests/test_chat_compaction.py
+30
@@ -72,6 +72,10 @@ class _CompactionAgent:
72 def __init__(self):
73 self.history = _CompactionHistory()
74 self.data = {
75 + "ctx_window": {
76 + "text": "pre-compaction transcript with secret values",
77 + "tokens": 42,
78 + },
79 "responses_state": {
80 "response_id": "resp_current",
81 "previous_response_id": "resp_previous",
@@ -86,6 +90,31 @@ class _CompactionAgent:
90 self.data[key] = value
91
92
93 +def test_compaction_prompt_is_resumable_task_state_without_secret_values():
94 + prompt = (
95 + PROJECT_ROOT / "plugins" / "_chat_compaction" / "prompts" / "compact.sys.md"
96 + ).read_text(encoding="utf-8")
97 + headings = [
98 + "## Current objective and latest user request",
99 + "## Authorized scope and prohibited actions",
100 + "## Decisions and assumptions",
101 + "## Completed work with evidence",
102 + "## Modified files and artifacts",
103 + "## Pending jobs and next executable step",
104 + "## Blockers and checks not run",
105 + "## Loaded skill names",
106 + "## Secret references",
107 + ]
108 +
109 + positions = [prompt.index(heading) for heading in headings]
110 + assert positions == sorted(positions)
111 + assert "Never include passwords, API keys, tokens, credentials" in prompt
112 + assert "Preserve only a secret's name, purpose, storage location" in prompt
113 + assert "Keep exact values: file paths, config values, code identifiers, credentials" not in prompt
114 + assert "next executable step" in prompt
115 + assert "job IDs" in prompt
116 +
117 +
118 def test_pre_compaction_backup_sanitizes_surrogate_text(tmp_path, monkeypatch):
119 monkeypatch.setattr(
120 compactor,
@@ -193,3 +222,4 @@ async def test_manual_compaction_clears_active_responses_state(monkeypatch):
222 assert "response_id" not in state
223 assert "previous_response_id" not in state
224 assert state["response_ids"] == ["resp_previous", "resp_current"]
225 + assert "ctx_window" not in agent.data