Skip to content

Commit 19b6813

Browse files
fix(polish-summaries): Batch API custom_id charset (hash ids, path in feature) (#91)
* fix(polish-summaries): Batch API custom_id charset — hash ids, path in feature First live run 400'd: custom_id must match ^[a-zA-Z0-9_-]{1,64}$ and 'sum__cli/concept.md' doesn't. custom_id becomes sum_<sha256[:24]>; the readable rel path rides in the request/result 'feature' field and apply_results maps through that instead of parsing the id. Regression test pins the charset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style(tests): pinned-black formatting for test_anthropic_fable.py Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 4ffa6b2 commit 19b6813

3 files changed

Lines changed: 38 additions & 15 deletions

File tree

src/attune_author/summaries_polish.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,16 @@ def polish_model() -> str:
131131

132132

133133
def custom_id_for(rel_path: str) -> str:
134-
"""Stable Anthropic ``custom_id`` for one template path."""
135-
return f"sum__{rel_path}"
134+
"""Stable Anthropic ``custom_id`` for one template path.
135+
136+
The Batch API requires ``^[a-zA-Z0-9_-]{1,64}$`` — template paths
137+
(slashes, dots) violate it, so the id is a path hash. The readable
138+
path rides in the request's ``feature`` field, which ``poll_batch``
139+
copies onto each result; ``apply_results`` maps results back
140+
through THAT, never by parsing the id. (Learned from a live 400 on
141+
the first attune-ai run, 2026-07-10.)
142+
"""
143+
return f"sum_{_sha256(rel_path)[:24]}"
136144

137145

138146
def build_requests(help_dir: Path, model: str, *, force: bool = False) -> BuildReport:
@@ -172,8 +180,10 @@ def build_requests(help_dir: Path, model: str, *, force: bool = False) -> BuildR
172180
)
173181
requests.append(
174182
BatchPolishRequest(
183+
# feature carries the FULL relative path — apply_results
184+
# maps batch results back through it (see custom_id_for).
175185
custom_id=custom_id_for(rel_path),
176-
feature=str(Path(rel_path).parent),
186+
feature=rel_path,
177187
depth=Path(rel_path).stem,
178188
system=_SYSTEM,
179189
user_message=user_message,
@@ -232,7 +242,7 @@ def apply_results(
232242
stamp = (_now or (lambda: datetime.now(timezone.utc)))().isoformat()
233243
counters = {"applied": 0, "truncated": 0, "errored": 0, "empty": 0}
234244
for result in results:
235-
rel_path = result.custom_id.removeprefix("sum__")
245+
rel_path = result.feature # full relative path — see custom_id_for
236246
if result.error is not None or result.text is None:
237247
counters["errored"] += 1
238248
continue

tests/test_anthropic_fable.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,7 @@ def test_fable_response_leading_thinking_block_skipped() -> None:
137137
is the answer. Hit live 2026-07-10 via the commit regen hook."""
138138
thinking = SimpleNamespace(thinking="reasoning...", type="thinking")
139139
text_block = SimpleNamespace(text="the answer", type="text")
140-
response = SimpleNamespace(
141-
content=[thinking, text_block], stop_reason="end_turn", usage=None
142-
)
140+
response = SimpleNamespace(content=[thinking, text_block], stop_reason="end_turn", usage=None)
143141
client = MagicMock()
144142
client.beta.messages.create.return_value = response
145143
assert (

tests/test_summaries_polish.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ def test_builds_one_request_per_entry(self, tmp_path):
4040
report = sp.build_requests(help_dir, "claude-sonnet-5")
4141
assert len(report.requests) == 2
4242
req = report.requests[0]
43-
assert req.custom_id == "sum__cli/concept.md"
43+
assert req.custom_id == sp.custom_id_for("cli/concept.md")
44+
assert req.feature == "cli/concept.md" # apply maps results via this
4445
assert req.model == "claude-sonnet-5"
4546
assert "Current (mechanical) summary: seed" in req.user_message
4647
assert "Body prose here." in req.user_message
@@ -78,7 +79,7 @@ def test_orphaned_sidecar_key_ignored(self, tmp_path):
7879
sidecar["gone/nope.md"] = "orphan"
7980
(help_dir / "summaries.json").write_text(json.dumps(sidecar))
8081
report = sp.build_requests(help_dir, "claude-sonnet-5")
81-
assert [r.custom_id for r in report.requests] == ["sum__cli/concept.md"]
82+
assert [r.feature for r in report.requests] == ["cli/concept.md"]
8283

8384

8485
class TestModelAndCost:
@@ -126,8 +127,8 @@ def test_strips_wrapping_quotes(self):
126127
class TestApplyResults:
127128
def _result(self, rel: str, text: str | None, error: str | None = None) -> BatchPolishResult:
128129
return BatchPolishResult(
129-
custom_id=f"sum__{rel}",
130-
feature=str(Path(rel).parent),
130+
custom_id=sp.custom_id_for(rel),
131+
feature=rel,
131132
depth=Path(rel).stem,
132133
text=text,
133134
error=error,
@@ -286,8 +287,8 @@ def test_source_hash_falls_back_to_body_sha(self, tmp_path):
286287
help_dir,
287288
[
288289
BatchPolishResult(
289-
custom_id="sum__cli/concept.md",
290-
feature="cli",
290+
custom_id=sp.custom_id_for("cli/concept.md"),
291+
feature="cli/concept.md",
291292
depth="concept",
292293
text="Polished.",
293294
error=None,
@@ -306,8 +307,8 @@ def test_apply_truncates_overlong_result_and_flags_meta(self, tmp_path):
306307
help_dir,
307308
[
308309
BatchPolishResult(
309-
custom_id="sum__cli/concept.md",
310-
feature="cli",
310+
custom_id=sp.custom_id_for("cli/concept.md"),
311+
feature="cli/concept.md",
311312
depth="concept",
312313
text=long_text,
313314
error=None,
@@ -358,3 +359,17 @@ def fake_poll(batch_id, expected, **kwargs):
358359
assert sidecar["cli/concept.md"] == "Resumed polish."
359360
assert not (help_dir / ".summaries-batch-state.json").exists()
360361
assert "applied 1" in capsys.readouterr().out
362+
363+
364+
class TestCustomIdContract:
365+
def test_matches_batch_api_charset(self):
366+
"""The Batch API enforces ^[a-zA-Z0-9_-]{1,64}$ — a raw template
367+
path (slashes, dots) 400s the whole submit. Hit live 2026-07-10."""
368+
import re
369+
370+
for rel in ("cli/concept.md", "elicitation-forms/troubleshooting.md"):
371+
cid = sp.custom_id_for(rel)
372+
assert re.fullmatch(r"[a-zA-Z0-9_-]{1,64}", cid), cid
373+
374+
def test_distinct_paths_get_distinct_ids(self):
375+
assert sp.custom_id_for("a/b.md") != sp.custom_id_for("a/c.md")

0 commit comments

Comments
 (0)