claude-dev-env 8.90.0 → 8.91.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,20 +1,16 @@
1
1
  #!/usr/bin/env python3
2
- """Write every message the user typed in a session to a file, and print its path.
2
+ """Read every message the user typed from a Claude Code or Codex transcript.
3
3
 
4
4
  ::
5
5
 
6
- python3 compaction_user_messages.py ~/.claude/projects/p/abc.jsonl
7
- -> ~/.claude/compaction-recall/abc.md
6
+ Claude Code entry {"type": "user", "message": {"content": "fix login"}}
7
+ Codex entry {"type": "event_msg",
8
+ "payload": {"type": "user_message", "message": "fix login"}}
9
+ -> TypedMessage(position=1, timestamp=..., text="fix login")
8
10
 
9
- The agent runs this after a compaction. Only the path reaches its context.
10
- A PostToolUse agent hook reads the file, condenses it, and returns the
11
- condensed messages to the agent.
12
-
13
- The file holds each typed message with its number and timestamp, oldest
14
- first. The trim rules live in the hook prompt. The file sits under
15
- ``~/.claude`` so the hook's Read permission rule can name its folder.
16
- The whole transcript is read each time, so a second compaction yields
17
- the same messages plus the newer ones.
11
+ The compaction recall hook trims these messages and returns them to the
12
+ agent after a compaction. The whole transcript is read each time, so a second
13
+ compaction yields the same messages plus the newer ones.
18
14
  """
19
15
 
20
16
  from __future__ import annotations
@@ -30,31 +26,26 @@ if _hooks_directory not in sys.path:
30
26
  sys.path.insert(0, _hooks_directory)
31
27
 
32
28
  from hooks_constants.compaction_user_messages_constants import (
33
- ALL_RECALL_DIRECTORY_PARTS,
34
29
  ALL_SKIPPED_ENTRY_FLAGS,
35
30
  ASSISTANT_ENTRY_TYPE,
36
31
  ATTACHMENT_ENTRY_TYPE,
37
32
  ATTACHMENT_KEY,
38
33
  BLOCK_SEPARATOR,
39
34
  BLOCK_TYPE_KEY,
35
+ CODEX_AGENT_MESSAGE_TYPE,
36
+ CODEX_EVENT_ENTRY_TYPE,
37
+ CODEX_IMAGES_KEY,
38
+ CODEX_PAYLOAD_KEY,
39
+ CODEX_USER_MESSAGE_TYPE,
40
40
  CONTENT_KEY,
41
- CUT_MESSAGE_SUFFIX_TEMPLATE,
42
41
  ENTRY_TYPE_KEY,
43
42
  HARNESS_TAG_OPENING_PATTERN,
44
43
  HUMAN_ORIGIN_KIND,
45
44
  IMAGE_BLOCK_TYPE,
46
45
  IMAGE_PLACEHOLDER,
47
- LEFT_OUT_MESSAGES_TEMPLATE,
48
- MAXIMUM_EXTRACT_CHARACTERS,
49
- MAXIMUM_MESSAGE_CHARACTERS,
50
- MESSAGE_HEADING_TEMPLATE,
51
46
  MESSAGE_KEY,
52
- MESSAGES_HEADING,
53
- NO_MESSAGES_TEXT,
54
47
  ORIGIN_KEY,
55
48
  ORIGIN_KIND_KEY,
56
- OUTPUT_ENCODING,
57
- OUTPUT_FILE_SUFFIX,
58
49
  QUEUED_COMMAND_ATTACHMENT_TYPE,
59
50
  QUEUED_PROMPT_KEY,
60
51
  SLASH_COMMAND_ARGS_PATTERN,
@@ -62,13 +53,8 @@ from hooks_constants.compaction_user_messages_constants import (
62
53
  TEXT_BLOCK_TYPE,
63
54
  TEXT_KEY,
64
55
  TIMESTAMP_KEY,
65
- TRANSCRIPT_ARGUMENT_COUNT,
66
- USAGE_EXIT_CODE,
67
- USAGE_TEXT,
68
56
  USER_ENTRY_TYPE,
69
57
  )
70
- from hooks_constants.session_file_id import session_file_id
71
- from hooks_constants.setup_project_paths_constants import DECODE_ERRORS_POLICY, UTF8_ENCODING
72
58
 
73
59
 
74
60
  @dataclass(frozen=True)
@@ -131,8 +117,34 @@ def _is_typed_by_a_person(all_entry_fields: dict[str, object]) -> bool:
131
117
  return not isinstance(origin, dict) or origin.get(ORIGIN_KIND_KEY) == HUMAN_ORIGIN_KIND
132
118
 
133
119
 
120
+ def _codex_payload(all_entry_fields: dict[str, object]) -> dict[str, object]:
121
+ if all_entry_fields.get(ENTRY_TYPE_KEY) != CODEX_EVENT_ENTRY_TYPE:
122
+ return {}
123
+ payload = all_entry_fields.get(CODEX_PAYLOAD_KEY)
124
+ return payload if isinstance(payload, dict) else {}
125
+
126
+
127
+ def _codex_user_text(all_payload_fields: dict[str, object]) -> str | None:
128
+ message = all_payload_fields.get(MESSAGE_KEY)
129
+ typed_text = _typed_text(message) if isinstance(message, str) else None
130
+ if not all_payload_fields.get(CODEX_IMAGES_KEY):
131
+ return typed_text
132
+ return BLOCK_SEPARATOR.join(
133
+ each_part for each_part in (typed_text, IMAGE_PLACEHOLDER) if each_part
134
+ )
135
+
136
+
137
+ def _is_assistant_turn(all_entry_fields: dict[str, object]) -> bool:
138
+ if all_entry_fields.get(ENTRY_TYPE_KEY) == ASSISTANT_ENTRY_TYPE:
139
+ return True
140
+ return _codex_payload(all_entry_fields).get(ENTRY_TYPE_KEY) == CODEX_AGENT_MESSAGE_TYPE
141
+
142
+
134
143
  def _entry_text(all_entry_fields: dict[str, object]) -> str | None:
135
144
  entry_type = all_entry_fields.get(ENTRY_TYPE_KEY)
145
+ codex_payload = _codex_payload(all_entry_fields)
146
+ if codex_payload.get(ENTRY_TYPE_KEY) == CODEX_USER_MESSAGE_TYPE:
147
+ return _codex_user_text(codex_payload)
136
148
  if entry_type == USER_ENTRY_TYPE and _is_typed_by_a_person(all_entry_fields):
137
149
  message = all_entry_fields.get(MESSAGE_KEY)
138
150
  content = message.get(CONTENT_KEY) if isinstance(message, dict) else None
@@ -152,7 +164,7 @@ def _typed_texts(all_transcript_lines: Iterable[str]) -> Iterator[tuple[str, str
152
164
  previous_text = None
153
165
  for each_line in all_transcript_lines:
154
166
  all_entry_fields = _parsed_entry(each_line)
155
- if all_entry_fields.get(ENTRY_TYPE_KEY) == ASSISTANT_ENTRY_TYPE:
167
+ if _is_assistant_turn(all_entry_fields):
156
168
  previous_text = None
157
169
  continue
158
170
  entry_text = _entry_text(all_entry_fields)
@@ -174,6 +186,8 @@ def typed_user_messages(all_transcript_lines: Iterable[str]) -> list[TypedMessag
174
186
  isCompactSummary, isSidechain, origin peer -> left out
175
187
  user "<system-reminder>...", "<local-command-stdout>" -> left out
176
188
  attachment queued_command "also check logs" -> kept
189
+ Codex event_msg user_message "fix it" -> kept
190
+ Codex event_msg user_message with images -> text plus "[image]"
177
191
 
178
192
  A message repeated right after itself, such as a queued prompt that later
179
193
  lands as a user entry, counts once.
@@ -190,118 +204,3 @@ def typed_user_messages(all_transcript_lines: Iterable[str]) -> list[TypedMessag
190
204
  _typed_texts(all_transcript_lines), start=1
191
205
  )
192
206
  ]
193
-
194
-
195
- def _capped_text(text: str) -> str:
196
- if len(text) <= MAXIMUM_MESSAGE_CHARACTERS:
197
- return text
198
- cut_count = len(text) - MAXIMUM_MESSAGE_CHARACTERS
199
- return text[:MAXIMUM_MESSAGE_CHARACTERS] + CUT_MESSAGE_SUFFIX_TEMPLATE.format(
200
- cut_count=cut_count
201
- )
202
-
203
-
204
- def _rendered_message(message: TypedMessage) -> str:
205
- return MESSAGE_HEADING_TEMPLATE.format(
206
- position=message.position,
207
- timestamp=message.timestamp,
208
- text=_capped_text(message.text),
209
- )
210
-
211
-
212
- def bounded_messages(all_messages: list[TypedMessage]) -> list[TypedMessage]:
213
- """Return the messages that fit the extract budget: the first, then the newest.
214
-
215
- ::
216
-
217
- 5 short messages -> all 5
218
- #1 plus 400 long messages -> #1, then the newest that fit
219
-
220
- The first message keeps the session's original ask. The newest carry the
221
- latest corrections, which override older asks.
222
-
223
- Args:
224
- all_messages: Every typed message, oldest first.
225
-
226
- Returns:
227
- The kept messages, oldest first.
228
- """
229
- if not all_messages:
230
- return []
231
- first_message, *all_later_messages = all_messages
232
- remaining_characters = MAXIMUM_EXTRACT_CHARACTERS - len(_rendered_message(first_message))
233
- all_kept_later_messages: list[TypedMessage] = []
234
- for each_message in reversed(all_later_messages):
235
- message_size = len(_rendered_message(each_message))
236
- if message_size > remaining_characters:
237
- break
238
- remaining_characters -= message_size
239
- all_kept_later_messages.append(each_message)
240
- return [first_message, *reversed(all_kept_later_messages)]
241
-
242
-
243
- def rendered_extract(all_messages: list[TypedMessage]) -> str:
244
- """Return the file text: a heading, then the kept messages.
245
-
246
- ::
247
-
248
- [#1 "fix login"] -> "# User messages, oldest first" + "## 1 | <ts>\\nfix login"
249
-
250
- Args:
251
- all_messages: Every typed message, oldest first.
252
-
253
- Returns:
254
- The text the extractor writes to its output file.
255
- """
256
- if not all_messages:
257
- return MESSAGES_HEADING + NO_MESSAGES_TEXT
258
- all_kept_messages = bounded_messages(all_messages)
259
- left_out_count = len(all_messages) - len(all_kept_messages)
260
- rendered_parts = [MESSAGES_HEADING]
261
- for each_index, each_message in enumerate(all_kept_messages):
262
- if each_index == 1 and left_out_count:
263
- rendered_parts.append(LEFT_OUT_MESSAGES_TEMPLATE.format(left_out_count=left_out_count))
264
- rendered_parts.append(_rendered_message(each_message))
265
- return "".join(rendered_parts)
266
-
267
-
268
- def output_path_for(transcript_path: Path) -> Path:
269
- """Return the recall file for one transcript, named after its session id.
270
-
271
- ::
272
-
273
- ~/.claude/projects/p/abc-123.jsonl -> ~/.claude/compaction-recall/abc-123.md
274
-
275
- Args:
276
- transcript_path: The session transcript.
277
-
278
- Returns:
279
- The path the extractor writes; a rerun overwrites it.
280
- """
281
- file_name = f"{session_file_id(transcript_path.stem)}{OUTPUT_FILE_SUFFIX}"
282
- return Path.home().joinpath(*ALL_RECALL_DIRECTORY_PARTS, file_name)
283
-
284
-
285
- def main() -> int:
286
- """Write the extract for the transcript named on the command line and print its path."""
287
- if len(sys.argv) != TRANSCRIPT_ARGUMENT_COUNT:
288
- sys.stderr.write(USAGE_TEXT)
289
- return USAGE_EXIT_CODE
290
- transcript_path = Path(sys.argv[1])
291
- try:
292
- with transcript_path.open(
293
- encoding=UTF8_ENCODING, errors=DECODE_ERRORS_POLICY
294
- ) as transcript:
295
- all_messages = typed_user_messages(transcript)
296
- except OSError as read_error:
297
- sys.stderr.write(f"cannot read {transcript_path}: {read_error}\n")
298
- return USAGE_EXIT_CODE
299
- output_path = output_path_for(transcript_path)
300
- output_path.parent.mkdir(parents=True, exist_ok=True)
301
- output_path.write_text(rendered_extract(all_messages), encoding=OUTPUT_ENCODING)
302
- sys.stdout.write(f"{output_path}\n")
303
- return 0
304
-
305
-
306
- if __name__ == "__main__":
307
- sys.exit(main())
@@ -0,0 +1,22 @@
1
+ {"timestamp": "2026-10-19T09:00:00.000Z", "type": "session_meta", "payload": {"id": "0199f0aa-1111-7000-8000-000000000001", "cwd": "/workspace/app", "originator": "codex_cli_rs", "cli_version": "0.47.0"}}
2
+ {"timestamp": "2026-10-19T09:00:00.100Z", "type": "response_item", "payload": {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "<environment_context>\n <cwd>/workspace/app</cwd>\n</environment_context>"}]}}
3
+ {"timestamp": "2026-10-19T09:02:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "Could you please move the nightly export to cron at 03:30 UTC on host atlas-12.internal?"}]}}
4
+ {"timestamp": "2026-10-19T09:02:00.000Z", "type": "event_msg", "payload": {"type": "user_message", "message": "Could you please move the nightly export to cron at 03:30 UTC on host atlas-12.internal?", "kind": "plain"}}
5
+ {"timestamp": "2026-10-19T09:03:00.000Z", "type": "event_msg", "payload": {"type": "agent_message", "message": "Moving the export."}}
6
+ {"timestamp": "2026-10-19T09:03:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "Moving the export."}]}}
7
+ {"timestamp": "2026-10-19T09:04:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "Keep the export output in Redis for now."}]}}
8
+ {"timestamp": "2026-10-19T09:04:00.000Z", "type": "event_msg", "payload": {"type": "user_message", "message": "Keep the export output in Redis for now.", "kind": "plain"}}
9
+ {"timestamp": "2026-10-19T09:05:00.000Z", "type": "event_msg", "payload": {"type": "agent_message", "message": "Done."}}
10
+ {"timestamp": "2026-10-19T09:05:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "Done."}]}}
11
+ {"timestamp": "2026-10-19T09:06:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "ok thanks"}]}}
12
+ {"timestamp": "2026-10-19T09:06:00.000Z", "type": "event_msg", "payload": {"type": "user_message", "message": "ok thanks", "kind": "plain"}}
13
+ {"timestamp": "2026-10-19T09:07:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "Also cap the batch at 25,000 rows and log to logs/export_timing.csv"}]}}
14
+ {"timestamp": "2026-10-19T09:07:00.000Z", "type": "event_msg", "payload": {"type": "user_message", "message": "Also cap the batch at 25,000 rows and log to logs/export_timing.csv", "kind": "plain"}}
15
+ {"timestamp": "2026-10-19T09:08:00.000Z", "type": "event_msg", "payload": {"type": "agent_message", "message": "Capped."}}
16
+ {"timestamp": "2026-10-19T09:08:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "Capped."}]}}
17
+ {"timestamp": "2026-10-19T09:09:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "Change of plan, put the export output in Postgres instead of Redis."}]}}
18
+ {"timestamp": "2026-10-19T09:09:00.000Z", "type": "event_msg", "payload": {"type": "user_message", "message": "Change of plan, put the export output in Postgres instead of Redis.", "kind": "plain"}}
19
+ {"timestamp": "2026-10-19T09:10:00.000Z", "type": "event_msg", "payload": {"type": "agent_message", "message": "Switched."}}
20
+ {"timestamp": "2026-10-19T09:10:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "Switched."}]}}
21
+ {"timestamp": "2026-10-19T09:11:00.000Z", "type": "response_item", "payload": {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "Ping Marisol when PR https://github.com/acme/export/pull/431 is green."}]}}
22
+ {"timestamp": "2026-10-19T09:11:00.000Z", "type": "event_msg", "payload": {"type": "user_message", "message": "Ping Marisol when PR https://github.com/acme/export/pull/431 is green.", "kind": "plain"}}
@@ -0,0 +1,42 @@
1
+ {"type": "user", "timestamp": "2026-10-19T10:00:07Z", "message": {"role": "user", "content": "just to be clear, we need to migrate the tundra service to the new cluster this week"}, "origin": {"kind": "human"}}
2
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:08Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
3
+ {"type": "user", "timestamp": "2026-10-19T10:00:08Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_8", "content": "exit 0"}]}}
4
+ {"type": "user", "timestamp": "2026-10-19T10:00:14Z", "message": {"role": "user", "content": "go ahead"}, "origin": {"kind": "human"}}
5
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:15Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
6
+ {"type": "user", "timestamp": "2026-10-19T10:00:15Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_15", "content": "exit 0"}]}}
7
+ {"type": "user", "timestamp": "2026-10-19T10:00:21Z", "message": {"role": "user", "content": "could you please make sure that open the PR against https://github.com/pixel-co/ember/pull/195 and ask Priya to review it"}, "origin": {"kind": "human"}}
8
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:22Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
9
+ {"type": "user", "timestamp": "2026-10-19T10:00:22Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_22", "content": "exit 0"}]}}
10
+ {"type": "user", "timestamp": "2026-10-19T10:00:28Z", "message": {"role": "user", "content": "go ahead"}, "origin": {"kind": "human"}}
11
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:29Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
12
+ {"type": "user", "timestamp": "2026-10-19T10:00:29Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_29", "content": "exit 0"}]}}
13
+ {"type": "user", "timestamp": "2026-10-19T10:00:35Z", "message": {"role": "user", "content": "I would like it if push the work to the branch feat/pixel-ember, not main"}, "origin": {"kind": "human"}}
14
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:36Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
15
+ {"type": "user", "timestamp": "2026-10-19T10:00:36Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_36", "content": "exit 0"}]}}
16
+ {"type": "user", "timestamp": "2026-10-19T10:00:42Z", "message": {"role": "user", "content": "just to be clear, use MongoDB as the cache for the tundra service"}, "origin": {"kind": "human"}}
17
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:43Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
18
+ {"type": "user", "timestamp": "2026-10-19T10:00:43Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_43", "content": "exit 0"}]}}
19
+ {"type": "user", "timestamp": "2026-10-19T10:00:49Z", "message": {"role": "user", "content": "so basically cap each batch at 47,000 rows so memory stays flat"}, "origin": {"kind": "human"}}
20
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:50Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
21
+ {"type": "user", "timestamp": "2026-10-19T10:00:50Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_50", "content": "exit 0"}]}}
22
+ {"type": "user", "timestamp": "2026-10-19T10:00:56Z", "message": {"role": "user", "content": "could you please make sure that the job lives on willow-16.internal and fires at 12:15 UTC, so do not move it"}, "origin": {"kind": "human"}}
23
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:57Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
24
+ {"type": "user", "timestamp": "2026-10-19T10:00:57Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_57", "content": "exit 0"}]}}
25
+ {"type": "user", "timestamp": "2026-10-19T10:01:03Z", "message": {"role": "user", "content": "[Request interrupted by user]"}, "origin": {"kind": "human"}}
26
+ {"type": "assistant", "timestamp": "2026-10-19T10:01:04Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
27
+ {"type": "user", "timestamp": "2026-10-19T10:01:04Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_64", "content": "exit 0"}]}}
28
+ {"type": "user", "timestamp": "2026-10-19T10:01:10Z", "message": {"role": "user", "content": "go ahead"}, "origin": {"kind": "human"}}
29
+ {"type": "assistant", "timestamp": "2026-10-19T10:01:11Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
30
+ {"type": "user", "timestamp": "2026-10-19T10:01:11Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_71", "content": "exit 0"}]}}
31
+ {"type": "user", "timestamp": "2026-10-19T10:01:17Z", "message": {"role": "user", "content": "thanks"}, "origin": {"kind": "human"}}
32
+ {"type": "assistant", "timestamp": "2026-10-19T10:01:18Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
33
+ {"type": "user", "timestamp": "2026-10-19T10:01:18Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_78", "content": "exit 0"}]}}
34
+ {"type": "user", "timestamp": "2026-10-19T10:01:24Z", "message": {"role": "user", "content": "could you please make sure that leave src/pixel/ember_nimbus.py alone, the finance team still imports it"}, "origin": {"kind": "human"}}
35
+ {"type": "assistant", "timestamp": "2026-10-19T10:01:25Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
36
+ {"type": "user", "timestamp": "2026-10-19T10:01:25Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_85", "content": "exit 0"}]}}
37
+ {"type": "user", "timestamp": "2026-10-19T10:01:31Z", "message": {"role": "user", "content": "scratch the MongoDB idea, switch to Postgres."}, "origin": {"kind": "human"}}
38
+ {"type": "assistant", "timestamp": "2026-10-19T10:01:32Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
39
+ {"type": "user", "timestamp": "2026-10-19T10:01:32Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_92", "content": "exit 0"}]}}
40
+ {"type": "user", "timestamp": "2026-10-19T10:01:38Z", "message": {"role": "user", "content": "yep"}, "origin": {"kind": "human"}}
41
+ {"type": "assistant", "timestamp": "2026-10-19T10:01:39Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
42
+ {"type": "user", "timestamp": "2026-10-19T10:01:39Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_99", "content": "exit 0"}]}}
@@ -0,0 +1,42 @@
1
+ {"type": "user", "timestamp": "2026-10-19T10:00:07Z", "message": {"role": "user", "content": "could you please make sure that we need to migrate the summit service to the new cluster this week"}, "origin": {"kind": "human"}}
2
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:08Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
3
+ {"type": "user", "timestamp": "2026-10-19T10:00:08Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_8", "content": "exit 0"}]}}
4
+ {"type": "user", "timestamp": "2026-10-19T10:00:14Z", "message": {"role": "user", "content": "ok"}, "origin": {"kind": "human"}}
5
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:15Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
6
+ {"type": "user", "timestamp": "2026-10-19T10:00:15Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_15", "content": "exit 0"}]}}
7
+ {"type": "user", "timestamp": "2026-10-19T10:00:21Z", "message": {"role": "user", "content": "I think pin the client library to 4.7.9, newer builds break the parser"}, "origin": {"kind": "human"}}
8
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:22Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
9
+ {"type": "user", "timestamp": "2026-10-19T10:00:22Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_22", "content": "exit 0"}]}}
10
+ {"type": "user", "timestamp": "2026-10-19T10:00:28Z", "message": {"role": "user", "content": "could you please make sure that use DynamoDB as the cache for the summit service"}, "origin": {"kind": "human"}}
11
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:29Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
12
+ {"type": "user", "timestamp": "2026-10-19T10:00:29Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_29", "content": "exit 0"}]}}
13
+ {"type": "user", "timestamp": "2026-10-19T10:00:35Z", "message": {"role": "user", "content": "just to be clear, open the PR against https://github.com/willow-co/orbit/pull/207 and ask Lena to review it"}, "origin": {"kind": "human"}}
14
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:36Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
15
+ {"type": "user", "timestamp": "2026-10-19T10:00:36Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_36", "content": "exit 0"}]}}
16
+ {"type": "user", "timestamp": "2026-10-19T10:00:42Z", "message": {"role": "user", "content": "yep"}, "origin": {"kind": "human"}}
17
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:43Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
18
+ {"type": "user", "timestamp": "2026-10-19T10:00:43Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_43", "content": "exit 0"}]}}
19
+ {"type": "user", "timestamp": "2026-10-19T10:00:49Z", "message": {"role": "user", "content": "just to be clear, the job lives on vector-71.internal and fires at 13:15 UTC, so do not move it"}, "origin": {"kind": "human"}}
20
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:50Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
21
+ {"type": "user", "timestamp": "2026-10-19T10:00:50Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_50", "content": "exit 0"}]}}
22
+ {"type": "user", "timestamp": "2026-10-19T10:00:56Z", "message": {"role": "user", "content": "go ahead"}, "origin": {"kind": "human"}}
23
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:57Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
24
+ {"type": "user", "timestamp": "2026-10-19T10:00:57Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_57", "content": "exit 0"}]}}
25
+ {"type": "user", "timestamp": "2026-10-19T10:01:03Z", "message": {"role": "user", "content": "just to be clear, leave src/willow/orbit_quartz.py alone, the finance team still imports it"}, "origin": {"kind": "human"}}
26
+ {"type": "assistant", "timestamp": "2026-10-19T10:01:04Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
27
+ {"type": "user", "timestamp": "2026-10-19T10:01:04Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_64", "content": "exit 0"}]}}
28
+ {"type": "user", "timestamp": "2026-10-19T10:01:10Z", "message": {"role": "user", "content": "yep"}, "origin": {"kind": "human"}}
29
+ {"type": "assistant", "timestamp": "2026-10-19T10:01:11Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
30
+ {"type": "user", "timestamp": "2026-10-19T10:01:11Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_71", "content": "exit 0"}]}}
31
+ {"type": "user", "timestamp": "2026-10-19T10:01:17Z", "message": {"role": "user", "content": "just to be clear, cap each batch at 40,000 rows so memory stays flat"}, "origin": {"kind": "human"}}
32
+ {"type": "assistant", "timestamp": "2026-10-19T10:01:18Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
33
+ {"type": "user", "timestamp": "2026-10-19T10:01:18Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_78", "content": "exit 0"}]}}
34
+ {"type": "user", "timestamp": "2026-10-19T10:01:24Z", "message": {"role": "user", "content": "[Request interrupted by user]"}, "origin": {"kind": "human"}}
35
+ {"type": "assistant", "timestamp": "2026-10-19T10:01:25Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
36
+ {"type": "user", "timestamp": "2026-10-19T10:01:25Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_85", "content": "exit 0"}]}}
37
+ {"type": "user", "timestamp": "2026-10-19T10:01:31Z", "message": {"role": "user", "content": "update: ops vetoed DynamoDB, so S3 it is."}, "origin": {"kind": "human"}}
38
+ {"type": "assistant", "timestamp": "2026-10-19T10:01:32Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
39
+ {"type": "user", "timestamp": "2026-10-19T10:01:32Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_92", "content": "exit 0"}]}}
40
+ {"type": "user", "timestamp": "2026-10-19T10:01:38Z", "message": {"role": "user", "content": "go ahead"}, "origin": {"kind": "human"}}
41
+ {"type": "assistant", "timestamp": "2026-10-19T10:01:39Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
42
+ {"type": "user", "timestamp": "2026-10-19T10:01:39Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_99", "content": "exit 0"}]}}
@@ -22,6 +22,8 @@
22
22
  "transcript": "long_session.jsonl",
23
23
  "facts": [
24
24
  "tools/photo_import/",
25
+ "yes go ahead",
26
+ "keep going",
25
27
  "90 seconds",
26
28
  "2,000",
27
29
  "exif_reader.py",
@@ -43,5 +45,49 @@
43
45
  "prompt hook"
44
46
  ],
45
47
  "overruled": []
48
+ },
49
+ {
50
+ "transcript": "codex_rollout.jsonl",
51
+ "facts": [
52
+ "03:30 UTC",
53
+ "atlas-12.internal",
54
+ "Redis",
55
+ "Postgres",
56
+ "25,000 rows",
57
+ "logs/export_timing.csv",
58
+ "Marisol",
59
+ "https://github.com/acme/export/pull/431"
60
+ ],
61
+ "overruled": [{"earlier": 2, "later": 5}]
62
+ },
63
+ {
64
+ "transcript": "random_9031_1.jsonl",
65
+ "facts": [
66
+ "tundra",
67
+ "https://github.com/pixel-co/ember/pull/195",
68
+ "Priya",
69
+ "feat/pixel-ember",
70
+ "47,000 rows",
71
+ "willow-16.internal",
72
+ "12:15 UTC",
73
+ "src/pixel/ember_nimbus.py",
74
+ "Postgres"
75
+ ],
76
+ "overruled": [{"earlier": 6, "later": 13}]
77
+ },
78
+ {
79
+ "transcript": "random_9031_2.jsonl",
80
+ "facts": [
81
+ "summit",
82
+ "4.7.9",
83
+ "https://github.com/willow-co/orbit/pull/207",
84
+ "Lena",
85
+ "vector-71.internal",
86
+ "13:15 UTC",
87
+ "src/willow/orbit_quartz.py",
88
+ "40,000 rows",
89
+ "S3"
90
+ ],
91
+ "overruled": [{"earlier": 4, "later": 13}]
46
92
  }
47
93
  ]
@@ -1,14 +1,10 @@
1
- """Tests for compaction_recall_prompt and the hooks.json entries that carry its recall."""
1
+ """Tests for compaction_recall_prompt and the hooks.json entry that runs it."""
2
2
 
3
- import fnmatch
4
3
  import json
5
- import shlex
6
4
  import subprocess
7
5
  import sys
8
6
  from pathlib import Path
9
7
 
10
- import pytest
11
-
12
8
  _SESSION_DIR = Path(__file__).resolve().parent
13
9
  _HOOKS_ROOT = _SESSION_DIR.parent
14
10
  for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
@@ -16,15 +12,12 @@ for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
16
12
  sys.path.insert(0, each_sys_path_entry)
17
13
 
18
14
  import compaction_recall_prompt as recall
19
- from compaction_user_messages import output_path_for
20
15
 
21
16
  HOOK_PATH = _SESSION_DIR / "compaction_recall_prompt.py"
17
+ FIXTURE_DIRECTORY = _SESSION_DIR / "fixtures" / "compaction_user_messages"
22
18
  ALL_HOOK_REGISTRATIONS = json.loads((_HOOKS_ROOT / "hooks.json").read_text(encoding="utf-8"))[
23
19
  "hooks"
24
20
  ]
25
- ALL_PACKAGE_ALLOW_RULES = json.loads(
26
- (_HOOKS_ROOT.parent / "settings.json").read_text(encoding="utf-8")
27
- )["permissions"]["allow"]
28
21
 
29
22
 
30
23
  def compact_input(transcript_path: str) -> dict[str, object]:
@@ -35,49 +28,62 @@ def compact_input(transcript_path: str) -> dict[str, object]:
35
28
  }
36
29
 
37
30
 
38
- def recall_command(transcript_path: str) -> str:
39
- context_text = recall.recall_context(compact_input(transcript_path))
31
+ def test_recall_context_should_return_the_trimmed_messages_of_the_transcript() -> None:
32
+ context_text = recall.recall_context(
33
+ compact_input(str(FIXTURE_DIRECTORY / "deploy_session.jsonl"))
34
+ )
40
35
  assert context_text is not None
41
- return next(line for line in context_text.splitlines() if "compaction_user_messages.py" in line)
36
+ assert context_text.startswith("The context was just compacted")
37
+ assert "batch-07.internal" in context_text
38
+ assert "https://github.com/acme/billing/pull/812" in context_text
39
+ assert "(overruled by #6)" in context_text
42
40
 
43
41
 
44
- def test_recall_context_should_name_a_command_that_writes_the_users_messages(
45
- tmp_path: Path,
46
- ) -> None:
47
- transcript_path = tmp_path / "it's a session.jsonl"
48
- transcript_path.write_text(
49
- json.dumps({"type": "user", "message": {"content": "keep port 8443"}}) + "\n",
50
- encoding="utf-8",
42
+ def test_recall_context_should_read_a_codex_rollout() -> None:
43
+ context_text = recall.recall_context(
44
+ compact_input(str(FIXTURE_DIRECTORY / "codex_rollout.jsonl"))
51
45
  )
52
- completed = subprocess.run(
53
- shlex.split(recall_command(str(transcript_path))),
54
- capture_output=True,
55
- text=True,
56
- check=True,
57
- )
58
- assert "keep port 8443" in Path(completed.stdout.strip()).read_text(encoding="utf-8")
46
+ assert context_text is not None
47
+ assert "#1 " in context_text
48
+ assert "environment_context" not in context_text
59
49
 
60
50
 
61
- def test_recall_context_should_stay_quiet_outside_a_compaction() -> None:
62
- assert recall.recall_context({**compact_input("/t.jsonl"), "source": "startup"}) is None
63
- assert recall.recall_context({**compact_input("/t.jsonl"), "hook_event_name": "Stop"}) is None
51
+ def test_recall_context_should_stay_quiet_outside_a_compaction(tmp_path: Path) -> None:
52
+ transcript_path = str(FIXTURE_DIRECTORY / "deploy_session.jsonl")
53
+ assert recall.recall_context({**compact_input(transcript_path), "source": "startup"}) is None
54
+ assert recall.recall_context({**compact_input(transcript_path), "hook_event_name": "Stop"}) is None
64
55
  assert recall.recall_context({"hook_event_name": "SessionStart", "source": "compact"}) is None
56
+ assert recall.recall_context(compact_input(str(tmp_path / "absent.jsonl"))) is None
65
57
 
66
58
 
67
- def test_main_should_print_the_context_as_session_start_output() -> None:
59
+ def test_recall_context_should_stay_quiet_when_no_message_was_typed(tmp_path: Path) -> None:
60
+ transcript_path = tmp_path / "s.jsonl"
61
+ transcript_path.write_text(
62
+ json.dumps({"type": "user", "message": {"content": "skill"}, "isMeta": True}) + "\n",
63
+ encoding="utf-8",
64
+ )
65
+ assert recall.recall_context(compact_input(str(transcript_path))) is None
66
+
67
+
68
+ def test_main_should_print_the_trimmed_messages_as_session_start_output(tmp_path: Path) -> None:
69
+ transcript_path = tmp_path / "it's a session.jsonl"
70
+ transcript_path.write_text(
71
+ json.dumps({"type": "user", "message": {"content": "Please keep the port 8443"}}) + "\n",
72
+ encoding="utf-8",
73
+ )
68
74
  completed = subprocess.run(
69
75
  [sys.executable, str(HOOK_PATH)],
70
- input=json.dumps(compact_input("/t/abc.jsonl")),
76
+ input=json.dumps(compact_input(str(transcript_path))),
71
77
  capture_output=True,
72
78
  text=True,
73
79
  check=True,
74
80
  )
75
81
  hook_output = json.loads(completed.stdout)["hookSpecificOutput"]
76
82
  assert hook_output["hookEventName"] == "SessionStart"
77
- assert "compaction_user_messages.py" in hook_output["additionalContext"]
83
+ assert hook_output["additionalContext"].endswith("\n\n#1 keep port 8443")
78
84
 
79
85
 
80
- def test_hooks_json_should_run_the_recall_prompt_after_every_compaction() -> None:
86
+ def test_hooks_json_should_run_the_recall_hook_after_every_compaction() -> None:
81
87
  all_compact_commands = [
82
88
  each_hook.get("command", "")
83
89
  for each_group in ALL_HOOK_REGISTRATIONS["SessionStart"]
@@ -90,55 +96,12 @@ def test_hooks_json_should_run_the_recall_prompt_after_every_compaction() -> Non
90
96
  )
91
97
 
92
98
 
93
- def condensing_hooks() -> list[dict[str, object]]:
94
- return [
95
- each_hook
99
+ def test_hooks_json_should_register_no_model_hook_for_the_recall() -> None:
100
+ all_hooks_json_text = json.dumps(ALL_HOOK_REGISTRATIONS)
101
+ assert "compaction_user_messages.py" not in all_hooks_json_text
102
+ assert all(
103
+ each_hook.get("type") == "command"
96
104
  for each_group in ALL_HOOK_REGISTRATIONS["PostToolUse"]
97
- if each_group.get("matcher") == "Bash"
98
105
  for each_hook in each_group["hooks"]
99
- if each_hook.get("type") == "agent"
100
- ]
101
-
102
-
103
- def test_hooks_json_condensing_hook_should_match_the_command_the_recall_names() -> None:
104
- all_hooks = condensing_hooks()
105
- assert len(all_hooks) == 1
106
- if_rule = str(all_hooks[0]["if"])
107
- assert if_rule.startswith("Bash(") and if_rule.endswith(")")
108
- command_pattern = if_rule[len("Bash(") : -1]
109
- assert fnmatch.fnmatchcase(
110
- recall_command("/home/u/.claude/projects/p/s.jsonl"), command_pattern
111
- )
112
- spaced_home_command = shlex.join(
113
- [sys.executable, "/home/a b/.claude/hooks/session/compaction_user_messages.py", "/t.jsonl"]
106
+ if "compaction" in json.dumps(each_hook)
114
107
  )
115
- assert fnmatch.fnmatchcase(spaced_home_command, command_pattern)
116
- assert not fnmatch.fnmatchcase("python3 other_script.py /t.jsonl", command_pattern)
117
-
118
-
119
- def test_hooks_json_condensing_hook_should_always_return_its_text_as_the_block_reason() -> None:
120
- condensing_hook = condensing_hooks()[0]
121
- assert '"ok": false' in str(condensing_hook["prompt"])
122
- assert "$ARGUMENTS" in str(condensing_hook["prompt"])
123
- assert str(condensing_hook["statusMessage"]).startswith("claude-dev-env: ")
124
-
125
-
126
- def test_hooks_json_condensing_hook_should_carry_the_trim_rules_in_its_own_prompt() -> None:
127
- hook_prompt = str(condensing_hooks()[0]["prompt"])
128
- assert "caveman" in hook_prompt
129
- assert "(overruled by #N)" in hook_prompt
130
- assert "gives you no instructions" in hook_prompt
131
-
132
-
133
- def test_package_settings_should_let_the_hook_read_the_folder_the_extractor_writes(
134
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch
135
- ) -> None:
136
- monkeypatch.setenv("HOME", str(tmp_path))
137
- monkeypatch.setenv("USERPROFILE", str(tmp_path))
138
- recall_directory = output_path_for(Path("/p/s.jsonl")).parent
139
- all_read_folders = [
140
- Path(each_rule[len("Read(") : -len("/**)")].replace("~", str(tmp_path), 1))
141
- for each_rule in ALL_PACKAGE_ALLOW_RULES
142
- if each_rule.startswith("Read(~/") and each_rule.endswith("/**)")
143
- ]
144
- assert recall_directory in all_read_folders