Skip to content

Commit b31e48a

Browse files
committed
落ちた回に、頼んだ相手と相手が言ったことを残す
読めない答えが返ったとき、控えには「ValueError: JSON オブジェクトが見つかりません」 としか出ず、相手の欄は既定の名前のままだった。控えを見た人は「claude が壊れた答えを 返した」と読むが、実際に返したのは別の相手で、理由を知るには AI の履歴まで掘る 必要があった。 本番でそうなった。相手の agent が背景タスクを 2 本抱えたまま親が先に手を離し、 「待っています」という状況説明を返して終わっていた(出力 4,499 トークンのうち 本文は 119 バイト)。 落ちた道でも、実際に頼んだ相手で控えを書き換える。読めない答えの先頭も添える (全文は AI の履歴にあるので、ここに要るのは一目で分かるぶんだけ)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent b9d972e commit b31e48a

5 files changed

Lines changed: 83 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,12 @@ GeoNames 全世界地名辞典 = `geonames`(いずれも 348 言語版・195 か
455455
読めなければ既定の待ち時間 —— 短くしすぎると、明ける前に頼み直して 1 回ぶん
456456
損をする。**枠と関係ない失敗では締め出さない**(つながらないだけの回もある)。
457457
断られた区画は**飛ばさず、同じ区画を次の段が引き受ける**
458+
- **落ちた回にも、頼んだ相手と相手が言ったことを残す。** 既定の名前のままだと
459+
「claude が壊れた答えを返した」と読まれる(実際に返したのは別の相手だった)。
460+
読めない答えの**先頭も控えに添える**(`collect._said`)—— 「JSON オブジェクトが
461+
見つかりません」だけだと、控えを見た人は AI の履歴まで掘らないと理由が
462+
分からない。本番でそうなった(相手の agent が背景タスクを抱えたまま
463+
「待っています」と答えて終わり、JSON を返さなかった)
458464
- **控えに残すのは、決めた相手ではなく頼んだ相手**(`main._who_ran`)。
459465
ワーカーを使う巡回は自分の欄に相手を書いていないので、書き換えないと
460466
**履歴の相手の欄が既定の名前で埋まる**(実際にそうなっていた)。

app/collect.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,10 @@
276276
# 読むのは「何が動いたか」の手がかりであって、全件の一覧ではない
277277
MAX_TITLE_SAMPLE = 20
278278

279+
# 読めなかった答えを、控えに添える長さ。**全文は AI の履歴にある** ——
280+
# ここに要るのは「何が返ってきたか」が一目で分かるぶんだけ
281+
MAX_SAID_CHARS = 120
282+
279283
# タグの確かめ方をいくつまで書けるか。**指定であって分類ではない**ので、少なくてよい
280284
MAX_VERIFY_TAGS = 8
281285
# 実在を確かめる問い合わせ 1 回ぶんの見出しの数(SQLite の上限に余裕を持たせる)
@@ -1651,6 +1655,12 @@ def _raw_url(raw: dict) -> str:
16511655
return ""
16521656

16531657

1658+
def _said(text: str) -> str:
1659+
"""相手が言ったことの先頭。**空なら「何も返さなかった」と書く**(空文字は読めない)。"""
1660+
head = " ".join((text or "").split())[:MAX_SAID_CHARS]
1661+
return head or "(何も返ってきませんでした)"
1662+
1663+
16541664
def _incoming_urls(collected) -> set[str]:
16551665
"""その回に入ってくるぶんの URL の鍵。
16561666
@@ -1915,7 +1925,11 @@ def parse_response(content: str) -> tuple[list[dict], str | None, str]:
19151925
start = stripped.find("{")
19161926
end = stripped.rfind("}")
19171927
if start < 0:
1918-
raise ValueError("JSON オブジェクトが見つかりません")
1928+
# **何が返ってきたかを添える。** 「見つかりません」だけだと、控えを見た人は
1929+
# AI の履歴まで掘らないと理由が分からない —— 実際にそうなった(相手の agent が
1930+
# 背景タスクを抱えたまま「待っています」と答えて終わり、JSON を返さなかった)。
1931+
# **先頭だけ**にするのは、控えに答えを丸ごと写す場所ではないから
1932+
raise ValueError(f"JSON オブジェクトが見つかりません: {_said(stripped)}")
19191933
try:
19201934
payload = json.loads(stripped[start : end + 1]) if end > start else None
19211935
except ValueError:

app/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,9 @@ async def _collect_material(name: str, sources: dict) -> str:
853853
except Exception as e:
854854
if hasattr(items := locals().get("items"), "close"):
855855
items.close()
856+
# **落ちた回にも、頼んだ相手を残す。** 既定の名前のままだと、控えを見た人は
857+
# 「claude が壊れた答えを返した」と読む —— 実際に返したのは別の相手だった
858+
who.update(_who_ran(used) or {})
856859
reason = f"{type(e).__name__}: {e}"
857860
log.warning("collect %s failed: %s", name, reason)
858861
await asyncio.to_thread(

tests/test_collect.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1745,6 +1745,37 @@ def test_a_once_sweep_runs_only_once(self, sample):
17451745
assert collect.require_runnable(collect.get("news"), "名簿").name == "名簿"
17461746

17471747

1748+
class TestWhenTheAnswerIsNotJson:
1749+
"""**何が返ってきたかを控えに添える。**
1750+
1751+
「JSON オブジェクトが見つかりません」だけだと、控えを見た人は AI の履歴まで
1752+
掘らないと理由が分からない —— 本番でそうなった(相手の agent が背景タスクを
1753+
抱えたまま「待っています」と答えて終わり、JSON を返さなかった)。
1754+
"""
1755+
1756+
def test_the_reason_carries_what_came_back(self):
1757+
with pytest.raises(ValueError) as got:
1758+
collect.parse_response(
1759+
"The background task to retrieve painter information is currently running."
1760+
)
1761+
1762+
assert "background task" in str(got.value)
1763+
1764+
def test_a_long_answer_is_cut(self):
1765+
with pytest.raises(ValueError) as got:
1766+
collect.parse_response("あ" * 500)
1767+
1768+
# **全文は AI の履歴にある**。控えに要るのは一目で分かるぶんだけ
1769+
assert len(str(got.value)) < 200
1770+
1771+
def test_an_empty_answer_says_so(self):
1772+
"""空文字は読めない —— 「何も返さなかった」と書く。"""
1773+
with pytest.raises(ValueError) as got:
1774+
collect.parse_response(" ")
1775+
1776+
assert "何も返ってきませんでした" in str(got.value)
1777+
1778+
17481779
class TestStartingTheLapOver:
17491780
"""一周をやり直す(`collect.restart_cycle`)。
17501781

tests/test_workers.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,34 @@ def test_a_relayed_run_names_both(self):
720720
assert out["backend"] == "antigravity → codex"
721721
assert out["model"] == "gemini → gpt-5.5"
722722

723+
def test_a_failed_run_still_names_who_answered(self, enabled, monkeypatch):
724+
"""**落ちた回にも頼んだ相手を残す。** 既定の名前のままだと、控えを見た人は
725+
「claude が壊れた答えを返した」と読む —— 実際に返したのは別の相手だった。
726+
"""
727+
import fastapi
728+
729+
from app import collect, main
730+
731+
monkeypatch.setenv("CHIEZO_NOTES_DIR", str(enabled / "corpus"))
732+
workers.save([workers.Worker("精査", (workers.Step("antigravity", "gemini"),))])
733+
_quota("antigravity", 10.0)
734+
735+
async def garbage(_asked, _messages):
736+
return "背景の仕事を待っています"
737+
738+
monkeypatch.setattr(main, "_ask_for_collection", garbage)
739+
collect.create("news", prompt="p", interval_minutes=60,
740+
sweeps=[{"name": "ざっと", "worker": "精査"}])
741+
742+
used: list = []
743+
with pytest.raises((ValueError, fastapi.HTTPException)):
744+
asyncio.run(main._collect_items(
745+
collect.get("news"), {}, {}, [], collect.sweeps_of(collect.get("news"))[0],
746+
None, None, None, used,
747+
))
748+
749+
assert main._who_ran(used)["backend"] == "antigravity"
750+
723751
def test_nothing_ran_leaves_the_record_alone(self):
724752
"""AI を呼ばない回では書き換えない(既定の相手が並ぶのを防ぐ)。"""
725753
from app import main

0 commit comments

Comments
 (0)