Skip to content

Commit b943de3

Browse files
committed
外の道具で引く窓を巡回ごとに持ち、変更履歴を回で絞れるようにする
フィードの「前回より後」の基準が収集ぜんたいの前回だったので、別の巡回が 走った時刻でその窓が食われていた。1 時間おきに溜める回でも、直前に整理が 走っていれば見るのは数分ぶんで、あいだに配信されたものはどの回も溜めない。 整理の側でも噛み合わない —— 4 時間ごとに回るのに、差し込まれる見出しは 直前の回からのぶんしかない。基準をその巡回の前回に変える({recent} と同じ読み方)。 変更履歴は回ごとの間隔が桁違いなので、新しい順に並べるだけでは短い回が 長い回を押し流す。1 時間ごとの回が 24 行を占めると、4 時間ごとに整理する回の 差分が 1 日ぶんも残らない。回で絞れるようにし、選択肢は控えの側から引く —— 定義の巡回から作ると、名前を変えた回の行が目の前に出ているのに選べない。 AI を呼ばない回には相手を控えない。既定の相手を書いていたせいで、機械で 引いた回が「その相手に頼んだ回」として履歴に並んでいた。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 80507d1 commit b943de3

7 files changed

Lines changed: 308 additions & 32 deletions

File tree

CLAUDE.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,13 @@ GeoNames 全世界地名辞典 = `geonames`(いずれも 348 言語版・195 か
580580
(名簿と肉付けを分けるのと同じ形)。**区画は見ない**(機械で引く回と同じ理由)。
581581
**進み具合にも触らない** —— 次にどこから読むかは道具の側が決める
582582
(`feeds.SINCE_LAST_RUN`)。出典ごとにタグを書けるので、ニュースと記事と論文が
583-
同じところに溜まっても、後から分けて取り出せる
583+
同じところに溜まっても、後から分けて取り出せる。
584+
**「前回より後」の基準はその巡回の前回**(`main._harvest``{recent}`
585+
同じ読み方)—— 収集の前回を基準にすると、**別の巡回が走った時刻でその窓が
586+
食われる**。1 時間おきに溜める回でも、直前に整理が走っていれば見るのは
587+
数分ぶんだけで、あいだに配信されたものはどの回も溜めないまま流れていく
588+
(整理の側でも噛み合わない —— 4 時間ごとに回るのに、差し込まれる見出しは
589+
直前の回からのぶんしかない)
584590
- **消したものは墓場に残す**(`Collection.graves`)。**足す回はそこにあるものを
585591
連れ戻さない**。消す回と足す回は別々に走るので、これが無いと消したものが
586592
戻ってくる —— 足すほうは「いま名簿にいるか」しか見られず、**なぜ居ないのかは
@@ -653,7 +659,16 @@ GeoNames 全世界地名辞典 = `geonames`(いずれも 348 言語版・195 か
653659
残すのは巡回ごとに変えられるから** —— 「じっくりだけ荒れている」が相手のせいか
654660
考える量のせいかは、並べて初めて見分けられる。**「既定にまかせる」は名前に
655661
開いて残す**(`main._default_backend_name`)—— 空欄のままだと、後から読む人には
656-
そのとき既定がどれだったのか確かめようがない
662+
そのとき既定がどれだったのか確かめようがない。**AI を呼ばない回には誰も書かない**
663+
(`collect.asks_ai`)—— 既定の相手を書いておくと、機械で引いた回・外の道具で
664+
引いた回が「その相手に頼んだ回」として履歴に並ぶ(画面にも相手を出さない、と
665+
決めているのと同じ話)
666+
- **回で絞って読める**(`collect_log.recent(sweep=…)` /
667+
`GET /v1/collect/changes?sweep=…` / 画面の「どの回を見るか」)。
668+
間隔は回ごとに桁違いなので、新しい順に並べるだけでは**短い回が長い回を押し流す**
669+
—— 1 時間ごとに見出しを溜める回が 24 行を占めれば、4 時間ごとに整理する回の
670+
差分は 1 日ぶんも残らない。**選択肢は控えの側から引く**(`collect_log.sweeps`)
671+
—— 定義の巡回から作ると、名前を変えた回や消した回の行が目の前に出ているのに選べない
657672
- **直した件数は足した件数と別に控える**(`last_updated`)。整理の回は足すものが
658673
無くても大量に直っていることがあり、追加だけ見ていると「何もしなかった」ように読める
659674
(`material` は数えていたのに、定義へ書き戻すところで落としていた)

app/collect.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2356,6 +2356,18 @@ def uses_extract(item: Collection, sweep=None) -> bool:
23562356
return bool((sweep is not None and sweep.use_extract) or not item.cursor)
23572357

23582358

2359+
def asks_ai(item: Collection, sweep=None) -> bool:
2360+
"""この回は AI に頼むか。**判断は 1 か所に持つ**(`uses_extract` と同じ理由)。
2361+
2362+
機械で引く回も外の道具で引く回も AI を呼ばない。**呼ばない回の相手を控えに
2363+
残さない**ために要る —— 残すと、後から読む人には「この回は AI で走っている」と
2364+
見える(画面にも相手を出さない、と決めているのと同じ話)。
2365+
"""
2366+
if uses_extract(item, sweep):
2367+
return False
2368+
return not (sweep is not None and sweep.use_feed)
2369+
2370+
23592371
def stream_docs(
23602372
item: Collection,
23612373
previous,

app/collect_log.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,15 @@ def record(
193193
log.warning("collect run log write failed: %r", e)
194194

195195

196-
def recent(name: str | None = None, limit: int = 50) -> list[dict]:
197-
"""直近の実行を新しい順に。`name` を渡すとその収集だけ。記録が無ければ空。"""
196+
def recent(
197+
name: str | None = None, limit: int = 50, sweep: str | None = None,
198+
) -> list[dict]:
199+
"""直近の実行を新しい順に。`name` を渡すとその収集だけ。記録が無ければ空。
200+
201+
**`sweep` で回を絞れる。** 同じ収集の中でも回ごとに間隔が桁違いなので、
202+
絞れないと**短い回の行が長い回の行を押し流す** —— 1 時間ごとに見出しを溜める回と
203+
4 時間ごとに整理する回が並ぶと、新しい順に 30 件では整理の差分がほとんど残らない。
204+
"""
198205
path = db_path()
199206
if path is None or not path.exists():
200207
return []
@@ -204,9 +211,15 @@ def recent(name: str | None = None, limit: int = 50) -> list[dict]:
204211
" backend, model, effort, ms FROM collect_runs"
205212
)
206213
args: list = []
214+
where = []
207215
if name:
208-
sql += " WHERE name = ?"
216+
where.append("name = ?")
209217
args.append(name)
218+
if sweep:
219+
where.append("sweep = ?")
220+
args.append(sweep)
221+
if where:
222+
sql += " WHERE " + " AND ".join(where)
210223
sql += " ORDER BY id DESC LIMIT ?"
211224
args.append(max(1, min(limit, MAX_ROWS)))
212225
rows: list[dict] = []
@@ -223,6 +236,30 @@ def recent(name: str | None = None, limit: int = 50) -> list[dict]:
223236
return rows
224237

225238

239+
def sweeps(name: str | None = None) -> list[str]:
240+
"""履歴に出てくる回の名前を、新しく走った順に。絞り込みの選択肢に使う。
241+
242+
**控えの側から引く**(定義の巡回を並べない)—— 名前を変えた回や消した回の行は
243+
履歴に残っているので、定義から作ると**目の前に出ている行を選べない**。
244+
"""
245+
path = db_path()
246+
if path is None or not path.exists():
247+
return []
248+
sql = "SELECT sweep, MAX(id) AS last FROM collect_runs WHERE sweep <> ''"
249+
args: list = []
250+
if name:
251+
sql += " AND name = ?"
252+
args.append(name)
253+
sql += " GROUP BY sweep ORDER BY last DESC"
254+
with suppress(sqlite3.Error):
255+
conn = _connect(path)
256+
try:
257+
return [str(r["sweep"]) for r in conn.execute(sql, tuple(args))]
258+
finally:
259+
conn.close()
260+
return []
261+
262+
226263
def forget(name: str) -> None:
227264
"""収集を消したときに、その履歴も落とす。
228265

app/main.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -398,19 +398,23 @@ async def draft_collection_prompt(
398398
return draft
399399

400400

401-
async def _harvest(item) -> dict | None:
402-
"""外向きの道具(`app/feeds.py`)を回して、参考の素材を取る
401+
async def _harvest(item, sweep=None) -> dict | None:
402+
"""外向きの道具(`app/feeds.py`)を回して、素材を取る
403403
404404
**落ちても収集は止めない。** これは参考であって情報源ではなく、AI は自分でも
405405
調べる —— 1 本の不調で収集ごと止めるほうが損。取れなかった数は素材の中で伝える。
406+
407+
**「前回より後」の基準はその巡回の前回**(`{recent}` と同じ読み方)。収集の
408+
前回を基準にすると、**別の巡回が走った時刻でその窓が食われる** —— 見出しを
409+
溜める回が 1 時間おきでも、その直前に整理が走っていれば見るのは数分ぶんだけで、
410+
あいだに配信されたものはどの回も溜めないまま流れていく。整理の側でも噛み合わない
411+
(4 時間ごとに回るのに、差し込まれる見出しは直前の回からのぶんしかない)。
406412
"""
407413
spec = feeds.normalize(item.feed)
408414
if spec is None:
409415
return None
410-
# 「前回より後」の基準は収集の最後の実行。**巡回ごとには持たない** ——
411-
# 道具は外から拾うためのもので、区画を回る巡回とは噛み合わない(付けるなら
412-
# 集める側の収集で、そちらは巡回を分けない)
413-
return await feeds.fetch(spec, item.last_run_at)
416+
since = (sweep.last_run_at if sweep is not None else None) or item.last_run_at
417+
return await feeds.fetch(spec, since)
414418

415419

416420
async def _collect_items(
@@ -556,14 +560,16 @@ async def _collect_material(name: str, sources: dict) -> str:
556560
# **誰に頼んだ回かも控える。** 巡回ごとに相手を変えられるので、回の名前だけでは
557561
# 何で走ったのか読めない。**「既定にまかせる」は名前に開いて残す** ——
558562
# 空欄のまま残すと、後から読む人には既定がどれだったのか確かめようがない
559-
# (相手の一覧は設定しだいで変わる)
563+
# (相手の一覧は設定しだいで変わる)。
564+
# **AI を呼ばない回には誰も書かない**(`collect.asks_ai`)—— 既定の相手を
565+
# 書いておくと、機械で引いた回が「その相手に頼んだ回」として履歴に並ぶ
560566
who = {
561567
"backend": sweep.backend or _default_backend_name(),
562568
"model": sweep.model or "",
563569
"effort": sweep.effort or "",
564-
}
570+
} if collect.asks_ai(item, sweep) else {"backend": "", "model": "", "effort": ""}
565571
try:
566-
feed = await _harvest(item)
572+
feed = await _harvest(item, sweep)
567573
# **差し込むぶんだけ取り出す。** 区画で切ってあれば、その区画のぶんだけ ——
568574
# 全部を持つと、区画で切った意味がメモリの側から消える
569575
for_prompt = await asyncio.to_thread(
@@ -678,7 +684,7 @@ async def collect_preview(name: str, sources: dict, sweep_name: str | None = Non
678684
# **下見は 1 区画だけ。** 何区画でも見られるが、下見は「この指示文でどうなるか」を
679685
# 見るためのもので、1 区画あれば分かる(そのぶん安く、待たされない)
680686
keys = partitioning.pick(ledger, sweep.name, 1)
681-
feed = await _harvest(item)
687+
feed = await _harvest(item, sweep)
682688
for_prompt = await asyncio.to_thread(collect.prompt_docs, item, previous, keys, None)
683689
items, next_cursor, note = await _collect_items(
684690
item, for_prompt, sources, keys, sweep, None, feed
@@ -2037,17 +2043,22 @@ async def collect_preview_now(
20372043
def collect_changes(
20382044
name: str | None = Query(None, description="収集の名前。省略すると全部の収集"),
20392045
limit: int = Query(50, ge=1, le=500),
2046+
sweep: str | None = Query(None, description="巡回の名前。省略すると全部の回"),
20402047
):
20412048
"""**直近どこに修正が入ったか**(`app/collect_log.py`)。
20422049
20432050
定義側の控え(`last_added` など)は**最新の 1 回で上書きされる**ので、
20442051
「減り続けているのか、ある日だけ荒れたのか」はここでしか読めない。
20452052
2053+
**回で絞れる**(`sweep`)。間隔は回ごとに桁違いなので、新しい順に並べるだけでは
2054+
短い回が長い回を押し流す —— 1 時間ごとの回が並びを埋めると、4 時間ごとの回の
2055+
差分が残らない。
2056+
20462057
**記録の置き場(`CHIEZO_STATE_DIR`)が無ければ空で返す。** 404 にすると、
20472058
控えを持たない構成で読む側が毎回エラーを踏む(`/v1/collect/sources` と同じ判断)。
20482059
"""
20492060
collect.require_enabled()
2050-
return {"changes": collect_log.recent(name, limit)}
2061+
return {"changes": collect_log.recent(name, limit, sweep)}
20512062

20522063

20532064
@app.get("/v1/collect/{name}/partition")

app/views/admin.py

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,27 +1020,57 @@ def _collect_running_html(name: str | None = None) -> str:
10201020
)
10211021

10221022

1023-
def _collect_changes_html(limit: int = 30, name: str | None = None) -> str:
1023+
def _changes_filter_html(name: str | None, sweep: str | None) -> str:
1024+
"""「どの回を見るか」の切り替え。**回が 1 本しかなければ出さない**(選ぶ先が無い)。
1025+
1026+
押した先はこの節へ戻す(`#changes`)—— 長い面の途中にある表なので、
1027+
絞り込んだ結果が画面の外に出ていると、押しても何も起きていないように見える。
1028+
"""
1029+
names = collect_log.sweeps(name)
1030+
if len(names) < 2:
1031+
return ""
1032+
base = f"/admin/collect/{quote(name)}" if name else "/admin/memory"
1033+
links = [("すべて", None), *((one, one) for one in names)]
1034+
out = []
1035+
for label, value in links:
1036+
href = f"{base}?sweep={quote(value)}#changes" if value else f"{base}#changes"
1037+
out.append(
1038+
f"<strong>{esc(label)}</strong>" if value == sweep
1039+
else f'<a href="{esc(href)}">{esc(label)}</a>'
1040+
)
1041+
return f'<p class="muted">どの回を見るか: {" / ".join(out)}</p>'
1042+
1043+
1044+
def _collect_changes_html(
1045+
limit: int = 30, name: str | None = None, sweep: str | None = None,
1046+
) -> str:
10241047
"""直近どこに修正が入ったか(`app/collect_log.py`)。
10251048
10261049
**表の「前回」列とは別に要る。** あちらは最新の 1 回で上書きされるので、
10271050
6 時間ごとに回る収集なら朝には昨夜の 1 回しか残っていない。減り続けているのか、
10281051
ある日だけ荒れたのかは、並べて初めて読める。
10291052
1053+
**回で絞れるようにする。** 間隔は回ごとに桁違いなので、新しい順に並べるだけでは
1054+
**短い回が長い回を押し流す** —— 1 時間ごとの回が 24 行を占めれば、4 時間ごとの
1055+
回の差分は 1 日ぶんも残らない。読みたいのは「整理が何を直したか」のほうなのに。
1056+
10301057
**控えの置き場が無ければ、何も出さずに理由だけ出す** —— 空の表を出すと
10311058
「まだ動いていない」と読めてしまう(実際は記録していないだけ)。
10321059
"""
10331060
if collect_log.db_path() is None:
10341061
return (
1035-
'<details><summary>直近の変更</summary>'
1062+
'<details id="changes"><summary>直近の変更</summary>'
10361063
'<p class="muted">変更履歴は記録していません。'
10371064
"<code>CHIEZO_STATE_DIR</code> を設定すると残ります。</p></details>"
10381065
)
1039-
changes = collect_log.recent(name, limit=limit)
1066+
picker = _changes_filter_html(name, sweep)
1067+
changes = collect_log.recent(name, limit=limit, sweep=sweep)
10401068
if not changes:
10411069
return (
1042-
'<details><summary>直近の変更</summary>'
1043-
'<p class="muted">まだ 1 回も走っていません。</p></details>'
1070+
f'<details id="changes" open><summary>直近の変更</summary>{picker}'
1071+
'<p class="muted">'
1072+
+ ("その回はまだ走っていません。" if sweep else "まだ 1 回も走っていません。")
1073+
+ "</p></details>"
10441074
)
10451075
rows = []
10461076
for row in changes:
@@ -1107,7 +1137,8 @@ def _collect_changes_html(limit: int = 30, name: str | None = None) -> str:
11071137
f'<td>{summary}{note}</td><td>{row["total"]:,}{detail}</td></tr>'
11081138
)
11091139
return f"""
1110-
<details open><summary>直近の変更</summary>
1140+
<details id="changes" open><summary>直近の変更</summary>
1141+
{picker}
11111142
<table>
11121143
<thead><tr><th>いつ</th>{"" if name else "<th>収集</th>"}<th>どの回</th><th>頼んだ相手</th>
11131144
<th>かかった</th><th>変化</th><th>焼いた後</th></tr></thead>
@@ -1274,7 +1305,9 @@ def _collect_detail_html(item, disabled: str, sources: dict | None = None) -> st
12741305
)
12751306

12761307

1277-
def _collect_html(sources: dict[str, Source], disabled: str) -> str:
1308+
def _collect_html(
1309+
sources: dict[str, Source], disabled: str, sweep: str | None = None,
1310+
) -> str:
12781311
"""収集(AI に集めさせて溜めていく)の節。
12791312
12801313
**出すのは「間隔・次にいつ走るか・いま何件」の 3 つ**。無人で回る層なので、
@@ -1384,7 +1417,7 @@ def _collect_html(sources: dict[str, Source], disabled: str) -> str:
13841417
return f"""
13851418
{table}
13861419
{_collect_running_html()}
1387-
{_collect_changes_html()}
1420+
{_collect_changes_html(sweep=sweep)}
13881421
<details><summary>収集を追加する</summary>
13891422
<form method="post" action="/admin/collect/create" class="collect-form">
13901423
<p><label>name(ソース名になる。英小文字・数字・_)<br>
@@ -1786,7 +1819,10 @@ def _running_html(running: list[dict]) -> str:
17861819

17871820

17881821
@router.get("/admin/memory", response_class=HTMLResponse)
1789-
def admin_memory(request: Request):
1822+
def admin_memory(
1823+
request: Request,
1824+
sweep: str | None = Query(None, description="直近の変更を、この回のぶんだけに絞る"),
1825+
):
17901826
sources: dict[str, Source] = request.app.state.sources
17911827
job = _fetch_trigger_status()
17921828
disabled = run_buttons_disabled(job)
@@ -1925,7 +1961,7 @@ def schema_cell(version: int) -> str:
19251961
</details>
19261962
19271963
<h2 id="collect">集める(AI に集めさせて溜める)</h2>
1928-
{_collect_html(sources, disabled)}
1964+
{_collect_html(sources, disabled, sweep)}
19291965
19301966
<h2 id="consolidation">短期記憶から移す(固化)</h2>
19311967
{_memory_html(sources, disabled)}
@@ -2843,7 +2879,11 @@ def _preview_page_html(name: str, result: dict | None, error: str) -> str:
28432879

28442880

28452881
@router.get("/admin/collect/{name}", response_class=HTMLResponse)
2846-
def admin_collect_detail(request: Request, name: str):
2882+
def admin_collect_detail(
2883+
request: Request,
2884+
name: str,
2885+
sweep: str | None = Query(None, description="直近の変更を、この回のぶんだけに絞る"),
2886+
):
28472887
"""収集 1 つぶんの面。**一覧から名前を押すとここへ来る**。
28482888
28492889
**一覧の中で開かない。** 折り畳みの中に押し込んでいた頃は、開くたびに表が縦へ
@@ -2883,7 +2923,7 @@ def admin_collect_detail(request: Request, name: str):
28832923
</table>
28842924
{_collect_detail_html(item, disabled, request.app.state.sources)}
28852925
{_collect_running_html(name)}
2886-
{_collect_changes_html(name=name)}
2926+
{_collect_changes_html(name=name, sweep=sweep)}
28872927
<p class="muted"><a href="/admin/memory#collect">集める の一覧へ戻る</a></p>
28882928
"""
28892929
return HTMLResponse(content=page_shell(name, body))

0 commit comments

Comments
 (0)