align-agent 0.4.3__py3-none-any.whl

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.
Files changed (60) hide show
  1. align_agent/__init__.py +11 -0
  2. align_agent/_audit/common.py +58 -0
  3. align_agent/_audit/deepswe.py +260 -0
  4. align_agent/_audit/excerpts.py +203 -0
  5. align_agent/_audit/extract.py +798 -0
  6. align_agent/_audit/materialize.py +624 -0
  7. align_agent/_audit/score.py +325 -0
  8. align_agent/_audit/shellparse.py +1141 -0
  9. align_agent/_audit/tarsur.py +153 -0
  10. align_agent/_vendor/src/__init__.py +3 -0
  11. align_agent/_vendor/src/harness/__init__.py +13 -0
  12. align_agent/_vendor/src/harness/arms.py +524 -0
  13. align_agent/_vendor/src/harness/completion.py +104 -0
  14. align_agent/_vendor/src/harness/dependency_notice.py +616 -0
  15. align_agent/_vendor/src/harness/freshness_hook.py +131 -0
  16. align_agent/_vendor/src/harness/lean.py +41 -0
  17. align_agent/_vendor/src/harness/mini_real.py +1040 -0
  18. align_agent/_vendor/src/harness/miniswe_scaffold.py +661 -0
  19. align_agent/_vendor/src/harness/tail_diff.py +292 -0
  20. align_agent/_vendor/src/harness/viewport.py +220 -0
  21. align_agent/_vendor/src/metrics/__init__.py +38 -0
  22. align_agent/_vendor/src/metrics/coherence.py +354 -0
  23. align_agent/_vendor/src/metrics/corruption_oracles.py +139 -0
  24. align_agent/_vendor/src/metrics/replay.py +399 -0
  25. align_agent/_vendor/src/metrics/statistics.py +185 -0
  26. align_agent/_vendor/src/metrics/verification.py +104 -0
  27. align_agent/_vendor/src/parsers/__init__.py +33 -0
  28. align_agent/_vendor/src/parsers/base_parser.py +349 -0
  29. align_agent/_vendor/src/parsers/file_events.py +806 -0
  30. align_agent/_vendor/src/parsers/miniswe_parser.py +96 -0
  31. align_agent/_vendor/src/parsers/openhands_parser.py +414 -0
  32. align_agent/_vendor/src/parsers/sweagent_parser.py +180 -0
  33. align_agent/_vendor/src/parsers/terminus_parser.py +106 -0
  34. align_agent/cli.py +101 -0
  35. align_agent/core.py +161 -0
  36. align_agent/run.py +84 -0
  37. align_agent/sessions.py +985 -0
  38. align_agent/writethrough/__init__.py +4 -0
  39. align_agent/writethrough/_lib/__init__.py +1 -0
  40. align_agent/writethrough/_lib/wt_src/__init__.py +3 -0
  41. align_agent/writethrough/_lib/wt_src/harness/__init__.py +1 -0
  42. align_agent/writethrough/_lib/wt_src/harness/dependency_notice.py +616 -0
  43. align_agent/writethrough/_lib/wt_src/harness/lean.py +41 -0
  44. align_agent/writethrough/_lib/wt_src/harness/tail_diff.py +292 -0
  45. align_agent/writethrough/_lib/wt_src/harness/viewport.py +220 -0
  46. align_agent/writethrough/_lib/wt_src/metrics/__init__.py +38 -0
  47. align_agent/writethrough/_lib/wt_src/metrics/replay.py +399 -0
  48. align_agent/writethrough/_lib/wt_src/parsers/__init__.py +1 -0
  49. align_agent/writethrough/_lib/wt_src/parsers/file_events.py +806 -0
  50. align_agent/writethrough/claude_code.py +81 -0
  51. align_agent/writethrough/engine.py +647 -0
  52. align_agent/writethrough/hooks.py +227 -0
  53. align_agent/writethrough/mcp_server.py +125 -0
  54. align_agent/writethrough/protocol.py +68 -0
  55. align_agent-0.4.3.dist-info/METADATA +224 -0
  56. align_agent-0.4.3.dist-info/RECORD +60 -0
  57. align_agent-0.4.3.dist-info/WHEEL +5 -0
  58. align_agent-0.4.3.dist-info/entry_points.txt +6 -0
  59. align_agent-0.4.3.dist-info/licenses/LICENSE +21 -0
  60. align_agent-0.4.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,524 @@
1
+ """Intervention arms for RQ2: what each one does to the prompt, and its names.
2
+
3
+ Three families (names and one-line effects are in ARM_INFO below, the single
4
+ source every script, table and figure reads):
5
+
6
+ - Baselines: Control, Re-read reminder, Recency eviction, and Volume-matched
7
+ eviction (the control for Stale-view removal).
8
+ - Retroactive rewrite: Stale flag, Stale-view removal, Changed-hunk removal,
9
+ In-place refresh, Tail refresh. All rewrite earlier turns, so all break the
10
+ prefix cache.
11
+ - Tool boundary: Write-through viewport, which never rewrites an earlier turn.
12
+
13
+ Enum values are the on-disk keys in traces and results and never change.
14
+ """
15
+
16
+ import os
17
+ import copy
18
+ import re
19
+ import difflib
20
+ from dataclasses import dataclass
21
+ from enum import Enum
22
+ from typing import List, Dict, Any, Optional
23
+ from .freshness_hook import ObservationBinding
24
+
25
+
26
+ class InterventionArm(Enum):
27
+ ARM_0_PROMPT_ONLY = "prompt_only_baseline"
28
+ ARM_1_CONTROL = "control"
29
+ ARM_2_POSITIONAL = "positional_elision"
30
+ ARM_3_MARKED = "freshness_marked"
31
+ ARM_4_MASKED = "freshness_masked"
32
+ ARM_5_VOLUME_CONTROL = "volume_matched_control"
33
+ ARM_6_IN_PLACE = "in_place_replacement"
34
+ ARM_7_JIT_PREFETCH = "jit_prefetch"
35
+ ARM_8_FINE_GRAINED = "fine_grained_invalidation"
36
+ ARM_9_WRITE_THROUGH = "write_through_viewport"
37
+ ARM_10_WRITE_THROUGH_DIFF = "write_through_diff"
38
+ ARM_11_SYNCED_CONTEXT = "synced_context"
39
+ ARM_12_DEPENDENCY_WRITE_THROUGH = "dependency_write_through"
40
+ ARM_13_WRITE_THROUGH_DIFF_LEAN = "write_through_diff_lean"
41
+ ARM_14_WRITE_THROUGH_DIFF_LINES = "write_through_diff_lines"
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class ArmInfo:
46
+ """How an arm is named and grouped everywhere outside the enum.
47
+
48
+ The enum value is the on-disk key (trace filenames, results records) and
49
+ never changes. Every script, table and figure takes its labels from here,
50
+ so an arm has exactly one name per medium.
51
+ """
52
+
53
+ family: str # "baseline" | "retroactive" | "tool_boundary"
54
+ display: str # table / paper name
55
+ short: str # figure axis label
56
+ cli: tuple # accepted --arm spellings; first is canonical
57
+ macro: str # suffix of the generated paper macros (stable)
58
+ effect: str # what it does to the prompt, one sentence
59
+ core: bool = False # reported in the main paper; the rest in the extended report
60
+
61
+
62
+ FAMILIES = {
63
+ "baseline": "Baselines",
64
+ "retroactive": "Retroactive rewrite",
65
+ "tool_boundary": "Tool boundary",
66
+ "sync": "Synchronized context",
67
+ }
68
+
69
+ ARM_INFO = {
70
+ InterventionArm.ARM_1_CONTROL: ArmInfo(
71
+ "baseline", "Control", "Control", ("control",), "Control",
72
+ "History is left unchanged.", core=True),
73
+ InterventionArm.ARM_0_PROMPT_ONLY: ArmInfo(
74
+ "baseline", "Re-read reminder", "Reminder", ("prompt_only", "prompt_only_baseline", "reminder"),
75
+ "PromptOnly",
76
+ "Adds an instruction to re-read a file before editing it to the system prompt; history unchanged.",
77
+ core=True),
78
+ InterventionArm.ARM_2_POSITIONAL: ArmInfo(
79
+ "baseline", "Recency eviction", "Recency", ("positional", "positional_elision", "recency"),
80
+ "Positional",
81
+ "Above 32K estimated tokens, replaces the oldest observations with a placeholder, whether stale or not."),
82
+ InterventionArm.ARM_5_VOLUME_CONTROL: ArmInfo(
83
+ "baseline", "Volume-matched eviction", "Vol.-matched",
84
+ ("volume_control", "volume_matched_control"), "VolumeMatched",
85
+ "Evicts as many tokens as stale-view removal would, taken oldest-first from observations "
86
+ "that are not stale; the control for stale-view removal."),
87
+ InterventionArm.ARM_3_MARKED: ArmInfo(
88
+ "retroactive", "Stale flag", "Flag", ("marked", "freshness_marked", "stale_flag"), "Marked",
89
+ "Appends a notice after each stale observation; the stale text stays in place."),
90
+ InterventionArm.ARM_4_MASKED: ArmInfo(
91
+ "retroactive", "Stale-view removal", "Removal", ("masked", "freshness_masked", "removal"), "Masked",
92
+ "Replaces each stale observation with a placeholder telling the agent to re-read the file.",
93
+ core=True),
94
+ InterventionArm.ARM_8_FINE_GRAINED: ArmInfo(
95
+ "retroactive", "Changed-hunk removal", "Hunk removal",
96
+ ("fine_grained", "fine_grained_invalidation", "hunk_removal"), "FineGrained",
97
+ "Removes only the changed hunks of each stale observation and keeps the unchanged lines."),
98
+ InterventionArm.ARM_6_IN_PLACE: ArmInfo(
99
+ "retroactive", "In-place refresh", "In-place", ("in_place", "in_place_replacement"), "InPlace",
100
+ "Overwrites each stale observation, where it sits in the history, with the file's full current content.",
101
+ core=True),
102
+ InterventionArm.ARM_7_JIT_PREFETCH: ArmInfo(
103
+ "retroactive", "Tail refresh", "Tail refresh", ("jit_prefetch", "tail_refresh"), "JIT",
104
+ "Replaces each stale observation with a placeholder and appends the file's full current "
105
+ "content at the end of the prompt."),
106
+ InterventionArm.ARM_9_WRITE_THROUGH: ArmInfo(
107
+ "tool_boundary", "Write-through viewport", "Write-through",
108
+ ("write_through_viewport", "write_through"), "WriteThrough",
109
+ "After a step writes a file, appends a line-numbered window around the edit to that step's own "
110
+ "tool output; earlier turns are never rewritten.", core=True),
111
+ InterventionArm.ARM_10_WRITE_THROUGH_DIFF: ArmInfo(
112
+ "tool_boundary", "Write-through diff", "WT diff",
113
+ ("write_through_diff", "wt_diff"), "WriteThroughDiff",
114
+ "After a step writes a file, appends the net line-numbered diff from the version the agent last "
115
+ "held to the file on disk, every hunk; earlier turns are never rewritten."),
116
+ InterventionArm.ARM_11_SYNCED_CONTEXT: ArmInfo(
117
+ "sync", "Synced context", "Synced",
118
+ ("synced_context", "synced", "corvus"), "SyncedContext",
119
+ "A CORVUS-style baseline: every prompt ends with the current contents of each file the agent has "
120
+ "viewed, read from disk, and observations that were only a read of such a file are elided."),
121
+ InterventionArm.ARM_12_DEPENDENCY_WRITE_THROUGH: ArmInfo(
122
+ "tool_boundary", "Dependency write-through", "Dep. WT",
123
+ ("dependency_write_through", "dep_wt"), "DependencyWriteThrough",
124
+ "Write-through diff, plus a notice listing uses in other files of names the write removed or "
125
+ "whose parameters it changed; earlier turns are never rewritten."),
126
+ InterventionArm.ARM_13_WRITE_THROUGH_DIFF_LEAN: ArmInfo(
127
+ "tool_boundary", "Write-through diff (lean)", "WT diff lean",
128
+ ("write_through_diff_lean", "wt_diff_lean"), "WriteThroughDiffLean",
129
+ "Write-through diff, without a block for writes that tell the agent nothing new: whole files it "
130
+ "typed itself, files the same command printed after writing, and patch.txt."),
131
+ InterventionArm.ARM_14_WRITE_THROUGH_DIFF_LINES: ArmInfo(
132
+ "tool_boundary", "Write-through diff (lean) with line numbers", "WT diff lines",
133
+ ("write_through_diff_lines", "wt_diff_lines"), "WriteThroughDiffLines",
134
+ "The lean write-through diff, plus a note of every stretch of the file that moved since the agent "
135
+ "last read it, with its offset, as the shipped hook does (tail_diff.line_number_note)."),
136
+ }
137
+
138
+ # Table and figure order: family by family, as listed above.
139
+ ARM_ORDER = sorted(ARM_INFO, key=lambda a: (list(FAMILIES).index(ARM_INFO[a].family),
140
+ list(ARM_INFO).index(a)))
141
+
142
+
143
+ def arm_from_cli(name: str) -> "InterventionArm":
144
+ """The arm a --arm spelling names (any alias, or the enum value)."""
145
+ for arm, info in ARM_INFO.items():
146
+ if name == arm.value or name in info.cli:
147
+ return arm
148
+ raise ValueError(f"unknown arm {name!r}")
149
+
150
+
151
+ def arm_info(value: str) -> ArmInfo:
152
+ """Info for an on-disk arm value, as stored in traces and results."""
153
+ return ARM_INFO[InterventionArm(value)]
154
+
155
+
156
+ # Appended to the system prompt for ARM_0_PROMPT_ONLY. One copy, shared by
157
+ # every scaffold, so the prompt baseline cannot drift between harnesses.
158
+ PROMPT_ONLY_POLICY = """
159
+
160
+ [POLICY: PROMPT-ONLY RE-READ INSTRUCTION]
161
+ Before modifying or referencing any file, if you have not inspected its content recently \
162
+ or if it was modified previously, always re-read the latest on-disk state with `cat <file>`. \
163
+ Never make assumptions about line numbers or file contents based on past observations."""
164
+
165
+
166
+ @dataclass(frozen=True)
167
+ class ArmRemediation:
168
+ """What an arm does to a stale observation before the prompt is sent.
169
+
170
+ Metrics need this to tell "the hazard never happened" from "the hazard
171
+ happened and the arm repaired it". Scoring every arm on invalidation
172
+ counts alone charges the repairing arms for the hazards they fix, which
173
+ ranked the arms that merely edited least at the top.
174
+
175
+ withdraws: the stale text is replaced, so the model cannot read it.
176
+ annotates: the stale text is kept and a warning is attached.
177
+ forwards: current file content is appended at the prompt tail.
178
+ refreshes: current file content is written over the stale text, where
179
+ it sat in the history.
180
+ partial: only the changed region is withdrawn; untouched lines remain,
181
+ and those lines are not stale.
182
+ """
183
+
184
+ withdraws: bool = False
185
+ annotates: bool = False
186
+ forwards: bool = False
187
+ refreshes: bool = False
188
+ partial: bool = False
189
+
190
+ @property
191
+ def supplies_current(self) -> bool:
192
+ """The model can see the file's current content without re-reading it."""
193
+ return self.forwards or self.refreshes
194
+
195
+
196
+ # Declared next to the transforms above so the two cannot drift apart. Arm 2
197
+ # evicts by recency rather than by staleness, so it withdraws stale content
198
+ # only by coincidence and is not counted as remediating.
199
+ ARM_REMEDIATION = {
200
+ InterventionArm.ARM_0_PROMPT_ONLY: ArmRemediation(),
201
+ InterventionArm.ARM_1_CONTROL: ArmRemediation(),
202
+ InterventionArm.ARM_2_POSITIONAL: ArmRemediation(),
203
+ InterventionArm.ARM_3_MARKED: ArmRemediation(annotates=True),
204
+ InterventionArm.ARM_4_MASKED: ArmRemediation(withdraws=True),
205
+ InterventionArm.ARM_5_VOLUME_CONTROL: ArmRemediation(),
206
+ InterventionArm.ARM_6_IN_PLACE: ArmRemediation(withdraws=True, refreshes=True),
207
+ InterventionArm.ARM_7_JIT_PREFETCH: ArmRemediation(withdraws=True, forwards=True),
208
+ InterventionArm.ARM_8_FINE_GRAINED: ArmRemediation(withdraws=True, partial=True),
209
+ InterventionArm.ARM_9_WRITE_THROUGH: ArmRemediation(forwards=True),
210
+ InterventionArm.ARM_10_WRITE_THROUGH_DIFF: ArmRemediation(forwards=True),
211
+ InterventionArm.ARM_11_SYNCED_CONTEXT: ArmRemediation(forwards=True),
212
+ InterventionArm.ARM_12_DEPENDENCY_WRITE_THROUGH: ArmRemediation(forwards=True),
213
+ InterventionArm.ARM_13_WRITE_THROUGH_DIFF_LEAN: ArmRemediation(forwards=True),
214
+ InterventionArm.ARM_14_WRITE_THROUGH_DIFF_LINES: ArmRemediation(forwards=True),
215
+ }
216
+
217
+ # Arms that append the written file's state to the step's own output.
218
+ WRITE_THROUGH_ARMS = (InterventionArm.ARM_9_WRITE_THROUGH, InterventionArm.ARM_10_WRITE_THROUGH_DIFF,
219
+ InterventionArm.ARM_12_DEPENDENCY_WRITE_THROUGH,
220
+ InterventionArm.ARM_13_WRITE_THROUGH_DIFF_LEAN, InterventionArm.ARM_14_WRITE_THROUGH_DIFF_LINES)
221
+ # Write-through arms whose block is the net diff since the last view.
222
+ DIFF_ARMS = (InterventionArm.ARM_10_WRITE_THROUGH_DIFF, InterventionArm.ARM_12_DEPENDENCY_WRITE_THROUGH,
223
+ InterventionArm.ARM_13_WRITE_THROUGH_DIFF_LEAN, InterventionArm.ARM_14_WRITE_THROUGH_DIFF_LINES)
224
+ # Arms that skip the block for writes that tell the agent nothing new (src/harness/lean.py).
225
+ LEAN_ARMS = (InterventionArm.ARM_13_WRITE_THROUGH_DIFF_LEAN, InterventionArm.ARM_14_WRITE_THROUGH_DIFF_LINES)
226
+
227
+
228
+ # Synced context (Arm 11). CORVUS injects whole files; a 32K-token backbone
229
+ # cannot hold that on SWE-Bench Pro's large files, so the block is capped and
230
+ # files are shown newest-viewed first, each clipped past SYNC_MAX_FILE_CHARS.
231
+ # Opens the block appended to the newest message. The prompt cache breakpoint
232
+ # goes just before it (PostQodeModel._payload), so the history is cached and
233
+ # only the block is recomputed: CORVUS's layout, history then synced context.
234
+ SYNC_HEADER = "[SYNCED CONTEXT: current contents of the files you have viewed]"
235
+ SYNC_MAX_CHARS = int(os.environ.get("SYNC_MAX_CHARS", 24_000))
236
+ SYNC_MAX_FILE_CHARS = int(os.environ.get("SYNC_MAX_FILE_CHARS", 12_000))
237
+ # Under a large window (over 64K tokens) the arm re-sends whole files, as
238
+ # CORVUS does. These limits only guard against a pathological file (a
239
+ # generated bundle); the largest block the v13 pilot runs would have sent
240
+ # uncapped was 51K characters. A step whose block hit them records it.
241
+ SYNC_WIDE_MAX_CHARS = 400_000
242
+ SYNC_WIDE_MAX_FILE_CHARS = 200_000
243
+ # An observation counts as "only a read" of a file when the file's text makes
244
+ # up at least this share of it; anything carrying more output is kept.
245
+ SYNC_READ_SHARE = 0.8
246
+ _OUTPUT_TAG = re.compile(r"<output>\n?(.*?)</output>", re.S)
247
+
248
+
249
+ def synced_context_view(messages: List[Dict[str, Any]], bindings: List[ObservationBinding],
250
+ workspace_root: str, max_chars: Optional[int] = None,
251
+ max_file_chars: Optional[int] = None) -> "tuple[List[Dict[str, Any]], List[str]]":
252
+ """The prompt the synced-context arm sends, and the files its block showed.
253
+
254
+ Returns a copy: the real history keeps every observation, as in CORVUS,
255
+ where the synced context is rebuilt for each reasoning cycle and never
256
+ stored in the trajectory.
257
+ """
258
+ transformed = copy.deepcopy(messages)
259
+ registry: List[str] = []
260
+ for b in sorted(bindings, key=lambda b: b.step_id, reverse=True):
261
+ if b.file_path not in registry:
262
+ registry.append(b.file_path)
263
+ for b in bindings:
264
+ # The newest read is elided too, as CORVUS's sync_file returns only a
265
+ # marker: the file is in the block below, and the history stays
266
+ # append-only, so the prompt cache can reuse it.
267
+ if b.message_index >= len(transformed) or b.message_index < 0:
268
+ continue
269
+ content = str(transformed[b.message_index].get("content", ""))
270
+ # mini-swe-agent wraps a command's output in <returncode>/<output> tags.
271
+ inner = _OUTPUT_TAG.search(content)
272
+ printed = inner.group(1) if inner else content
273
+ body = b.file_content if b.file_content is not None else ""
274
+ if body and len(body) >= SYNC_READ_SHARE * len(printed):
275
+ transformed[b.message_index]["content"] = (
276
+ f"[Contents of {b.file_path} are kept current in the synced context at the end of the prompt.]")
277
+ max_chars = SYNC_MAX_CHARS if max_chars is None else max_chars
278
+ max_file_chars = SYNC_MAX_FILE_CHARS if max_file_chars is None else max_file_chars
279
+ blocks, shown, used = [], [], 0
280
+ for rel in registry:
281
+ path = rel if os.path.isabs(rel) else os.path.join(workspace_root, rel)
282
+ try:
283
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
284
+ text = f.read()
285
+ except OSError:
286
+ continue # deleted or unreadable: nothing current to show
287
+ if len(text) > max_file_chars:
288
+ text = text[:max_file_chars] + f"\n...[{len(text) - max_file_chars} more characters]"
289
+ block = f"=== {rel} ===\n{text}"
290
+ if used + len(block) > max_chars:
291
+ break
292
+ blocks.append(block)
293
+ shown.append(rel)
294
+ used += len(block)
295
+ if blocks and transformed and transformed[-1].get("role") in ("user", "tool"):
296
+ transformed[-1]["content"] = (str(transformed[-1].get("content", "")) +
297
+ "\n\n" + SYNC_HEADER + "\n" +
298
+ "\n\n".join(blocks))
299
+ return transformed, shown
300
+
301
+
302
+ def _evict_oldest_by_recency(
303
+ transformed: List[Dict[str, Any]],
304
+ budget: int,
305
+ min_tokens: int,
306
+ placeholder_template: str,
307
+ skip_indices: Optional[Dict[int, ObservationBinding]] = None,
308
+ ) -> None:
309
+ """Evict oldest eligible user/tool messages in place until budget tokens are freed."""
310
+ skip_indices = skip_indices or {}
311
+ evicted_tokens = 0
312
+ for i, msg in enumerate(transformed):
313
+ if i in skip_indices or msg.get("role") not in ["user", "tool"] or i <= 1:
314
+ continue
315
+ content = msg.get("content", "")
316
+ tok_len = len(content) // 4
317
+ if tok_len > min_tokens:
318
+ msg["content"] = placeholder_template.format(tok_len=tok_len)
319
+ evicted_tokens += tok_len
320
+ if evicted_tokens >= budget:
321
+ break
322
+
323
+
324
+ def apply_intervention_arm(
325
+ messages: List[Dict[str, Any]],
326
+ bindings: List[ObservationBinding],
327
+ arm: InterventionArm,
328
+ max_tokens: int = 32000,
329
+ current_tokens: int = 0,
330
+ workspace_root: str = "."
331
+ ) -> List[Dict[str, Any]]:
332
+ """Transform conversation message history according to selected intervention arm."""
333
+ transformed = copy.deepcopy(messages)
334
+
335
+ if arm in [InterventionArm.ARM_1_CONTROL, InterventionArm.ARM_0_PROMPT_ONLY,
336
+ InterventionArm.ARM_9_WRITE_THROUGH, InterventionArm.ARM_10_WRITE_THROUGH_DIFF,
337
+ InterventionArm.ARM_11_SYNCED_CONTEXT, InterventionArm.ARM_12_DEPENDENCY_WRITE_THROUGH,
338
+ InterventionArm.ARM_13_WRITE_THROUGH_DIFF_LEAN, InterventionArm.ARM_14_WRITE_THROUGH_DIFF_LINES]:
339
+ # Arm 11 builds its prompt with synced_context_view, which needs the
340
+ # workspace; here, as for the others, the history is left unchanged.
341
+ # Re-read reminder, Control, Write-through viewport:
342
+ # Return unmodified history in context. Arm 9's intervention operates
343
+ # at the tool-output layer (viewport echo after writes), not by
344
+ # retroactively rewriting prompt history.
345
+ return transformed
346
+
347
+ elif arm == InterventionArm.ARM_2_POSITIONAL:
348
+ # Recency eviction: oldest observations first, stale or not
349
+ # Evict oldest observation contents when context exceeds limit
350
+ if current_tokens > max_tokens:
351
+ excess = current_tokens - max_tokens
352
+ _evict_oldest_by_recency(
353
+ transformed, excess, 100,
354
+ "[CONTENT ELIDED BY RECENCY: {tok_len} tokens truncated]"
355
+ )
356
+ return transformed
357
+
358
+ elif arm == InterventionArm.ARM_3_MARKED:
359
+ # Stale flag
360
+ # Inject inline warning if an observation has been invalidated
361
+ stale_indices = {b.message_index: b for b in bindings if b.is_invalid}
362
+ for idx, binding in stale_indices.items():
363
+ if idx < len(transformed):
364
+ orig_content = transformed[idx].get("content", "")
365
+ marker = (
366
+ f"\n[SYSTEM NOTICE: File '{binding.file_path}' has been modified on disk "
367
+ f"at Step {binding.invalidation_step}. The observation above is STALE.]"
368
+ )
369
+ if marker not in orig_content:
370
+ transformed[idx]["content"] = orig_content + marker
371
+ return transformed
372
+
373
+ elif arm == InterventionArm.ARM_4_MASKED:
374
+ # Stale-view removal
375
+ # Replace stale observation entirely with structural placeholder
376
+ stale_indices = {b.message_index: b for b in bindings if b.is_invalid}
377
+ for idx, binding in stale_indices.items():
378
+ if idx < len(transformed):
379
+ placeholder = (
380
+ f"[OBSERVATION EVICTED: Content of '{binding.file_path}' observed at Step {binding.step_id} "
381
+ f"is STALE following on-disk modification at Step {binding.invalidation_step}. "
382
+ f"Execute 'cat {binding.file_path}' to inspect current on-disk state.]"
383
+ )
384
+ transformed[idx]["content"] = placeholder
385
+ return transformed
386
+
387
+ elif arm == InterventionArm.ARM_5_VOLUME_CONTROL:
388
+ # Volume-matched eviction (oldest-first, from non-stale observations)
389
+ # Calculate tokens masked in Arm 4, but evict equivalent tokens from NON-STALE observations
390
+ stale_indices = {b.message_index: b for b in bindings if b.is_invalid}
391
+ stale_tokens = sum(len(b.raw_content) // 4 for b in stale_indices.values())
392
+
393
+ if stale_tokens > 0:
394
+ _evict_oldest_by_recency(
395
+ transformed, stale_tokens, 50,
396
+ "[VOLUME CONTROL EVICTED: {tok_len} tokens removed without regard to staleness]",
397
+ skip_indices=stale_indices,
398
+ )
399
+ return transformed
400
+
401
+ elif arm == InterventionArm.ARM_6_IN_PLACE:
402
+ # In-place refresh
403
+ # Replaces stale observations in-place with current on-disk file state
404
+ stale_indices = {b.message_index: b for b in bindings if b.is_invalid}
405
+ for idx, binding in stale_indices.items():
406
+ if idx < len(transformed):
407
+ abs_p = os.path.join(workspace_root, binding.file_path) if not os.path.isabs(binding.file_path) else binding.file_path
408
+ if os.path.exists(abs_p) and os.path.isfile(abs_p):
409
+ try:
410
+ with open(abs_p, "r", encoding="utf-8", errors="replace") as f:
411
+ fresh_content = f.read()
412
+ transformed[idx]["content"] = (
413
+ f"[OBSERVATION SYNCHRONIZED (Step {binding.invalidation_step}): "
414
+ f"Content of '{binding.file_path}' updated to current on-disk state]:\n"
415
+ f"{fresh_content}"
416
+ )
417
+ except Exception as e:
418
+ transformed[idx]["content"] = (
419
+ f"[OBSERVATION SYNCHRONIZED: File '{binding.file_path}' "
420
+ f"could not be re-read: {e}]"
421
+ )
422
+ else:
423
+ transformed[idx]["content"] = (
424
+ f"[OBSERVATION SYNCHRONIZED: File '{binding.file_path}' "
425
+ f"was removed on disk at Step {binding.invalidation_step}.]"
426
+ )
427
+ return transformed
428
+
429
+ elif arm == InterventionArm.ARM_7_JIT_PREFETCH:
430
+ # Tail refresh: replaces stale views AND appends current content, so it
431
+ # rewrites earlier turns too; it is not append-only
432
+ # 1. Mask the old stale observation to prevent reading superseded content
433
+ stale_indices = {b.message_index: b for b in bindings if b.is_invalid}
434
+ for idx, binding in stale_indices.items():
435
+ if idx < len(transformed):
436
+ transformed[idx]["content"] = (
437
+ f"[OBSERVATION SUPERSEDED: Content of '{binding.file_path}' observed at Step {binding.step_id} "
438
+ f"is STALE after Step {binding.invalidation_step}. (Synchronized snapshot forwarded below).]"
439
+ )
440
+
441
+ # 2. Forward the latest on-disk state of all mutated files at the immediate prompt tail
442
+ if stale_indices and transformed:
443
+ forwarding_blocks = []
444
+ # Deduplicate by file path, keeping latest invalidation step
445
+ latest_by_file: Dict[str, ObservationBinding] = {}
446
+ for b in bindings:
447
+ if b.is_invalid:
448
+ latest_by_file[b.file_path] = b
449
+
450
+ for f_path, binding in latest_by_file.items():
451
+ abs_p = os.path.join(workspace_root, f_path) if not os.path.isabs(f_path) else f_path
452
+ if os.path.exists(abs_p) and os.path.isfile(abs_p):
453
+ try:
454
+ with open(abs_p, "r", encoding="utf-8", errors="replace") as f:
455
+ f_content = f.read()
456
+ forwarding_blocks.append(
457
+ f"[HARNESS STORE-FORWARDING BUFFER: File '{f_path}' (mutated at Step {binding.invalidation_step}) current on-disk content]:\n"
458
+ f"{f_content}"
459
+ )
460
+ except Exception:
461
+ pass
462
+
463
+ if forwarding_blocks:
464
+ forwarding_text = "\n\n" + "\n\n".join(forwarding_blocks)
465
+ # Append to the latest message if it is a user observation, or append a new user message
466
+ if transformed[-1].get("role") in ["user", "tool"]:
467
+ transformed[-1]["content"] += forwarding_text
468
+ else:
469
+ transformed.append({"role": "user", "content": forwarding_text.strip()})
470
+
471
+ return transformed
472
+
473
+ elif arm == InterventionArm.ARM_8_FINE_GRAINED:
474
+ # Changed-hunk removal
475
+ # Instead of evicting the entire observation block, only invalidate line hunks that mutated on disk
476
+ stale_indices = {b.message_index: b for b in bindings if b.is_invalid}
477
+ for idx, binding in stale_indices.items():
478
+ if idx < len(transformed):
479
+ abs_p = os.path.join(workspace_root, binding.file_path) if not os.path.isabs(binding.file_path) else binding.file_path
480
+ if os.path.exists(abs_p) and os.path.isfile(abs_p):
481
+ try:
482
+ with open(abs_p, "r", encoding="utf-8", errors="replace") as f:
483
+ fresh_content = f.read()
484
+
485
+ # Diff the file as it was read against the file as it
486
+ # is now. Using the observation message here compared
487
+ # harness text with source and marked every line
488
+ # changed, which made this arm identical to Arm 4.
489
+ # None (never read) still falls back to raw_content;
490
+ # "" (read, genuinely empty) must not.
491
+ orig_content = binding.file_content if binding.file_content is not None else binding.raw_content
492
+ orig_lines = orig_content.splitlines(keepends=True)
493
+ fresh_lines = fresh_content.splitlines(keepends=True)
494
+ matcher = difflib.SequenceMatcher(None, orig_lines, fresh_lines)
495
+
496
+ reconstructed_blocks = []
497
+ for tag, alo, ahi, blo, bhi in matcher.get_opcodes():
498
+ if tag == "equal":
499
+ reconstructed_blocks.append("".join(orig_lines[alo:ahi]))
500
+ elif tag in ("replace", "delete"):
501
+ hunk_desc = f"{alo + 1}" if alo + 1 == ahi else f"{alo + 1}-{ahi}"
502
+ reconstructed_blocks.append(
503
+ f"[HUNK INVALIDATED (Observed lines {hunk_desc}): modified on disk at Step {binding.invalidation_step}]\n"
504
+ )
505
+ elif tag == "insert":
506
+ num_lines = bhi - blo
507
+ reconstructed_blocks.append(
508
+ f"[HUNK INSERTED on disk at Step {binding.invalidation_step} (+{num_lines} lines)]\n"
509
+ )
510
+
511
+ transformed[idx]["content"] = "".join(reconstructed_blocks)
512
+ except Exception as e:
513
+ transformed[idx]["content"] = (
514
+ f"[OBSERVATION EVICTED: Content of '{binding.file_path}' observed at Step {binding.step_id} "
515
+ f"is STALE (read error: {e})]"
516
+ )
517
+ else:
518
+ transformed[idx]["content"] = (
519
+ f"[OBSERVATION EVICTED: Content of '{binding.file_path}' observed at Step {binding.step_id} "
520
+ f"is STALE (file removed on disk)]"
521
+ )
522
+ return transformed
523
+
524
+ return transformed
@@ -0,0 +1,104 @@
1
+ """HTTP client for chat completions, with retries for transient failures.
2
+
3
+ Shared by the LM Studio and PostQode model backends. A single blip previously
4
+ discarded an entire trajectory: one arm losing a step meant the whole cell was
5
+ abandoned and its trace never written. Three of nine arms were lost this way
6
+ in one run, all failing within the same second while two requests ran
7
+ concurrently.
8
+ """
9
+
10
+ import json
11
+ import logging
12
+ import os
13
+ import socket
14
+ import ssl
15
+ import time
16
+ import urllib.error
17
+ import urllib.request
18
+ from typing import Any, Dict, Optional
19
+
20
+ logger = logging.getLogger("completion")
21
+
22
+ MAX_COMPLETION_ATTEMPTS = 5
23
+ # A local server that is unreachable (the Turing vLLM job being replaced at its
24
+ # walltime, the SSH tunnel re-establishing) is waited out for this long before
25
+ # the cell is failed. Only connection-level failures on a local endpoint count;
26
+ # cloud requests keep the plain attempt budget.
27
+ LOCAL_OUTAGE_WAIT_S = float(os.environ.get("LOCAL_OUTAGE_WAIT_S", 900))
28
+ # A TLS fault (a corrupted record, a reset mid-handshake) comes in bursts on a
29
+ # flaky path and says nothing about the request; it is waited out this long
30
+ # for any endpoint before the cell is failed. A DNS failure counts too.
31
+ TRANSPORT_FAULT_WAIT_S = float(os.environ.get("TRANSPORT_FAULT_WAIT_S", 1800))
32
+
33
+
34
+ def post_completion(
35
+ url: str,
36
+ payload: Dict[str, Any],
37
+ headers: Dict[str, str],
38
+ timeout: int,
39
+ max_attempts: int = MAX_COMPLETION_ATTEMPTS,
40
+ ) -> Dict[str, Any]:
41
+ """POST a completion request, retrying transient failures.
42
+
43
+ Retries cover network faults, 5xx and 429, and also 400 from a local
44
+ server, which LM Studio returns for transient states such as a model being
45
+ swapped or reloaded under concurrent load. A 400 from the cloud gateway is
46
+ treated as a genuine client error and raised immediately, since retrying a
47
+ malformed or oversized request only wastes the budget; its body is logged
48
+ so the cause is visible.
49
+ """
50
+ is_local = "localhost" in url or "127.0.0.1" in url
51
+ delay = 2.0
52
+ last_exc: Optional[Exception] = None
53
+ outage_since: Optional[float] = None
54
+
55
+ attempt = 0
56
+ while attempt < max_attempts:
57
+ attempt += 1
58
+ req = urllib.request.Request(url, data=json.dumps(payload).encode("utf-8"), headers=headers)
59
+ try:
60
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
61
+ return json.loads(resp.read().decode("utf-8"))
62
+ except urllib.error.HTTPError as exc:
63
+ body = ""
64
+ try:
65
+ body = exc.read().decode("utf-8", errors="replace")[:300]
66
+ except Exception:
67
+ pass
68
+ if exc.code == 400 and ("context length" in body.lower() or "max_model_len" in body.lower()):
69
+ logger.warning("Context length exceeded: %s. Terminating this task.", body)
70
+ raise RuntimeError(f"Context length exceeded: {body}")
71
+ retryable = exc.code >= 500 or exc.code == 429 or (exc.code == 400 and is_local)
72
+ if not retryable:
73
+ if exc.code == 401:
74
+ # The caller may recover (another process refreshed the
75
+ # token); a real auth failure surfaces when its retry fails.
76
+ logger.warning("Completion unauthorized: %s", body[:120])
77
+ else:
78
+ logger.error("Completion failed permanently (HTTP %s): %s", exc.code, body)
79
+ raise
80
+ last_exc = exc
81
+ logger.warning("HTTP %s on attempt %d/%d: %s", exc.code, attempt, max_attempts, body)
82
+ except Exception as exc:
83
+ last_exc = exc
84
+ unreachable = isinstance(exc, (urllib.error.URLError, ConnectionError)) or \
85
+ "Remote end closed" in str(exc)
86
+ reason = getattr(exc, "reason", None)
87
+ # A failed DNS lookup means this machine lost its network, not that
88
+ # the request is bad: waited out like a TLS fault, for any endpoint.
89
+ transport = isinstance(exc, ssl.SSLError) or isinstance(reason, (ssl.SSLError, socket.gaierror))
90
+ if (is_local and unreachable) or transport:
91
+ outage_since = outage_since or time.monotonic()
92
+ waited = time.monotonic() - outage_since
93
+ if waited < (LOCAL_OUTAGE_WAIT_S if is_local and unreachable else TRANSPORT_FAULT_WAIT_S):
94
+ logger.warning("Local server unreachable (%.0fs so far): %r; waiting", waited, exc)
95
+ attempt -= 1 # an outage does not use up the attempt budget
96
+ time.sleep(15)
97
+ continue
98
+ logger.warning("Completion error on attempt %d/%d: %r", attempt, max_attempts, exc)
99
+
100
+ if attempt < max_attempts:
101
+ time.sleep(delay)
102
+ delay = min(delay * 2, 30.0)
103
+
104
+ raise RuntimeError(f"Completion failed after {max_attempts} attempts: {last_exc!r}")