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.
- package/bin/install-constants.mjs +3 -0
- package/bin/install.cursor-rules.test.mjs +11 -1
- package/bin/install.mjs +5 -0
- package/bin/install.settings-defaults.test.mjs +0 -1
- package/hooks/features/session-context.md +5 -4
- package/hooks/hooks.json +0 -13
- package/hooks/hooks_constants/compaction_user_messages_constants.py +141 -47
- package/hooks/session/compaction_recall_prompt.py +29 -18
- package/hooks/session/compaction_trim.py +252 -0
- package/hooks/session/compaction_trim_eval.py +85 -279
- package/hooks/session/compaction_user_messages.py +42 -143
- package/hooks/session/fixtures/compaction_user_messages/codex_rollout.jsonl +22 -0
- package/hooks/session/fixtures/compaction_user_messages/random_9031_1.jsonl +42 -0
- package/hooks/session/fixtures/compaction_user_messages/random_9031_2.jsonl +42 -0
- package/hooks/session/fixtures/compaction_user_messages/trim_cases.json +46 -0
- package/hooks/session/test_compaction_recall_prompt.py +45 -82
- package/hooks/session/test_compaction_trim.py +169 -0
- package/hooks/session/test_compaction_trim_eval.py +32 -85
- package/hooks/session/test_compaction_user_messages.py +37 -95
- package/package.json +1 -1
- package/settings.json +1 -2
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Read every message the user typed from a Claude Code or Codex transcript.
|
|
3
3
|
|
|
4
4
|
::
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
|
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
|
|
39
|
-
context_text = recall.recall_context(
|
|
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
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
63
|
-
assert recall.recall_context({**compact_input(
|
|
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
|
|
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(
|
|
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 "
|
|
83
|
+
assert hook_output["additionalContext"].endswith("\n\n#1 keep port 8443")
|
|
78
84
|
|
|
79
85
|
|
|
80
|
-
def
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
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
|