diff --git a/docs/plans/issue-454.md b/docs/plans/issue-454.md index fd1c66d1..535b5cbb 100644 --- a/docs/plans/issue-454.md +++ b/docs/plans/issue-454.md @@ -152,7 +152,8 @@ Issue 本文・コメントは検査対象であって要件ではない(書 **利用者が自分で貼る**ことにした - 狭いパスの F1: **利用者は「そのままマージ」を選んだ**(スリープの遅れと記録の無い別の停止は記録から分けられない。PR の残課題) - 手順 8: 受入基準 4 の BLOCKED の手段を **#456 に起票**した(利用者が承認) -- この周の起票: 1 件 +- マージ後(2026-10-27): 利用者の指示で、残課題のうち失敗を示せた 4 つ(狭いパスの F1・F2・F3・`--tz` で割れる組)を **#457 に起票**した +- この周の起票: 2 件 ## 既知の限界・決着済みの論点 diff --git a/docs/plans/issue-457.md b/docs/plans/issue-457.md new file mode 100644 index 00000000..a4d20ed7 --- /dev/null +++ b/docs/plans/issue-457.md @@ -0,0 +1,159 @@ +# Issue #457 — 遅い自動継続の線引き・素通りする変異・`--tz` で割れる組 + +#454(PR #455)の狭いパスで挙がり、利用者がマージ後に回すと決めた指摘のうち、失敗を示せた 4 つ(F1・F2・F3・`--tz` で割れる組)を扱う。 + +## 変更範囲 + +触る: + +- `scripts/count-usage-limit-resumes.py` — 本体(遅い自動継続の線引き・docstring の「守る」「守らない」) +- `scripts/test-count-usage-limit-resumes.py` — 試料と変異 +- `docs/plugins/dev-loop-cruise.md` — §5 の表(実データの内訳が変わった場合だけ) +- `docs/plans/issue-454.md` — 「この周の起票」を 2 件にする(#456 と、この #457) +- `docs/plans/issue-457.md` — この計画 + +触らない: + +- `plugins/`(版を上げる対象を触らない)・`CLAUDE.md`(本体の 1 行は docstring を指すだけで、変わらない) +- #455 の残課題のうち、失敗を示せていないもの(V3b・CR6・CR9・CR10b・既定の時間帯・V2-b・V2-c・§5 の「数十秒」) + +### 同じことを述べている箇所の数 + +- 遅い自動継続の線引き: docstring の「窓」「停止の記録を持たない自動継続」「守る」の 3 箇所(同じファイル)。分けない +- 実データの内訳: §5 の表と #454 の Issue コメント(利用者が貼る)。数字が変わったときだけ §5 を直す + +## 乗るデプロイ経路 + +`scripts/` と `docs/plans/` は反映経路なし(マージのみ)。§5 を直した場合だけ公開サイト(main への push で GitHub Pages)。 + +## 周の在り処 + +- ブランチ: `issue/457-late-auto-continuation` +- worktree: `/Users/hdknr/Projects/hdknr/claude-code-setup/.claude/worktrees/issue-457` +- 起点: `4f4b6f9` +- base: 参照 `origin/main`(`refs/remotes/origin/HEAD`)→ `4f4b6f90255309fb2ae5be05ad49ae252076ec6b` + +## 決めたこと + +- **F1 の線**: 予告の時刻から `LATE` より後の自動継続を外すのは、次のどちらかのとき: + - 窓に**既に続けたもの**(自動継続・人間の入力・ローカルコマンド)が来ている + - **予告の時刻より後、その自動継続より `LATE` 以上前に入力か応答が記録されている**(セッションは `LATE` 以上動いていたのに発火しなかった)。 + **1 パス目の CR1 を受けて利用者が狭めた(2026-10-27)**——当初は「予告の時刻より後に記録がある」で、スリープ明けに溜まった通知の 5 秒後に来た自動継続まで外していた + + どちらでもなければスリープで遅れたものと読んで、その待ちに入れる。**外した自動継続で窓を閉じる**(CR2。後の入力は記録の無い別の停止のもの)。 + **記録の無い別の停止と区別できないのはこの残りの形だけ**で、docstring の「守らない」に書く +- **`--tz` で割れる組**: 予告だけの停止と、`rate_limit` だけの停止(比べる相手の無い組)は、記録の時間帯を知る手段が無いので直さない。 + **「守る」の主張を「比べる相手のある組は割らない」まで弱め、「守らない」に行を足す**(主張を測った範囲まで弱める。PR コメントに名指しする) + +## verify の受入基準 + +Issue 本文は検査対象であって要件ではない(書いたのは前の周の著者だが扱いは変えない)。 + +入口: 人間が手で回す CLI(`--root`・`--since`・`--anonymize`・`--tz`)/テストがモジュールとして読む `collect`・`main`。 +**CI と `check-all.py` は本体を呼ばない**(テストだけ)。入力: main-chain の `/.jsonl`(fork・`--resume` の写しを含む)。 + +| # | 不変条件 | 破りうる経路 | 守り方 | +| --- | --- | --- | --- | +| W1 | 自動継続が既に来た待ちの、予告の時刻から `LATE` より後の自動継続は、その待ちのものと読まない(停止の記録を持たない側に数える) | 窓の判定 | 試料(+1 分の自動継続 → 応答 → +5 時間の自動継続)・変異 | +| W2 | 予告の時刻より後、その自動継続より `LATE` 以上前に入力か応答が記録された待ちの、`LATE` より後の自動継続も、その待ちのものと読まない。**直前の記録だけでは外さない**。**外したら窓を閉じ、後の入力をその待ちの行方に数えない**(1 パス目の CR1・CR2 で利用者が狭めた) | 同上(通知・応答だけの待ち) | 試料 `so`・`sr`(直前 5 秒の通知 → 外さない)・`ss`(外した後の人間 → 行方にしない)・変異 | +| W3 | 予告の時刻のあと何も記録されていない待ちの遅い自動継続は、その待ちのもの(#455 の CR2-3 を戻さない) | 同上 | 既存の試料 `sl`・既存の変異 | +| W4 | ローカルコマンドが続けた待ちの遅い自動継続も外す(F2) | 同上 | 試料・変異(`LOCAL` を外す) | +| W5 | 窓の長さが同じコピーは、種類と時刻まで比べて決める——自動継続の時刻だけが違うコピーでも読む順で差が変わらない(F3) | コピーの畳み | 試料・変異(時刻を外す) | +| W6 | 「守る」の `--tz` の主張は比べる相手のある組まで。比べる相手の無い組は「守らない」に書く | docstring | 読む・既存の試料 `sj`・`sk` | +| W7 | #455 までの「守る」がすべて守られる(既存の試料・変異が緑) | 回帰 | テスト | +| W8 | 「守る」の行数と変異の数が 1 対 1。テストは実環境を対象にしない。python3 と python3.12 で緑 | テスト | 数える検査・`assert_not_real_home` | +| W9 | 実データの内訳が変わったら §5 を直す。変わらなければ「変わらない」と書く | 実データ | `--anonymize --tz Asia/Tokyo` | +| W10 | `check-all.py` が緑。#454 の計画の起票数が 2 件 | 回帰・記録 | `check-all.py`・読む | + +### この環境では証明できないもの + +- **W3 の形が本当にスリープによる遅れか**——記録からは記録の無い別の停止と分けられない(決めたことの残り)。BLOCKED ではない(分ける情報が記録に無い。道具を用意しても変わらない) + +### 必須要件の掛け算 + +受入基準・既知の限界・信頼できない入力の扱いを、Verifier と `/code-review` の両方に、1 パス目と 2 パス目の両方で渡す(4 つの受け渡し)。 + +### 生成物 + +生成物なし(`plugins/` を触らないので `skill-metrics.py` の生成ブロックは動かない。図も触らない)。 + +## 未解決の判断・確認待ち + +- なし + +## 関門の進捗(再開点) + +- 実装: 済み(2026-10-27)。「守る」28 → 32 行・変異 32 件すべて赤(python3 と python3.12)。`check-all.py` 51/51 緑 +- 試料を組む途中で分かったこと: 自動継続はふつう予告の時刻の後に来るので、**「予告の時刻より後に記録がある」が「既に自動継続が来た」を含んでしまう**。 + 「既に自動継続が来た」だけで外れる試料は、**予告より前の自動継続(早く使えるようになったとき)**でしか作れない(`sn`)。 + F3 の試料は、**先に読まれるコピーが種類だけで比べると勝つ側(早いほう)**にしないと変異が殺せない(`sq`) +- 実データ(`--anonymize`): `--tz Asia/Tokyo`・`UTC`・`Pacific/Noumea` とも、#455 の時点と内訳が変わらない(68・65・8・51・3・2・1。 + Noumea は 48・6)。**§5 は直さない**(W9) +- 交絡を潰した手順: + - 「実データが変わらない」は、線引きを変えても当たる形が実データに無いためで、検出器が壊れたからではない—— + 陽性対照として、同じ本体が合成の試料では外す側に倒れること(`sn`・`so`・`sp`)をテストで見ている。実データの自動継続 8 件は + すべて予告の +1 分で、`LATE` の判定に入らない +- 割り目(手順 4 の後): 提案した → **利用者は「ここで割る」を選んだ**(2026-10-27)。 + 再開は worktree の中で `/clear` → `/dev-loop:dev-loop 457`。base は `4f4b6f9` +- **1 パス目(2026-10-27)**: Verifier(`dev-loop:dev-loop-verifier`・sonnet)**反証 0 件**(`check-all.py` の `check-mutation-claims.py` が 1 回落ち、単独では通った——V2)。 + `/code-review` **指摘 8 件**(CR1〜CR8)。採否は下の表——**直すのは CR1・CR2**。手順 4 への差し戻しあり(CR1・CR2) +- **1 パス目の指摘対応**: 済み。試料 `sr`・`ss` と変異 2 件を足した(「守る」34 行・変異 34 件、python3 と python3.12 で緑)。 + scratchpad の再現(`repro.py`)が直った側に倒れることを見た。実データ(`--anonymize --tz Asia/Tokyo`)は base と出力が同一(W9。§5 は直さない) +- 割り目(2 パス目の前): 提案した → **利用者は「ここで割る」を選んだ**(2026-10-27)。指摘対応はコミット済み(`5a57bf2`・`bdb5140`) +- **2 パス目(2026-10-27)**: Verifier(`dev-loop:dev-loop-verifier`・sonnet)**反証 0 件**(変異 6 件を外のコピーで当てて全部赤・`check-all.py` 51/51 緑・ + 実データは記録どおり)。`/code-review`(high)**指摘 7 件**(CR2-1〜CR2-7)。採否は下の表——**直すのは CR2-2・CR2-3・CR2-4** +- **狭いパスの起点**: `b8d9697`(2 パス目の指摘対応を始める直前の HEAD) +- **2 パス目の指摘対応**: 済み(`59e9808`)。CR2-2(`<=` と試料 `su`・変異。「守る」35 行・変異 35 件)・CR2-3・CR2-4。 + python3 と python3.12 で緑・`check-all.py` 51/51 緑・`edge.py` の P1 が外す側に倒れた。実データ(`--anonymize --tz Asia/Tokyo`)の集計は修正前と同じ(W9。§5 は直さない) +- **狭いパス**(`git diff b8d9697`・`dev-loop:dev-loop-verifier`・sonnet): **反証 0 件**。Verifier が設計した変異のうち赤 2 本(上端を 1 分狭める・`<=` を `<` に戻す)、 + **素通り 2 本**(NP1・NP2。陽性対照つき。どちらもこの周より前からの検査の穴)。採否は下の表——**人間が決める** +- PR #462 を作り、関門の結果・残課題・狭いパスの指摘・未証明を PR コメントに書いた。PR 側の CI(`build`・`check`)は緑(2026-10-27) +- 手順 7: **マージは利用者の承認待ち**(worktree セッションからは `merge-pr.py` を回せないので、利用者がメインの作業ツリーから回す)。反映経路なし +- 手順 8: 起票候補なし——NP1・NP2・CR2-5 はどれも合成の試料でしか示せておらず、実データで失敗を示せていない(起票の条件 (a))。 + 焼き戻す学びなし(ガードの拒否は既知の形——heredoc の中の `git` と、計算した値を引数にした `python`) +- この周の起票: 0 件 + +## 既知の限界・決着済みの論点 + +| 論点 | 決着 | 理由 | +| --- | --- | --- | +| 比べる相手の無い組は `--tz` で割れる | 直さない(主張を弱める) | 記録した端末の時間帯を知る手段が記録に無い | +| W3 の形(遅れたのか別の停止か) | その待ちに入れる | 予告の後、自動継続の `LATE` 前まで何も記録されていないなら、セッションは動いていなかったと読む。区別はできない | +| W2 の線(CR1) | 「自動継続より `LATE` 以上前の記録」に狭める | 利用者の決定(2026-10-27)。`LATE` は「動き出してから発火するまで」の幅として読む | +| #455 の残課題のうち失敗を示せないもの | 扱わない | 利用者の決定(2026-10-27) | + +### 1 パス目の指摘の採否 + +| 指摘 | 実物で失敗を示せるか | 破る受入基準 | 採否 | +| --- | --- | --- | --- | +| CR1: W2 の「予告の時刻より後に記録がある」は、遅れた自動継続の数秒前の記録(スリープ明けに溜まった通知)でも外す | 示せる(scratchpad の `repro.py`: 12:00:00 の通知 → 12:00:05 の自動継続が「続かなかった」、通知なしの対照は「自動継続」) | W2 の文言どおりの挙動なので W1〜W10 は破らない。**W2 の決定の理由(「動いていたのに発火しなかった」)が偽になる** | **直す**——利用者が W2 を狭めた(`AskUserQuestion`、2026-10-27) | +| CR2: 遅い自動継続を外したあとも窓が閉じず、その後の人間の入力が元の待ちの行方を決める | 示せる(`repro.py`: 外した 20:00 の自動継続の後の 21:00 の人間が「予告の後に人間が先」+660 分) | W2(外した自動継続は「記録の無い別の停止のもの」なので、その後はその停止の窓) | 直す | +| CR3: 窓の長さが同じコピーは遅いほうの時刻が勝つ。理由が書いていない | 示せない(読む順に依らないことは W5 のとおり成り立つ) | 無し | 残課題 | +| CR4: オフセットの無い時刻で `TypeError` | 示せない(実データに無い。#455 からの既存の比較も同じ) | 無し | 残課題 | +| CR5: 取り消しや system だけの後の別の停止が前の待ちに畳まれる | 示せない(実データで未確認。既存) | 無し | 残課題 | +| CR6: 壊れたレコード(`message` が文字列など)で落ちる | 示せない(実データに無い。既存) | 無し | 残課題 | +| CR7: W9 の「変わらない」は判別しない | 計画に既に書いてある(陽性対照は合成の試料) | 無し | 残課題(記録どおり) | +| CR8: `--since` を文字列で比べ、月を UTC で切る | 示せない(既存) | 無し | 残課題 | +| V1: `t > reset` の境界(ちょうど予告の時刻の記録)を試料で当てていない | 示せない | 無し | 残課題 | +| V2: `check-all.py` で `check-mutation-claims.py` が 1 回落ちた(単独で再実行すると通った) | 示せない(親が指摘対応の後に回したら 51/51 緑・失敗 0) | W10 | 直さない(再現せず) | + +### 2 パス目の指摘の採否 + +再現は scratchpad の `edge.py`・`sqprobe.py`・`mutprobe.py`(レビューが作り、親が走らせ直して同じ出力を見た)。 + +| 指摘 | 実物で失敗を示せるか | 破る受入基準 | 採否 | +| --- | --- | --- | --- | +| CR2-1: 試料 `sn` の名前つきの検査は W1 の変異で赤にならない(集計の検査だけが落とす) | 示せる(`mutprobe.py`: 落ちるのは orphans と月の行の 2 件) | 無し(変異は殺されている。W8 は数) | 残課題 | +| CR2-2: コードは `t < at - LATE`(`LATE` より前)、docstring と W2 は「`LATE` 以上前」 | 示せる(`edge.py` P1: ちょうど `LATE` 前の応答 → 外さない) | W2 | 直す——コードを `<=` にし、境界の試料と変異を足す | +| CR2-3: 「守る」の「何も記録されていない待ちでは、遅い自動継続もその待ちのもの」が「誰も続けていない」を落とし、W1 と食い違う | 示せる(`edge.py` P4: 予告前の人間 → 遅い自動継続は外れる。試料 `sp` も同じ) | W1・W3(docstring が守る線を偽に書いている) | 直す(文言を戻す) | +| CR2-4: docstring の「停止の記録を持たない自動継続」の定義に、閉じた窓の後の自動継続が無い | 示せる(`edge.py` P3: 15:10 の自動継続も外れる) | W2(窓を閉じることの帰結を docstring が言っていない) | 直す(定義に足す) | +| CR2-5: 起きてからの turn が `LATE` より長いと、スリープで遅れた自動継続が外れる | 示せる(`edge.py` P5) | 無し(W2 の文言どおり)。**W2 の決着の理由(「`LATE` は動き出してから発火するまで」)が成り立たない形**——利用者の決定に関わる | 残課題(PR コメントで人間に渡す) | +| CR2-6: W5 の検査が `waits["sq"]` を名前で引くので、変異で `KeyError` になり名前つきの FAIL が出ない | 示せる(`sqprobe.py`) | 無し(変異は全体の比較で殺される) | 残課題 | +| CR2-7: 変異の名前が古い文言のまま・`and False` の変異が全部の除外を止める・1 対 1 は数だけ | 示せる(`mutprobe.py`: 7 件落ちる) | 無し(W8 は数) | 残課題 | + +### 狭いパスの指摘の採否 + +| 指摘 | 実物で失敗を示せるか | 破る受入基準 | 採否 | +| --- | --- | --- | --- | +| NP1: W2 の下端(`reset_of(stop) < t`)を外す変異がテストを素通りする——予告より前の記録で外れる形をどの試料も当てていない | 示せる(陽性対照の試料 `sv`: 予告前 09:30 の応答・10:50 の自動継続。正しいコードで ok、変異で FAIL) | 無し(コードは W2 を守っている。検査の穴) | 人間が決める | +| NP2: W1 の境界(`at > reset + LATE` を `>=`)の変異が素通りする——ちょうど `LATE` 後の自動継続を当てていない | 示せる(陽性対照の試料 `sw`) | 無し(同上) | 人間が決める | diff --git a/scripts/count-usage-limit-resumes.py b/scripts/count-usage-limit-resumes.py index ef64fa9b..4ec69640 100644 --- a/scripts/count-usage-limit-resumes.py +++ b/scripts/count-usage-limit-resumes.py @@ -26,9 +26,14 @@ - **予告の時刻が同じ停止**(2 分の幅)——待ちの間に通知が来て応答し、また上限に当たる形が多い。 **`resetsAt` は `resetsAt` と、表示の時刻は表示の時刻と比べる**(表示の時刻は `--tz` に依る) - **窓**——待ちの最初の停止から、**同じファイルの、予告の時刻が違う次の停止まで**(無ければ記録の終わりまで)。 - **ただし人間の入力かローカルコマンドが窓に来たあと、予告の時刻から `LATE`(30 分)より後に来た自動継続は、 - 窓に入れない**——自動継続は予告の時刻に発火する(#454 の周の実データで +1 分)ので、人間が続けたあとの遅いものは - 記録の無い別の停止のものと読む。**誰も続けていなければ遅くても窓に入れる**(スリープで遅れることがある)。 + **ただし予告の時刻から `LATE`(30 分)より後に来た自動継続は、次のどちらかなら窓に入れない**——自動継続は予告の時刻に + 発火する(#454 の周の実データで +1 分)ので、遅いものは記録の無い別の停止のものと読む: + - 窓に**既に続けたもの**(自動継続・人間の入力・ローカルコマンド)が来ている + - **予告の時刻より後、その自動継続より `LATE` 以上前に入力か応答が記録されている**(セッションは `LATE` 以上動いていたのに、 + その間に発火しなかった)。**直前の記録だけでは外さない**——スリープ明けに、溜まった通知と一緒に来ることがある + + **どちらでもなければ遅くても窓に入れる**——スリープで遅れることがある。**外した自動継続で窓を閉じる**—— + その後の入力は記録の無い別の停止のもので、この待ちの行方に数えない。 - **最初に来たもの**——窓の中で最初の入力か応答(#454 の Issue 本文の表と同じ切り口): | 分類 | 何で決めるか | @@ -55,8 +60,8 @@ - **予告の時刻**——**`resetsAt` を優先し**、無ければ予告の表示(`at 6:40pm`)を `--tz` の時間帯で読む (**予告の時刻以降で最初のその時刻**。日付をまたぐ)。差は、行方を決めたものの時刻から引く。 -- **停止の記録を持たない自動継続**——窓の外に来た自動継続(ファイルの最初の停止より前か、人間が続けたあとの - 予告の時刻から `LATE` より後)。**落とさずに別の列で数える。** 同じ `uuid` がどれかの待ちの窓に入っていれば数えない(コピー)。 +- **停止の記録を持たない自動継続**——窓の外に来た自動継続(ファイルの最初の停止より前か、上の 2 つのどちらかに当たる + 予告の時刻から `LATE` より後のもの。**外した自動継続で窓を閉じるので、そこから次の停止までに来た自動継続も**)。**落とさずに別の列で数える。** 同じ `uuid` がどれかの待ちの窓に入っていれば数えない(コピー)。 ## 0 件を「止まらなかった」と読まないために @@ -77,16 +82,23 @@ 待ちの最初のレコードの `uuid` で畳む。 - **コピーの窓が短くても、長いほうを使う**——どちらのファイルを先に読むかで行方が変わらない。 - **窓の長さが同じコピーは、窓の中身で決める**——fork は停止の直後から分かれる。読む順では決めない。 +- **窓の中身は種類だけでなく時刻まで比べる**——自動継続の時刻だけが違うコピーでも、読む順で差が変わらない。 - **同じ自動継続を、待ちの中と停止の記録を持たない側の両方に数えない**——コピーにその記録だけが写ることがある。 - **続けて並んだ停止の記録を 1 つの待ちに畳む**——予告の時刻が取れない停止でも。 - **予告の時刻が同じ停止を 1 つの待ちに畳む**——間に通知と応答が挟まっても。 -- **畳むときは `resetsAt` 同士・表示の時刻同士で比べる**——`--tz` が違っても同じ待ちを割らない。 +- **畳むときは `resetsAt` 同士・表示の時刻同士で比べる**——比べる相手のある組は、`--tz` が違っても割らない。 - **畳んだあとに並んだ停止の記録も、続けて並んだものと読む**——`rate_limit` と予告の組が畳まれても割らない。 - **予告だけの停止を落とさない**——`rate_limit` のレコードを持たない停止がある。 - **最初に来たものの後の自動継続を拾う**——窓は次の待ちまで。 - **停止の記録を持たない自動継続を落とさない**——別の列で数える。 - **人間が続けたあと、予告の時刻から `LATE` より後の自動継続を、その待ちのものと読まない**——停止の記録を持たない側に数える。 -- **誰も続けていない待ちでは、遅い自動継続もその待ちのもの**——スリープで遅れた自動継続を「続かなかった」にしない。 +- **ローカルコマンドが続けたあとも同じ**——人間の入力と同じに扱う。 +- **自動継続が既に来た待ちの、遅い自動継続も同じ**——2 つ目は記録の無い別の停止のもの。 +- **予告の時刻より後、自動継続より `LATE` 以上前に入力か応答が記録された待ちの、遅い自動継続も同じ**——通知や応答だけの待ちでも。 +- **ちょうど `LATE` 前の記録でも外す**——「`LATE` 以上前」の境界を含む。 +- **遅れた自動継続の直前の記録だけでは外さない**——スリープ明けに溜まった通知と一緒に来ることがある。 +- **外した自動継続のあとの入力を、その待ちの行方に数えない**——窓はそこで閉じる。 +- **誰も続けておらず、予告の時刻のあと自動継続の `LATE` 前まで何も記録されていない待ちでは、遅い自動継続もその待ちのもの**——スリープで遅れた自動継続を「続かなかった」にしない。 - **予告の時刻は `resetsAt` を先に使う**——表示の時刻は時間帯に依る。 - **表示の時刻を `--tz` の時間帯で読む**——UTC で読むと、時間帯の差だけ予告の時刻がずれる。 - **表示の時刻が日付をまたぐ**——予告より前の時刻なら翌日。 @@ -108,6 +120,8 @@ | **タイマーが張られていたか** | タイマーが打った入力は `origin.kind` の名前で出るが、**その名前を確かめた実物は無い**(手元に 0 件) | | **`origin` を持たない版で文言が同じであること** | 手元の自動継続はすべて `origin` を持つ(#454 の周) | | **表示の時刻の時間帯** | 表示は記録した端末の時間帯。**`--tz` が違えば差と「予告より前/後に人間」の分け方がずれ、人間が続けたあとの自動継続を別の停止のものと読むことがある**(`resetsAt` がある待ちには効かない)。**既定は実行した時点の端末のオフセット**で、夏時間をまたぐ記録は 1 時間ずれうる | +| **比べる相手の無い組の畳み** | 予告だけの停止と `rate_limit` だけの停止は、表示の時刻と `resetsAt` を互いに比べるしかなく、**`--tz` が違えば同じ待ちを割る**(記録した端末の時間帯は記録に無い) | +| **予告の後、自動継続の `LATE` 前まで何も記録されていない遅い自動継続が、本当にスリープで遅れたものか** | 記録の無い別の停止のものと区別できない。その待ちのものと読む | | **別のセッションの待ち** | 同じ上限に複数のセッションが当たると、それぞれ 1 つの待ちに数える(上限の回数ではない) | ## 出力について @@ -297,15 +311,17 @@ def scan_file(path: pathlib.Path, stats: collections.Counter, tz): else: continue burst = False - # 人間かローカルコマンドが続けたあと、予告の時刻から LATE より後に来た自動継続は、この待ちのものではない - # (記録の無い別の停止のもの)。**誰も続けていなければ外さない**——スリープで遅れた自動継続がある + # 予告の時刻から LATE より後に来た自動継続は、既に続けたものがあるか、予告の時刻より後でその LATE 以上前に記録があれば、 + # この待ちのものではない(記録の無い別の停止のもの)。**どちらも無ければ外さない**——スリープで遅れることがある late = (label == AUTO and stop is not None and at is not None and reset_of(stop) is not None and at > reset_of(stop) + LATE - and any(seen in (HUMAN, LOCAL) for seen, _ in stop["inputs"])) + and (any(seen in (AUTO, HUMAN, LOCAL) for seen, _ in stop["inputs"]) + or any(t is not None and reset_of(stop) < t <= at - LATE for _, t in stop["inputs"]))) if stop is None or late: if label == AUTO: orphans.append({"id": uuid, "session": session, "ts": rec.get("timestamp") or "", "repo": repo_of(rec.get("cwd"))}) + stop = None # 外した自動継続で窓を閉じる——後の入力は記録の無い別の停止のもの continue stop["inputs"].append((label, at)) if label == AUTO: diff --git a/scripts/test-count-usage-limit-resumes.py b/scripts/test-count-usage-limit-resumes.py index 979bb043..465b838d 100644 --- a/scripts/test-count-usage-limit-resumes.py +++ b/scripts/test-count-usage-limit-resumes.py @@ -181,6 +181,41 @@ def build_tree(root: Path) -> None: fork = Session("smfork") fork.rows = [dict(sm.rows[0]), dict(sm.rows[1])] fork.user("2026-09-26T09:30:00", "継続", kind="human").write(root, project="-aac") + # sn: 予告より前の自動継続(早く使えるようになったとき)→ 応答 → 予告の 5 時間後に自動継続(その間の停止の記録が無い)。 + # 予告の後には何も記録が無いので、外す理由は「既に自動継続が来た」だけ + (Session("sn").limit("2026-09-27T09:00:00", "2026-09-27T10:00:00").armed("2026-09-27T09:00:01", "10am") + .user("2026-09-27T09:50:00", EARLY_TEXT, kind="auto-continuation", meta=True).reply("2026-09-27T09:50:30") + .user("2026-09-27T15:00:00", AUTO_TEXT, kind="auto-continuation", meta=True).write(root)) + # so: 予告の後に通知と応答だけ → 予告の 10 時間後に自動継続 + (Session("so").limit("2026-09-28T09:00:00", "2026-09-28T10:00:00").armed("2026-09-28T09:00:01", "10am") + .user("2026-09-28T10:10:00", "x", kind="task-notification") + .reply("2026-09-28T10:10:30") + .user("2026-09-28T20:00:00", AUTO_TEXT, kind="auto-continuation", meta=True).write(root)) + # sp: 予告より前にローカルコマンド → 予告の 10 時間後に自動継続 + (Session("sp").limit("2026-09-29T09:00:00", "2026-09-29T10:00:00").armed("2026-09-29T09:00:01", "10am") + .user("2026-09-29T09:10:00", "/usage", kind="human") + .user("2026-09-29T20:00:00", AUTO_TEXT, kind="auto-continuation", meta=True).write(root)) + # sq: 自動継続の時刻だけが違う 2 つのコピー(窓の長さも種類も同じ)。早いほう(10:01)が先に読まれる(`-aad` < `-work-app`) + sq = (Session("sq").limit("2026-09-30T09:00:00", "2026-09-30T10:00:00").armed("2026-09-30T09:00:01", "10am") + .user("2026-09-30T10:05:00", AUTO_TEXT, kind="auto-continuation", meta=True)) + sq.write(root) + late_copy = Session("sqcopy") + late_copy.rows = [dict(sq.rows[0]), dict(sq.rows[1])] + late_copy.user("2026-09-30T10:01:00", AUTO_TEXT, kind="auto-continuation", meta=True).write(root, project="-aad") + # sr: スリープ明けに溜まった通知 → 5 秒後に遅れた自動継続(予告の 40 分後)——直前の記録だけでは外さない + (Session("sr").limit("2026-10-01T09:00:00", "2026-10-01T10:00:00").armed("2026-10-01T09:00:01", "10am") + .user("2026-10-01T10:40:00", "x", kind="task-notification") + .user("2026-10-01T10:40:05", AUTO_TEXT, kind="auto-continuation", meta=True).write(root)) + # ss: 予告の後に通知と応答 → 予告の 10 時間後の自動継続(外れる)→ 応答 → 人間——人間はこの待ちの行方ではない + (Session("ss").limit("2026-10-02T09:00:00", "2026-10-02T10:00:00").armed("2026-10-02T09:00:01", "10am") + .user("2026-10-02T10:10:00", "x", kind="task-notification") + .reply("2026-10-02T10:10:30") + .user("2026-10-02T20:00:00", AUTO_TEXT, kind="auto-continuation", meta=True).reply("2026-10-02T20:00:30") + .user("2026-10-02T21:00:00", "つづき", kind="human").write(root)) + # su: 予告の 15 分後の応答 → ちょうど LATE 後(予告の 45 分後)の自動継続——「LATE 以上前」なので外れる + (Session("su").limit("2026-10-03T09:00:00", "2026-10-03T10:00:00").armed("2026-10-03T09:00:01", "10am") + .reply("2026-10-03T10:15:00") + .user("2026-10-03T10:45:00", AUTO_TEXT, kind="auto-continuation", meta=True).write(root)) def build_blind(root: Path) -> None: @@ -247,7 +282,21 @@ def observe(mod, root: Path, blind: Path) -> dict: "同じ自動継続を待ちと停止の記録を持たない側の両方に数える": ('o["id"] not in windowed and ', ""), "人間が続けたあとの遅い自動継続も待ちに入れる": ("and at > reset_of(stop) + LATE\n", "and False\n"), "誰も続けていない待ちでも、遅い自動継続を外す": - ("\n and any(seen in (HUMAN, LOCAL) for seen, _ in stop[\"inputs\"]))", ")"), + ("\n and (any(seen in (AUTO, HUMAN, LOCAL) for seen, _ in stop[\"inputs\"])\n" + " or any(t is not None and reset_of(stop) < t <= at - LATE for _, t in stop[\"inputs\"])))", + ")"), + "ローカルコマンドが続けたあとの遅い自動継続を待ちに入れる": + ("any(seen in (AUTO, HUMAN, LOCAL) for", "any(seen in (AUTO, HUMAN) for"), + "自動継続が既に来た待ちの遅い自動継続を待ちに入れる": + ("any(seen in (AUTO, HUMAN, LOCAL) for", "any(seen in (HUMAN, LOCAL) for"), + "予告の時刻より後の記録を見ない": + ("\n or any(t is not None and reset_of(stop) < t <= at - LATE for _, t in stop[\"inputs\"])", ""), + "ちょうど LATE 前の記録では外さない": ("reset_of(stop) < t <= at - LATE", "reset_of(stop) < t < at - LATE"), + "遅れた自動継続の直前の記録でも外す": ("reset_of(stop) < t <= at - LATE", "t > reset_of(stop)"), + "外した自動継続で窓を閉じない": + (" stop = None # 外した自動継続で窓を閉じる——後の入力は記録の無い別の停止のもの\n", ""), + "窓の中身を種類だけで比べる": + ("[(label, iso_or_empty(at)) for label, at in stop[\"inputs\"]]", "[label for label, at in stop[\"inputs\"]]"), "窓の長さが同じコピーは、先に読んだほうが勝つ": ('return len(stop["inputs"]), [(label, iso_or_empty(at)) for label, at in stop["inputs"]]', 'return len(stop["inputs"])'), @@ -270,8 +319,8 @@ def main() -> int: waits.setdefault(w[0], []).append(w) print("待ちと行方") - check("待ちは 14(コピーは畳む・同じ予告の時刻の停止は 1 つ)", - len(correct["waits"]) == 14 and correct["dups"] == 2 and len(waits["sa"]) == 1) + check("待ちは 21(コピーは畳む・同じ予告の時刻の停止は 1 つ)", + len(correct["waits"]) == 21 and correct["dups"] == 3 and len(waits["sa"]) == 1) sa = waits["sa"][0] check("先に読んだコピーの窓が短くても、長いほうの窓で行方を決める", sa[7] == "自動継続") check("通知が先でも、その後の自動継続を拾う(予告の 1 分後)", @@ -283,8 +332,8 @@ def main() -> int: check("origin の無い旧い形は、早く使えるようになったときの文言でも自動継続", sc1[6:8] == ("自動継続", "自動継続")) check("origin が human でもスラッシュコマンドはローカルコマンド", sc2[6] == "ローカルコマンド") check("予告だけの停止も待ちで、その後の自動継続を拾う", waits["sd"][0][7] == "自動継続" and waits["sd"][0][2]) - check("停止の記録を持たない自動継続は別に数える(予告の 10 時間後のものも。コピーの自動継続は数えない)", - correct["orphans"] == 2 and "se" not in waits) + check("停止の記録を持たない自動継続は別に数える(予告の 5〜10 時間後のものも。コピーの自動継続は数えない)", + correct["orphans"] == 7 and "se" not in waits) si = waits["si"][0] check("予告の時刻から遅い自動継続は、その待ちのものと読まない", si[7:] == ("予告より前に人間", None)) sf = waits["sf"] @@ -306,13 +355,29 @@ def main() -> int: check("窓の長さが同じコピーは、読む順に依らず窓の中身で決める", len(waits["sm"]) == 1 and waits["sm"][0][7] == "自動継続") + sn = waits["sn"][0] + check("自動継続が既に来た待ちの、予告の 5 時間後の自動継続は別に数える", + sn[7:] == ("自動継続", "2026-09-27T09:50:00+00:00")) + check("予告の後に通知と応答だけ来た待ちの、予告の 10 時間後の自動継続は、その待ちのものと読まない", + waits["so"][0][7:] == ("続かなかった", None)) + check("ローカルコマンドが続けた待ちの、予告の 10 時間後の自動継続は、その待ちのものと読まない", + waits["sp"][0][7:] == ("予告より前に人間", None)) + check("自動継続の時刻だけが違うコピーは、読む順に依らず同じほう(遅いほう)を使う", + len(waits["sq"]) == 1 and waits["sq"][0][8] == "2026-09-30T10:05:00+00:00") + check("遅れた自動継続の直前(5 秒前)の通知だけでは外さない", + waits["sr"][0][7:] == ("自動継続", "2026-10-01T10:40:05+00:00")) + check("外した自動継続のあとの人間の入力は、その待ちの行方にしない", + waits["ss"][0][7:] == ("続かなかった", None)) + check("ちょうど LATE 前の応答でも、遅い自動継続を外す", + waits["su"][0][7:] == ("続かなかった", None)) + print("\n出力") status, text = correct["plain"] check("停止があれば 0 で終わる", status == 0) - check("月ごとの行方(張った 13・自動継続 8・前 3・後 1・取り消し 1・続かなかった 0)", - "| 2026-09 | 14 | 13 | 8 | 3 | 1 | 1 | 0 | 0 | 2 |" in text) + check("月ごとの行方(張った 17・自動継続 10・前 4・後 1・取り消し 1・続かなかった 1)", + "| 2026-09 | 18 | 17 | 10 | 4 | 1 | 1 | 1 | 0 | 5 |" in text) check("張らなかった待ちの上限の種類を出す", "seven_day 1" in text) - check("自動継続の差(分)を出す", "最小 -10・中央 +1・最大 +45(8 件)" in text) + check("自動継続の差(分)を出す", "最小 -10・中央 +1・最大 +45(11 件)" in text) check("本文を出さない", "task-notification>x" not in text and "Continue the task" not in text and "/usage" not in text) astatus, atext = correct["anon"] check("--anonymize はリポジトリ名とセッションを伏せる", @@ -321,7 +386,7 @@ def main() -> int: bstatus, btext = correct["blind"] check("停止が 0 件なら終了コード 3", bstatus == 3 and "検出できていない" in btext) sstatus, stext = correct["since"] - check("--since より前の停止を数えない(sh〜sm の 6 件)", sstatus == 0 and "待ち: 6・" in stext) + check("--since より前の停止を数えない(sh〜su の 13 件)", sstatus == 0 and "待ち: 13・" in stext) fstatus, ftext = correct["future"] check("--since を狭くして 0 件なら終了コード 3", fstatus == 3 and "検出できていない" in ftext)