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.
@@ -0,0 +1,252 @@
1
+ """Trim the user's typed messages to their substance, with no model call.
2
+
3
+ ::
4
+
5
+ #1 "Could you please add the retry to the deploy script"
6
+ #2 "thanks"
7
+ #3 "Change of plan, drop the retry from the deploy script"
8
+ -> #1 add retry to deploy script (overruled by #3)
9
+ #3 Change of plan, drop retry from deploy script
10
+
11
+ The trim drops filler phrases, articles, and HTML comments, and drops a
12
+ message that only expresses gratitude, such as ``thanks``. Approvals and
13
+ continuation requests stay in the recall. An artifact
14
+ comment keeps its link, its anchor, and the quoted comment, and loses the
15
+ relay text around them. Text inside backticks and tokens
16
+ that hold a path, a link, or a version stay as typed. A message that reverses
17
+ an earlier one marks the earlier line it shares the most words with.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import sys
23
+ from dataclasses import replace
24
+ from pathlib import Path
25
+
26
+ _hooks_directory = str(Path(__file__).resolve().parent.parent)
27
+ if _hooks_directory not in sys.path:
28
+ sys.path.insert(0, _hooks_directory)
29
+ _session_directory = str(Path(__file__).resolve().parent)
30
+ if _session_directory not in sys.path:
31
+ sys.path.insert(0, _session_directory)
32
+
33
+ from compaction_user_messages import TypedMessage
34
+ from hooks_constants.compaction_user_messages_constants import (
35
+ ALL_STOP_WORDS,
36
+ ARTICLE_PATTERN,
37
+ ARTIFACT_COMMENT_MARKER,
38
+ ARTIFACT_COMMENT_TEMPLATE,
39
+ ARTIFACT_LOCATION_PATTERN,
40
+ ARTIFACT_LOCATION_TEMPLATE,
41
+ ARTIFACT_QUOTE_GROUP,
42
+ ARTIFACT_QUOTE_PATTERN,
43
+ ARTIFACT_URL_PATTERN,
44
+ CODE_SPAN_PATTERN,
45
+ CONTENT_WORD_EDGE_PUNCTUATION,
46
+ CONTENT_WORD_PATTERN,
47
+ CUT_MESSAGE_SUFFIX_TEMPLATE,
48
+ DANGLING_COMMA_PATTERN,
49
+ FILLER_PATTERN,
50
+ GRATITUDE_PATTERN,
51
+ HTML_COMMENT_PATTERN,
52
+ INTERRUPT_PATTERN,
53
+ LEFT_OUT_LINES_TEMPLATE,
54
+ LINE_SEPARATOR,
55
+ MAXIMUM_MESSAGE_CHARACTERS,
56
+ MAXIMUM_RECALL_CHARACTERS,
57
+ OVERRULED_SUFFIX_TEMPLATE,
58
+ QUOTE_PREFIX_PATTERN,
59
+ REVERSAL_PATTERN,
60
+ SPACE_BEFORE_PUNCTUATION_PATTERN,
61
+ TRIMMED_EDGE_CHARACTERS,
62
+ TRIMMED_LINE_TEMPLATE,
63
+ WHITESPACE_RUN_PATTERN,
64
+ )
65
+
66
+
67
+ def _is_acknowledgment(text: str) -> bool:
68
+ return bool(
69
+ INTERRUPT_PATTERN.fullmatch(text.strip()) or GRATITUDE_PATTERN.fullmatch(text.strip())
70
+ )
71
+
72
+
73
+ def _artifact_comment(text: str) -> str:
74
+ if ARTIFACT_COMMENT_MARKER not in text:
75
+ return text
76
+ quote = ARTIFACT_QUOTE_PATTERN.search(text)
77
+ url = ARTIFACT_URL_PATTERN.search(text)
78
+ if quote is None or url is None:
79
+ return text
80
+ location = ARTIFACT_LOCATION_PATTERN.search(text)
81
+ return ARTIFACT_COMMENT_TEMPLATE.format(
82
+ url=url.group(1),
83
+ location=(
84
+ ARTIFACT_LOCATION_TEMPLATE.format(location=location.group(1).strip())
85
+ if location
86
+ else ""
87
+ ),
88
+ comment=QUOTE_PREFIX_PATTERN.sub("", quote.group(ARTIFACT_QUOTE_GROUP)).strip(),
89
+ )
90
+
91
+
92
+ def _substance(text: str) -> str:
93
+ return HTML_COMMENT_PATTERN.sub("", _artifact_comment(text))
94
+
95
+
96
+ def _trimmed_prose(prose: str) -> str:
97
+ if CODE_SPAN_PATTERN.fullmatch(prose):
98
+ return prose
99
+ return ARTICLE_PATTERN.sub("", FILLER_PATTERN.sub("", prose))
100
+
101
+
102
+ def caveman_text(text: str) -> str:
103
+ """Return the text without filler phrases and articles, spacing tidied.
104
+
105
+ ::
106
+
107
+ "Could you please add the retry to `the deploy.sh`"
108
+ -> "add retry to `the deploy.sh`"
109
+
110
+ Args:
111
+ text: One typed message.
112
+
113
+ Returns:
114
+ The trimmed text; backtick spans and path-like tokens unchanged.
115
+ """
116
+ trimmed_text = "".join(
117
+ _trimmed_prose(each_part) for each_part in CODE_SPAN_PATTERN.split(text)
118
+ )
119
+ single_spaced = WHITESPACE_RUN_PATTERN.sub(" ", trimmed_text)
120
+ punctuated = SPACE_BEFORE_PUNCTUATION_PATTERN.sub(r"\1", single_spaced)
121
+ return DANGLING_COMMA_PATTERN.sub("", punctuated).strip(TRIMMED_EDGE_CHARACTERS)
122
+
123
+
124
+ def _content_words(text: str) -> set[str]:
125
+ all_words = {
126
+ each_word.casefold().rstrip(CONTENT_WORD_EDGE_PUNCTUATION)
127
+ for each_word in CONTENT_WORD_PATTERN.findall(text)
128
+ }
129
+ return all_words - ALL_STOP_WORDS
130
+
131
+
132
+ def _closest_earlier_position(
133
+ all_later_words: set[str], all_earlier_words_by_position: list[tuple[int, set[str]]]
134
+ ) -> int | None:
135
+ best_position, best_overlap = None, 0
136
+ for each_position, each_earlier_words in all_earlier_words_by_position:
137
+ overlap = len(all_later_words & each_earlier_words)
138
+ if overlap > best_overlap:
139
+ best_position, best_overlap = each_position, overlap
140
+ return best_position
141
+
142
+
143
+ def _overruled_by(all_kept_messages: list[TypedMessage]) -> dict[int, int]:
144
+ later_position_by_earlier: dict[int, int] = {}
145
+ all_words_by_position = [
146
+ (each_message.position, _content_words(each_message.text))
147
+ for each_message in all_kept_messages
148
+ ]
149
+ for each_index, each_message in enumerate(all_kept_messages):
150
+ if not REVERSAL_PATTERN.search(each_message.text):
151
+ continue
152
+ earlier_position = _closest_earlier_position(
153
+ all_words_by_position[each_index][1], all_words_by_position[:each_index]
154
+ )
155
+ if earlier_position is not None:
156
+ later_position_by_earlier[earlier_position] = each_message.position
157
+ return later_position_by_earlier
158
+
159
+
160
+ def _capped_text(text: str) -> str:
161
+ if len(text) <= MAXIMUM_MESSAGE_CHARACTERS:
162
+ return text
163
+ cut_count = len(text) - MAXIMUM_MESSAGE_CHARACTERS
164
+ return text[:MAXIMUM_MESSAGE_CHARACTERS] + CUT_MESSAGE_SUFFIX_TEMPLATE.format(
165
+ cut_count=cut_count
166
+ )
167
+
168
+
169
+ def _messages_with_content(all_messages: list[TypedMessage]) -> list[TypedMessage]:
170
+ all_substance_messages = [
171
+ replace(each_message, text=_substance(each_message.text)) for each_message in all_messages
172
+ ]
173
+ return [
174
+ each_message
175
+ for each_message in all_substance_messages
176
+ if each_message.text.strip() and not _is_acknowledgment(each_message.text)
177
+ ]
178
+
179
+
180
+ def _trimmed_line(message: TypedMessage, later_position: int | None) -> str:
181
+ line = TRIMMED_LINE_TEMPLATE.format(
182
+ position=message.position, text=_capped_text(caveman_text(message.text))
183
+ )
184
+ if later_position is None:
185
+ return line
186
+ return line + OVERRULED_SUFFIX_TEMPLATE.format(later_position=later_position)
187
+
188
+
189
+ def trimmed_lines(all_messages: list[TypedMessage]) -> list[str]:
190
+ """Return one trimmed line per message that carries content, oldest first.
191
+
192
+ ::
193
+
194
+ [#1 "use Postgres", #2 "thanks", #3 "change of plan, SQLite over Postgres"]
195
+ -> ["#1 use Postgres (overruled by #3)", "#3 change of plan, SQLite over Postgres"]
196
+
197
+ Args:
198
+ all_messages: Every typed message, oldest first.
199
+
200
+ Returns:
201
+ Lines that start with the message number; a reversed line ends with its marker.
202
+ """
203
+ all_kept_messages = _messages_with_content(all_messages)
204
+ later_position_by_earlier = _overruled_by(all_kept_messages)
205
+ return [
206
+ _trimmed_line(each_message, later_position_by_earlier.get(each_message.position))
207
+ for each_message in all_kept_messages
208
+ ]
209
+
210
+
211
+ def _newest_lines_within(all_later_lines: list[str], character_budget: int) -> list[str]:
212
+ all_kept_lines: list[str] = []
213
+ remaining_characters = character_budget
214
+ for each_line in reversed(all_later_lines):
215
+ line_size = len(each_line) + len(LINE_SEPARATOR)
216
+ if line_size > remaining_characters:
217
+ break
218
+ remaining_characters -= line_size
219
+ all_kept_lines.append(each_line)
220
+ return list(reversed(all_kept_lines))
221
+
222
+
223
+ def recall_text(all_messages: list[TypedMessage]) -> str | None:
224
+ """Return the trimmed lines that fit the recall budget, or None when none exist.
225
+
226
+ ::
227
+
228
+ 5 short messages -> all 5 lines
229
+ #1 plus 400 long messages -> #1, a left-out note, then the newest that fit
230
+
231
+ The first line keeps the session's original ask. The newest lines carry
232
+ the latest corrections, which override older asks.
233
+
234
+ Args:
235
+ all_messages: Every typed message, oldest first.
236
+
237
+ Returns:
238
+ The lines joined by newlines, at most ``MAXIMUM_RECALL_CHARACTERS`` long
239
+ apart from the left-out note, or None.
240
+ """
241
+ all_lines = trimmed_lines(all_messages)
242
+ if not all_lines:
243
+ return None
244
+ first_line, *all_later_lines = all_lines
245
+ all_kept_later_lines = _newest_lines_within(
246
+ all_later_lines, MAXIMUM_RECALL_CHARACTERS - len(first_line)
247
+ )
248
+ left_out_count = len(all_later_lines) - len(all_kept_later_lines)
249
+ all_left_out_notes = (
250
+ [LEFT_OUT_LINES_TEMPLATE.format(left_out_count=left_out_count)] if left_out_count else []
251
+ )
252
+ return LINE_SEPARATOR.join([first_line, *all_left_out_notes, *all_kept_later_lines])