Compare commits
508 Commits
365b8c34fd
...
abe21a5e8a
| Author | SHA1 | Date | |
|---|---|---|---|
| abe21a5e8a | |||
| 9707a0173a | |||
| e95c35b76d | |||
| 28ad8f01ec | |||
| 0a07c50be4 | |||
| 5872a53ec1 | |||
| 5b3b16c4b2 | |||
| 46765404bf | |||
| 3d3db8b6ac | |||
| 3f750668ba | |||
| fe9b12aa65 | |||
| a414655366 | |||
| 0b6f09c112 | |||
| e5b918283a | |||
| 8dc23183c1 | |||
| 2bd73fdca8 | |||
| 6dc696a461 | |||
| 7c14b51bac | |||
| 8653fdd3e5 | |||
| ef9c23251e | |||
| 9172ad3af1 | |||
| dae7d10fd4 | |||
| 49abacf694 | |||
| 7054a179d4 | |||
| 2109733cb7 | |||
| ebff9a0293 | |||
| 8daa0f1a01 | |||
| 1f8afd1243 | |||
| f6024c0c2c | |||
| 0fd0a89730 | |||
| 2b547d6dd9 | |||
| 081070f03e | |||
| 614d461877 | |||
| f1bd498df7 | |||
| 6e4512afeb | |||
| 918ed3900a | |||
| ff747da1a0 | |||
| c8810280af | |||
| 82775bf9d3 | |||
| 0775b4112b | |||
| e79e7362f8 | |||
| e078be443a | |||
| 3db9726062 | |||
| b9498810a4 | |||
| fbe8e64410 | |||
| de653f546a | |||
| 63407f153c | |||
| ee9bedc5de | |||
| da7c1a54a1 | |||
| 9f7c2f3856 | |||
| 3f7de349c3 | |||
| 1feb560ff9 | |||
| 902083bd38 | |||
| 221b1edd92 | |||
| 828004a5e2 | |||
| b26bc420f1 | |||
| fb7abb1b7c | |||
| 66996f902b | |||
| d62cd09c4d | |||
| a4f03e7688 | |||
| 5ade50dec5 | |||
| dbb6cca894 | |||
| 7c9abb37ad | |||
| 0535260c8a | |||
| c435635e5b | |||
| c78cd28b27 | |||
| abe890cda5 | |||
| fa04b03643 | |||
| 1bebd8b6df | |||
| 4dec7f916f | |||
| 8662ca404f | |||
| 5f2efeb75e | |||
| 820dea1873 | |||
| 8d5ee0f0b8 | |||
| 0d39170bbe | |||
| 16963d15f2 | |||
| 2cea02648f | |||
| f55503edc3 | |||
| d18e3a0256 | |||
| 5ba4be1c9b | |||
| 90d4c8f5ad | |||
| 61c9719da5 | |||
| 73fbdcc025 | |||
| 5fdc46db47 | |||
| 534c6f4cac | |||
| 5540ca3d0e | |||
| edfdca3457 | |||
| bd32f704b4 | |||
| e55fc9a834 | |||
| 7f6e3b949f | |||
| 0954a4804a | |||
| e9cc4b90dc | |||
| 8aca67c97c | |||
| d7eabb18c9 | |||
| b13c2735bb | |||
| 67f135fbc6 | |||
| a728b7045d | |||
| 3eabcb6a6d | |||
| ffcba3aa54 | |||
| eb752fb295 | |||
| ac3ee5fcbe | |||
| 46b0e20685 | |||
| 6a4ee37be8 | |||
| 3f3ead3b71 | |||
| a6cc05f74c | |||
| e4cda5d3f2 | |||
| dd9abfee2e | |||
| d7ff25b6a7 | |||
| 7577577c9f | |||
| 0d7c37f673 | |||
| d5c7330659 | |||
| 9c1f51b4f0 | |||
| 1d8a490504 | |||
| 6e791d8668 | |||
| d5dff6d17b | |||
| 35c15923db | |||
| be5e413b55 | |||
| 58c2a51ae1 | |||
| e00e284d8c | |||
| e5f5670f68 | |||
| a2376b0742 | |||
| fbd7d8acb7 | |||
| 282a857248 | |||
| 9304b52f17 | |||
| d0dbac109d | |||
| d710cac879 | |||
| bca9161a42 | |||
| 61b4c0f5cd | |||
| d076258d30 | |||
| b5069a9f82 | |||
| da417efddd | |||
| 21053f7d01 | |||
| 456af3167b | |||
| 8019d0d77c | |||
| 6e5b1482e6 | |||
| 7520dcad87 | |||
| 112ccb2365 | |||
| fe9cecb51a | |||
| 65a5e68035 | |||
| 63e27b2dee | |||
| 350bb1afd8 | |||
| 0f76142993 | |||
| 7c66b7e073 | |||
| 69d67ab050 | |||
| 877e094a53 | |||
| 5a16cc6daf | |||
| b0c91049b1 | |||
| 418451ebf8 | |||
| 3d0dce2a2e | |||
| 4bde31e952 | |||
| 533610f053 | |||
| 8e50a9583a | |||
| cb24586362 | |||
| e32b208dee | |||
| 7363b105f6 | |||
| 0ebe173009 | |||
| 023de0f58d | |||
| eae0efb2c0 | |||
| 0141880b9d | |||
| 2b5da965ca | |||
| ba72a66a40 | |||
| 671e07a31e | |||
| 7ce4600a42 | |||
| 2f70411254 | |||
| 7c5b810fa1 | |||
| 86fc889606 | |||
| 59bf20f2cd | |||
| f7f59dd30c | |||
| 806440ac7a | |||
| 7b8eb3af8d | |||
| 705c873097 | |||
| ae6c27a5c7 | |||
| 03a577527a | |||
| b4dff2835e | |||
| e87a515474 | |||
| 9df6bd5fcb | |||
| 6610ef8150 | |||
| 7159a66a60 | |||
| 7db4146f3d | |||
| d8f29bcbcb | |||
| f444b387be | |||
| d18f536945 | |||
| 19badfe8b7 | |||
| 31f94bf791 | |||
| ac09bfcc21 | |||
| 76f83a0894 | |||
| d581a35426 | |||
| f69aa469f8 | |||
| 3d4d83db68 | |||
| 9abddb5a95 | |||
| 075730d0a6 | |||
| aae36f2b56 | |||
| e418f3996f | |||
| 240b36d738 | |||
| 0356e29707 | |||
| eec33aba98 | |||
| 248e3d7aa2 | |||
| 3beaff7679 | |||
| ef0cdf75e2 | |||
| 5976aac78d | |||
| d818b37f3d | |||
| f8bae4a298 | |||
| 3abf4d4d1a | |||
| e2a6c43fea | |||
| df01d8e567 | |||
| e647d1a7c9 | |||
| 29f45bee6e | |||
| 576814ed20 | |||
| 5590fc4ff1 | |||
| 25d22fc4af | |||
| 9194b10d50 | |||
| 99bc161e43 | |||
| 37dfd7e327 | |||
| 7bbc9afc7a | |||
| 3337731222 | |||
| 16bd8e3a88 | |||
| ed08ee1ce1 | |||
| 12e7d27a08 | |||
| f09e6a0156 | |||
| 8c12e799da | |||
| 04f1837fa9 | |||
| 5ec24707f4 | |||
| c0c5eb9ad2 | |||
| e9e80c5918 | |||
| f4ab361889 | |||
| 60c779b80c | |||
| 50fa2ce3f7 | |||
| 760b304969 | |||
| 5fe9a5805e | |||
| 64b5d61a23 | |||
| 461d7f9142 | |||
| 6f62ea8ce8 | |||
| 9fd61e8068 | |||
| dd3903efde | |||
| 089db05535 | |||
| 0a83909f30 | |||
| 9072ac4e03 | |||
| 6178812979 | |||
| d385a72d85 | |||
| c48b99cfe3 | |||
| 632d63df33 | |||
| 107dcf6636 | |||
| 7f9d2f93f9 | |||
| 9771533b31 | |||
| 36a4e9f9b8 | |||
| 0be30052c1 | |||
| 72e03f9e8f | |||
| 09a1cde92c | |||
| 7183847ee5 | |||
| 1451998e0e | |||
| 5bc6fb4b5c | |||
| ac1a672973 | |||
| 4ec1c8b64c | |||
| 771503e69c | |||
| 89a66f1d58 | |||
| 8be579dc3c | |||
| ffd59b05a1 | |||
| d2f2b7920d | |||
| 357fedc1a1 | |||
| a07ccb0158 | |||
| 9cbcd87f20 | |||
| 1602eea2c8 | |||
| 878e64597e | |||
| 46208a3b45 | |||
| c214ea79d4 | |||
| c693126703 | |||
| 46390a9006 | |||
| 420f74edc6 | |||
| ceafff92b6 | |||
| e8045776f2 | |||
| 6f2aca84bf | |||
| 28fe1dae1c | |||
| ee9c60bec2 | |||
| 7e0d61eb08 | |||
| 4328cb334a | |||
| 02dcf182a7 | |||
| 8ff6318cef | |||
| c759307e40 | |||
| 0c10150b02 | |||
| b9635c5002 | |||
| 9010a920a4 | |||
| 44bc35bd31 | |||
| f8a3a7838b | |||
| 8b5f75ecc4 | |||
| be22c65af3 | |||
| d9f55185f0 | |||
| 4b9b4f1450 | |||
| 670abdc336 | |||
| 2d5c6aad5f | |||
| f16ccc0a09 | |||
| 6ebd10a006 | |||
| d8d802d120 | |||
| 288e2239d4 | |||
| 8194bb10f4 | |||
| a9ad0c2e0d | |||
| 8ed739261f | |||
| 5a29c90786 | |||
| 0d66b397af | |||
| fa84d48c62 | |||
| 433ee0f37c | |||
| 00755cf1b8 | |||
| 31d9b9b2b7 | |||
| d18de45293 | |||
| 2790a35acf | |||
| 776a6a29bd | |||
| 0046f1efc9 | |||
| 1e060914ce | |||
| 2159711cd0 | |||
| 1afe7c53aa | |||
| 25b016f3da | |||
| beb6b686a1 | |||
| 0070aabd26 | |||
| 3e2c9ee32b | |||
| 97f9b14ceb | |||
| 6e9ef385c8 | |||
| c331936455 | |||
| 75c61bd3cb | |||
| 6788db1ef2 | |||
| 4b09ff0234 | |||
| 9177ee8ef3 | |||
| d2ee84775b | |||
| cb1d3e72e4 | |||
| 1b1f8f40c6 | |||
| c70b0bdc5d | |||
| 010edf2c94 | |||
| 1ab6dbcee3 | |||
| bd8154204a | |||
| 7c2ef374e6 | |||
| 119a73c112 | |||
| 862c38d7f7 | |||
| 0ad3923932 | |||
| 588c25a570 | |||
| 043c2e862c | |||
| 27e5074450 | |||
| 5fa060a748 | |||
| 71434b9d8b | |||
| 8a1baa5020 | |||
| 6fa7f169b4 | |||
| 44d660c894 | |||
| b9575f1534 | |||
| e98a596235 | |||
| 3c90729156 | |||
| 51309ec5bf | |||
| af57d5b566 | |||
| 0f6b724184 | |||
| d5d0e4124b | |||
| b192a3ce4e | |||
| 1466f11a0b | |||
| a79abb3c27 | |||
| e72aac8cf2 | |||
| 06c5ecfeb3 | |||
| 092386d98f | |||
| 03c4f49f73 | |||
| ed5c07c301 | |||
| 18dd98e05b | |||
| 46e1b92ade | |||
| 341bd71dc5 | |||
| 1e068f4beb | |||
| bc9f93ab09 | |||
| 7d0b639fa4 | |||
| 553b69b55a | |||
| 532218dd40 | |||
| f8948be43d | |||
| 34d5c3aab6 | |||
| 5c91535a74 | |||
| 915061fd49 | |||
| 29812a9262 | |||
| b62b5c47b2 | |||
| 7e921be43e | |||
| 2e562dc3e0 | |||
| 13974c66fb | |||
| 97446180cd | |||
| c73c870844 | |||
| 8ab1e5a858 | |||
| d000e777b7 | |||
| e296320c7a | |||
| db187813ff | |||
| 837e77449d | |||
| be88b4bae5 | |||
| e122f4aadb | |||
| ee6d2d2100 | |||
| de3e4f78ab | |||
| 7903259348 | |||
| 30508f7851 | |||
| 505d3a8341 | |||
| bd0e10653d | |||
| 79987dd754 | |||
| 539dc604e3 | |||
| a4bd599d0f | |||
| 553e281ba1 | |||
| 85de0aa2b1 | |||
| 4ae2a46ccb | |||
| c6c02f846f | |||
| 6c406cdfd5 | |||
| bb14109b4e | |||
| a39bce779c | |||
| 18533b3580 | |||
| 789348c252 | |||
| e0e6cc8616 | |||
| b8d5398520 | |||
| 94e40c8ee4 | |||
| 7698619f1b | |||
| 822f8d9ec2 | |||
| 25906220eb | |||
| 0668bd5213 | |||
| f4d21cd994 | |||
| 0ea1ca5ec7 | |||
| d9358047cf | |||
| 177ff80615 | |||
| bd8c5601c7 | |||
| 97326eef04 | |||
| bf072cc4f0 | |||
| b1e4572823 | |||
| 1a4863d211 | |||
| ebee0b95ef | |||
| 170e0c2099 | |||
| 79b0e3d51b | |||
| 0284b5a76f | |||
| 78cf4599a2 | |||
| 18b7556e0a | |||
| 5375698813 | |||
| 1a3e9030bd | |||
| fc634bcd87 | |||
| da021103e4 | |||
| db2dd8a3c0 | |||
| 83f68e35ad | |||
| f0a865552c | |||
| a88febc15e | |||
| 143715fb22 | |||
| 3cdd8323de | |||
| 7637f0e440 | |||
| cc7bb0b711 | |||
| 3e788da7a7 | |||
| 6434b068fe | |||
| 3cb9b251fe | |||
| 1668e981b4 | |||
| bb71439787 | |||
| 8087349474 | |||
| cdbad36a48 | |||
| 2a7ee256f5 | |||
| 8e43503bdb | |||
| 4f5f5bfe76 | |||
| 5786fedc1c | |||
| e9a464a23c | |||
| 9d038fc3b7 | |||
| cc8c4c8189 | |||
| aa8a1ee64b | |||
| ac5265be41 | |||
| 9e11cfac7e | |||
| 2052ac498c | |||
| e8a5fe557a | |||
| 5b25287471 | |||
| 62c5cb87dd | |||
| 6e10a722c3 | |||
| 7c59b8677b | |||
| 4b1a73d38f | |||
| 3a02358668 | |||
| fade875c6f | |||
| 61fabbc3b8 | |||
| 34ac754644 | |||
| 203f188dae | |||
| 57fb22ed94 | |||
| db02afb74f | |||
| f8eabd3ac8 | |||
| 9eef8117c8 | |||
| be96efb5ed | |||
| ff88fbc7e4 | |||
| 0a1d01d9b5 | |||
| a89c4487c9 | |||
| 13d83e0fd3 | |||
| d5e2c3819d | |||
| 101679dbb8 | |||
| 313b5158b8 | |||
| 6f2362ec77 | |||
| a947922192 | |||
| c850cdf2b6 | |||
| 029bb1fba7 | |||
| 9747bd6d34 | |||
| 48e62f65df | |||
| f2aaa3683f | |||
| 46526ed262 | |||
| be1119d859 | |||
| afabd3d7fd | |||
| 0c9551eef0 | |||
| 444c90d7e4 | |||
| 37e6301397 | |||
| 601d93f8d0 | |||
| 57a8ad6b97 | |||
| 1617a982e1 | |||
| 26f9294f42 | |||
| 982e0d2dbb | |||
| 61a977779e | |||
| e1cf8fad0f | |||
| 3151fc27ef | |||
| 748e858ec5 | |||
| b15a5ceffc | |||
| c936492a29 | |||
| 71b3d550a4 | |||
| 88aa2cf953 | |||
| 1b8e2173bb | |||
| 13ba83d45d | |||
| b3d3fd524a | |||
| 8aa058ef75 | |||
| 5af96aa191 | |||
| ee307e2926 | |||
| 8d18357460 | |||
| 1d92680e76 | |||
| cc1ceee81d |
20
.insomnia/knowledge/claude-code.md
Normal file
20
.insomnia/knowledge/claude-code.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
created_at: 2026-05-11T22:10:00Z
|
||||
updated_at: 2026-05-11T22:10:00Z
|
||||
kind: policy
|
||||
description: Claude Codeを用いてレビューやinsomniaだけではできないタスクを行う
|
||||
model_invokation: false
|
||||
user_invocable: true
|
||||
last_sources: []
|
||||
---
|
||||
|
||||
Bashツールを用いて`claude`を呼び出す。
|
||||
|
||||
`claude -p "<prompt>"`で非対話モードでのClaude Codeの利用が出来る。
|
||||
|
||||
また、`claude -p "<prompt>" --continue`を用いることで、直前のセッションを再開する形で実行できる。
|
||||
|
||||
|
||||
insomniaではまだできないのでclaudeにやらせたいタスク
|
||||
- WebSearch / WebFetch
|
||||
-
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
[scope]
|
||||
allow = [
|
||||
{ target = ".", permission = "write", recursive = true },
|
||||
{ target = "/home/hare/ghq", permission = "read", recursive = true },
|
||||
]
|
||||
|
||||
[session]
|
||||
record_event_trace = true
|
||||
|
||||
[memory]
|
||||
extract_threshold = 50000
|
||||
|
||||
|
|
|
|||
|
|
@ -64,13 +64,13 @@ test ! -e .worktree/<task-name>/.insomnia
|
|||
|
||||
## 子 Pod へ渡す scope
|
||||
|
||||
子 Pod を使う場合、子 Pod の cwd は main workspace のままになる。必ず作業対象が child worktree であることを明示し、Bash 実行時は毎回 `cd <repo>/.worktree/<task-name> && ...` させる。
|
||||
子 Pod を使う場合、子 Pod の cwd は main workspace のままになる。必ず作業対象が child worktree であることを明示し、Bash 実行時は毎回 `cd /home/hare/Projects/insomnia/.worktree/<task-name> && ...` させる。
|
||||
|
||||
推奨 scope:
|
||||
|
||||
```text
|
||||
read: <repo>
|
||||
write: <repo>/.worktree/<task-name>
|
||||
read: /home/hare/Projects/insomnia
|
||||
write: /home/hare/Projects/insomnia/.worktree/<task-name>
|
||||
```
|
||||
|
||||
より狭く切れる場合は、write scope を変更対象 crate / directory まで狭めてよい。ただし build / test に必要な生成物を書けることを確認する。
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ workflowで明示されない限り、読み取り以外の操作は控えるこ
|
|||
基本はworktree上の一時的なブランチでコミットを重ね、メインブランチに取り込む運用をしている。
|
||||
コミットメッセージは適当に`<prefix>: *簡潔な1行*`で書いている。
|
||||
|
||||
外部の参考プロジェクトは必要に応じてローカルの外部 checkout からReadすること。
|
||||
外部の参考プロジェクトはghqでgetしており、必要に応じて`~/ghq`からReadすること。
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
75
CLAUDE.md
75
CLAUDE.md
|
|
@ -1,75 +0,0 @@
|
|||
全体設計が概ね固まり、随所の細かい仕様を詰めながら実装を進めている。
|
||||
|
||||
## このシステムに置ける設計要旨
|
||||
|
||||
- プロンプトはすべて resources/promptsに集約している。管理効率の工場と同時に、ユーザーがオーバーライドする形式でもある。
|
||||
- E2E(実プロセスをスポーンさせてのテスト)は未設計。
|
||||
- 変更量を最小にするために設計を歪めたり、設計問題に対して不必要な後方互換性を作らない。長期的なメンテナンスと型安全性を追求すること。
|
||||
|
||||
### LLM コンテキストの加工原則
|
||||
|
||||
LLM に投げる context への割り込みは、大きく2種類に分かれる。**前者は許されるが、後者は禁止**。
|
||||
|
||||
Podの状態から純粋に再現可能で、且つ揮発性の無い操作であることが望ましい。(pruning、tool result の content 切り詰め、prompt cache anchor の付与等)。
|
||||
原則として、コンテキストは積み重ねるものであり、一時的にメッセージを差し込むことや、過去のメッセージを改ざんすることはKVキャッシュのヒット率を下げる。
|
||||
|
||||
**禁止**: ターンを跨ぐことができない情報に基づいて、history に記録せずに context だけにコンテンツを差し込むこと。これをやると LLM はそれに反応して生成を行う一方、次以降のターンでhistoryに残らないため、「自分がなぜその発言/tool call をしたか」の根拠が消えるうえ、prompt cache のヒット率も低下させることになる。
|
||||
|
||||
新しい input を context に乗せたいなら、必ず先に `worker.history` に append して commit すること。`history.json` への永続化はそこから自動的についてくる。Notify / PodEvent / `<system-reminder>` 系はこの原則で扱う(→ `tickets/notify-history-persist.md`)。
|
||||
また、キャッシュを破壊するタイミングは正確にコントロールされる必要があり、キャッシュ破壊とトークン消費のトレードオフに基づいて慎重に設計されるべきである。
|
||||
|
||||
---
|
||||
|
||||
## 実際のセッションを読んでデバッグする
|
||||
|
||||
`~/.insomnia/sessions`にすべてのセッションがある。jsonlなので、いい感じにBashで読むこと。
|
||||
|
||||
---
|
||||
|
||||
## Git操作
|
||||
|
||||
workflowで明示されない限り、読み取り以外の操作は控えること。
|
||||
基本はworktree上の一時的なブランチでコミットを重ね、メインブランチに取り込む運用をしている。
|
||||
コミットメッセージは適当に`<prefix>: *簡潔な1行*`で書いている。
|
||||
|
||||
外部の参考プロジェクトは必要に応じてローカルの外部 checkout からReadすること。
|
||||
|
||||
---
|
||||
|
||||
## Ticketの運用について
|
||||
|
||||
`TODO.md`、`tickets/`はgitで管理されていて、時系列の管理はgitを参照して把握すること。
|
||||
|
||||
### TODO.md
|
||||
|
||||
- 1チケット = 1行。未完了のみ記載し、完了したら行ごと削除する(履歴はgitで追える)
|
||||
- ネストは同一領域のグルーピング(表示用)にのみ使う。実装上の依存関係はネストで表現しない
|
||||
- 完了した子は削除し、親は未完了の子がある限り残す。最後の子が完了したら親ごと削除
|
||||
- Ticketを追加する際は、合わせてTODOも書くこと
|
||||
|
||||
### Ticket の粒度
|
||||
|
||||
- 1チケット = 完了時点で、実装が仕様又は機能として説明できる粒度。
|
||||
- 作成時、背景や要件を前提として書き、実装の方針やコードの詳細は不必要に増やさない。
|
||||
- チケット内のステップ(Phase 1, 2, ...)は実装順序であり、TODO等、外に出さない
|
||||
- ビルドが通り、その機能に限り,まだ動作できないと明示出来ている場合を除いて全体を通して動作させられる状態である必要がある。
|
||||
|
||||
### Ticket のライフサイクル
|
||||
|
||||
gitがタイムラインの単一の情報源。ファイル操作とcommitで状態遷移を表現する。
|
||||
|
||||
a. 作成: `tickets/foo.md` を作成してcommit
|
||||
b. 詳細化や前提の変化: `tickets/foo.md` を更新してcommit
|
||||
c. レビュー: `tickets/foo.md` にレビュー状態を追記 + `tickets/foo.review.md` を作成してcommit
|
||||
d. 完了: `tickets/foo.md` と `tickets/foo.review.md` を両方削除してcommit
|
||||
|
||||
worktreeと併用して作業を進める場合、必ずブランチを切る前に対象のチケットをコミットしてから切ること。
|
||||
|
||||
TODO.mdのリンクは完了後に切れるが、そのリンクを元にgitで消されたファイルを読み、内容を把握できる。
|
||||
`.review.md` にはレビューの指摘事項と判断結果を記載する。
|
||||
レビューはdiffの確認だけでなく、チケットはどのような前提・要件であり、それが達成されたかの確認まで含めて行う。
|
||||
常に、提出された実装で良いのか、コードベースを歪めていないか、不必要な実装ではないかを確認すること。
|
||||
|
||||
---
|
||||
|
||||
insomniaでinsomniaを開発している際、AI自身のフィードバックを元に改善を回すために `docs/report/`ディレクトリに感じた障壁や改善案等を書き残す形にした。 明確に力不足な点/ツールの問題があった場合や、ユーザーからの指示があった際に作ること。
|
||||
29
Cargo.lock
generated
29
Cargo.lock
generated
|
|
@ -1671,7 +1671,6 @@ dependencies = [
|
|||
"tracing-subscriber",
|
||||
"trybuild",
|
||||
"wiremock",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4543,31 +4542,3 @@ name = "zmij"
|
|||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.16+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
|
|
|||
31
TODO.md
31
TODO.md
|
|
@ -1,5 +1,28 @@
|
|||
# TODO legacy notice
|
||||
- Workflow / Skills
|
||||
- 内部 Worker / 内部 Pod の Workflow 化 → [tickets/internal-worker-workflow.md](tickets/internal-worker-workflow.md)
|
||||
- 半自動開発運用 Workflow → [tickets/auto-maintain-workflow.md](tickets/auto-maintain-workflow.md)
|
||||
- AI maintainer 用 WorkItem / Thread 抽象 → [tickets/maintainer-work-items.md](tickets/maintainer-work-items.md)
|
||||
- Prompt / Workflow 評価メトリクスと改善 Offer → [tickets/prompt-eval-metrics.md](tickets/prompt-eval-metrics.md)
|
||||
- Permission: allow-all 既定 policy への整理 → [tickets/permission-default-policy.md](tickets/permission-default-policy.md)
|
||||
- Pod: 任意ターンからの Fork(複数ターン巻き戻しを汎用化) → [tickets/pod-session-fork.md](tickets/pod-session-fork.md)
|
||||
- Pod/TUI: 手動 rollback 導線 → [tickets/manual-turn-rollback.md](tickets/manual-turn-rollback.md)
|
||||
- Pod: Inbound PodEvent ハンドリングの重複を統合 → [tickets/pod-inbound-pod-event-dedup.md](tickets/pod-inbound-pod-event-dedup.md)
|
||||
- SpawnPod 初回 task delivery の受理確認 → [tickets/spawnpod-initial-run-confirmation.md](tickets/spawnpod-initial-run-confirmation.md)
|
||||
- E2E テストハーネス(`tests/e2e/`、opt-in) → [tickets/e2e-harness.md](tickets/e2e-harness.md)
|
||||
- メモリ機構
|
||||
- consolidation skip 表示と invalid staging の観測性 → [tickets/memory-consolidation-skip-observability.md](tickets/memory-consolidation-skip-observability.md)
|
||||
- summary.md の resident 注入 → [tickets/memory-summary-resident-injection.md](tickets/memory-summary-resident-injection.md)
|
||||
|
||||
Active repository work items have been migrated to `work-items/`.
|
||||
|
||||
Use `./tickets.sh list --status all` for the generated/current view and `./tickets.sh doctor` to validate the migration state.
|
||||
- TUI 拡充
|
||||
- navigation mode / block focus の設計 → [tickets/tui-navigation-mode-design.md](tickets/tui-navigation-mode-design.md)
|
||||
- spawned child Pod の一覧と一時 attach → [tickets/tui-spawned-pod-panel.md](tickets/tui-spawned-pod-panel.md)
|
||||
- actionbar transient notice API → [tickets/tui-actionbar-transient-notice-api.md](tickets/tui-actionbar-transient-notice-api.md)
|
||||
- tui -r picker で live pending Pod が表示から漏れる → [tickets/tui-picker-live-pending-pods.md](tickets/tui-picker-live-pending-pods.md)
|
||||
- user manifest env override 時の spawn scope overlay 前提ズレ → [tickets/tui-user-manifest-env-overlay.md](tickets/tui-user-manifest-env-overlay.md)
|
||||
- ユーザーマニフェストのモデル設定 wizard → [tickets/tui-user-model-setup.md](tickets/tui-user-model-setup.md)
|
||||
- セッション内 Task ツールの注意機構(無アクティビティで `<system-reminder>` ナッジ) → [tickets/session-todo-reminder.md](tickets/session-todo-reminder.md)
|
||||
- ワークスペースのメモリーをLintするヘッドレスCLI
|
||||
- system-reminder 注入機構の汎用化(2件目の利用者が出た時に検討。タグ形式 `<system-reminder>...</system-reminder>` の規約は session-todo-reminder で先行確立。注入された Item は worker.history に append する方針)
|
||||
- Bashツールがファイル編集に常用されている問題をdesciptionで抑制
|
||||
- 事前定義したManifestをProfile的に扱い、Orchestrator/Coder/Researcherで別々のモデル/設定を使わせる運用ができるようにする
|
||||
- 複数のPodのViewを行き来できるUI
|
||||
|
|
|
|||
|
|
@ -12,11 +12,10 @@ thiserror = { workspace = true }
|
|||
tracing = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
tokio-util = "0.7"
|
||||
reqwest = { version = "0.13", default-features = false, features = ["stream", "json", "native-tls", "http2"] }
|
||||
eventsource-stream = "0.2"
|
||||
zstd = "0.13"
|
||||
llm-worker-macros = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
|||
|
|
@ -45,13 +45,4 @@ pub enum AuthRequirement {
|
|||
pub trait AuthProvider: Send + Sync + std::fmt::Debug {
|
||||
/// 1 リクエスト分の認証ヘッダを返す。refresh が必要なら内部で行う。
|
||||
async fn headers(&self) -> Result<Vec<(HeaderName, HeaderValue)>, ClientError>;
|
||||
|
||||
/// ChatGPT Codex backend 向けの複合認証かどうか。
|
||||
///
|
||||
/// transport は provider crate の具象型を知らないため、この hook だけで
|
||||
/// Codex CLI 互換の wire behavior(conversation header / request compression 等)
|
||||
/// を切り替える。
|
||||
fn is_codex_backend(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,6 @@ pub enum ClientError {
|
|||
message: String,
|
||||
retry_after: Option<Duration>,
|
||||
},
|
||||
/// A request lifecycle phase exceeded its hard timeout.
|
||||
Timeout {
|
||||
phase: &'static str,
|
||||
timeout: Duration,
|
||||
},
|
||||
/// 設定エラー
|
||||
Config(String),
|
||||
}
|
||||
|
|
@ -48,9 +43,6 @@ impl fmt::Display for ClientError {
|
|||
}
|
||||
write!(f, ": {}", message)
|
||||
}
|
||||
ClientError::Timeout { phase, timeout } => {
|
||||
write!(f, "{phase} timed out after {}s", timeout.as_secs())
|
||||
}
|
||||
ClientError::Config(msg) => write!(f, "Config error: {}", msg),
|
||||
}
|
||||
}
|
||||
|
|
@ -99,7 +91,6 @@ impl ClientError {
|
|||
/// 対象:
|
||||
/// - `Api { status }` のうち 408 / 425 / 429 / 500 / 502 / 503 / 504 / 529
|
||||
/// - `Http(reqwest::Error)` のうち `is_connect()` または `is_timeout()`
|
||||
/// - `Timeout { .. }` の lifecycle hard timeout
|
||||
///
|
||||
/// それ以外(Json、Sse、Config、上記以外の Api ステータス)は false。
|
||||
/// SSE 読み出し開始後の失敗は呼び出し側で `Sse` として上に流すため、
|
||||
|
|
@ -110,7 +101,6 @@ pub fn is_retryable(error: &ClientError) -> bool {
|
|||
status: Some(code), ..
|
||||
} => matches!(*code, 408 | 425 | 429 | 500 | 502 | 503 | 504 | 529),
|
||||
ClientError::Api { status: None, .. } => false,
|
||||
ClientError::Timeout { .. } => true,
|
||||
ClientError::Http(e) => e.is_connect() || e.is_timeout(),
|
||||
ClientError::Json(_) | ClientError::Sse(_) | ClientError::Config(_) => false,
|
||||
}
|
||||
|
|
@ -154,14 +144,6 @@ mod tests {
|
|||
assert!(!is_retryable(&api_err(None)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lifecycle_timeout_is_retryable() {
|
||||
assert!(is_retryable(&ClientError::Timeout {
|
||||
phase: "stream_open",
|
||||
timeout: Duration::from_secs(30),
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_sse_config_not_retryable() {
|
||||
let json_err = serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
|
|||
///
|
||||
/// # イベントの種類
|
||||
///
|
||||
/// - **メタイベント**: `Ping`, `Usage`, `Status`, `Error`, `UnhandledSse`
|
||||
/// - **メタイベント**: `Ping`, `Usage`, `Status`, `Error`
|
||||
/// - **ブロックイベント**: `BlockStart`, `BlockDelta`, `BlockStop`, `BlockAbort`
|
||||
/// - **永続化イベント**: `ReasoningItem` (history に commit すべき完成済み
|
||||
/// reasoning item。streaming 表示用の Thinking BlockStart/Delta/Stop と
|
||||
|
|
@ -35,10 +35,6 @@ pub enum Event {
|
|||
Status(StatusEvent),
|
||||
/// エラー発生
|
||||
Error(ErrorEvent),
|
||||
/// Scheme が生成内容として解釈しない未対応 SSE イベント。
|
||||
///
|
||||
/// stream trace 用の観測イベントであり、timeline / history には反映しない。
|
||||
UnhandledSse(UnhandledSseEvent),
|
||||
|
||||
/// ブロック開始(テキスト、ツール使用等)
|
||||
BlockStart(BlockStart),
|
||||
|
|
@ -123,18 +119,6 @@ pub struct ErrorEvent {
|
|||
pub message: String,
|
||||
}
|
||||
|
||||
/// 未対応 SSE イベントの観測用メタイベント。
|
||||
///
|
||||
/// `data_preview` は provider から受け取った raw SSE data の bounded preview、
|
||||
/// `data_len` は preview 前の raw data byte length。
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct UnhandledSseEvent {
|
||||
pub provider: String,
|
||||
pub event_type: String,
|
||||
pub data_preview: String,
|
||||
pub data_len: usize,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Block Types
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -5,16 +5,15 @@
|
|||
//! insomnia 側 1 次元 `BlockStart/Delta/Stop::index` のマッピングは
|
||||
//! [`OpenAIResponsesState`] が保持する。
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::llm_client::{
|
||||
ClientError,
|
||||
event::{
|
||||
BlockDelta, BlockMetadata, BlockStart, BlockStop, BlockType, DeltaContent, ErrorEvent,
|
||||
Event, ReasoningItemEvent, ResponseStatus, StatusEvent, UnhandledSseEvent, UsageEvent,
|
||||
Event, ReasoningItemEvent, ResponseStatus, StatusEvent, UsageEvent,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -256,16 +255,12 @@ struct InputTokensDetails {
|
|||
#[derive(Debug, Deserialize)]
|
||||
struct ResponseFailed {
|
||||
response: FailedResponse,
|
||||
#[serde(flatten)]
|
||||
extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct FailedResponse {
|
||||
#[serde(default)]
|
||||
error: Option<ErrorDetail>,
|
||||
#[serde(flatten)]
|
||||
extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -274,17 +269,6 @@ struct ErrorDetail {
|
|||
error_type: Option<String>,
|
||||
#[serde(default)]
|
||||
message: Option<String>,
|
||||
#[serde(default)]
|
||||
code: Option<String>,
|
||||
#[serde(flatten)]
|
||||
extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TopLevelErrorEnvelope {
|
||||
error: TopLevelError,
|
||||
#[serde(flatten)]
|
||||
extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -295,8 +279,6 @@ struct TopLevelError {
|
|||
error_type: Option<String>,
|
||||
#[serde(default)]
|
||||
code: Option<String>,
|
||||
#[serde(flatten)]
|
||||
extra: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -305,9 +287,9 @@ struct TopLevelError {
|
|||
|
||||
/// SSE フレーム 1 件をパースし、0 個以上の [`Event`] に変換する。
|
||||
///
|
||||
/// `event_type` は SSE の `event:` フィールド。未対応の event type は
|
||||
/// [`Event::UnhandledSse`] として観測可能にする。`data` が JSON でない /
|
||||
/// 必要なフィールドが抜けている等は [`ClientError::Api`] で返す。
|
||||
/// `event_type` は SSE の `event:` フィールド。未対応の event は
|
||||
/// 静かに無視する。`data` が JSON でない / 必要なフィールドが抜けて
|
||||
/// いる等は [`ClientError::Api`] で返す。
|
||||
pub(crate) fn parse_sse(
|
||||
event_type: &str,
|
||||
data: &str,
|
||||
|
|
@ -343,7 +325,10 @@ pub(crate) fn parse_sse(
|
|||
|
||||
"response.failed" | "response.incomplete" => {
|
||||
let ev: ResponseFailed = from_json(data)?;
|
||||
let (code, message) = response_failure_diagnostic(event_type, ev);
|
||||
let (code, message) = match ev.response.error {
|
||||
Some(err) => (err.error_type, err.message.unwrap_or_default()),
|
||||
None => (None, format!("response {event_type}")),
|
||||
};
|
||||
Ok(vec![
|
||||
Event::Error(ErrorEvent { code, message }),
|
||||
Event::Status(StatusEvent {
|
||||
|
|
@ -566,167 +551,22 @@ pub(crate) fn parse_sse(
|
|||
}
|
||||
|
||||
"error" => {
|
||||
let ev = from_json::<TopLevelErrorEnvelope>(data).unwrap_or_else(|_| {
|
||||
TopLevelErrorEnvelope {
|
||||
error: TopLevelError {
|
||||
message: Some(data.to_string()),
|
||||
error_type: None,
|
||||
code: None,
|
||||
extra: BTreeMap::new(),
|
||||
},
|
||||
extra: BTreeMap::new(),
|
||||
}
|
||||
let ev: TopLevelError = from_json(data).unwrap_or(TopLevelError {
|
||||
message: Some(data.to_string()),
|
||||
error_type: None,
|
||||
code: None,
|
||||
});
|
||||
let (code, message) = top_level_error_diagnostic(ev);
|
||||
Ok(vec![Event::Error(ErrorEvent { code, message })])
|
||||
Ok(vec![Event::Error(ErrorEvent {
|
||||
code: ev.error_type.or(ev.code),
|
||||
message: ev.message.unwrap_or_default(),
|
||||
})])
|
||||
}
|
||||
|
||||
// 未対応 / 情報系 event type は生成 semantics からは無視しつつ trace に残す。
|
||||
_ => Ok(vec![unhandled_sse_event(event_type, data)]),
|
||||
// 未対応 / 情報系イベントは無視
|
||||
_ => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn response_failure_diagnostic(event_type: &str, ev: ResponseFailed) -> (Option<String>, String) {
|
||||
let mut diagnostic = Map::new();
|
||||
diagnostic.insert("event".to_string(), Value::String(event_type.to_string()));
|
||||
|
||||
let mut code = None;
|
||||
let base_message = if let Some(err) = ev.response.error {
|
||||
code = err.code.clone().or(err.error_type.clone());
|
||||
if let Some(error_type) = err.error_type {
|
||||
diagnostic.insert("error_type".to_string(), Value::String(error_type));
|
||||
}
|
||||
if let Some(error_code) = err.code {
|
||||
diagnostic.insert("error_code".to_string(), Value::String(error_code));
|
||||
}
|
||||
if !err.extra.is_empty() {
|
||||
diagnostic.insert(
|
||||
"error_extra".to_string(),
|
||||
diagnostic_object(err.extra, DIAGNOSTIC_VALUE_LIMIT),
|
||||
);
|
||||
}
|
||||
err.message
|
||||
.filter(|message| !message.trim().is_empty())
|
||||
.unwrap_or_else(|| format!("OpenAI Responses {event_type}"))
|
||||
} else {
|
||||
format!("OpenAI Responses {event_type}")
|
||||
};
|
||||
|
||||
let response_extra = ev.response.extra;
|
||||
if let Some(reason) = response_extra
|
||||
.get("incomplete_details")
|
||||
.and_then(|value| value.get("reason"))
|
||||
.and_then(Value::as_str)
|
||||
{
|
||||
diagnostic.insert(
|
||||
"incomplete_reason".to_string(),
|
||||
Value::String(reason.to_string()),
|
||||
);
|
||||
if code.is_none() {
|
||||
code = Some(reason.to_string());
|
||||
}
|
||||
}
|
||||
if !response_extra.is_empty() {
|
||||
diagnostic.insert(
|
||||
"response_extra".to_string(),
|
||||
diagnostic_object(response_extra, DIAGNOSTIC_VALUE_LIMIT),
|
||||
);
|
||||
}
|
||||
if !ev.extra.is_empty() {
|
||||
diagnostic.insert(
|
||||
"event_extra".to_string(),
|
||||
diagnostic_object(ev.extra, DIAGNOSTIC_VALUE_LIMIT),
|
||||
);
|
||||
}
|
||||
|
||||
(code, append_diagnostic(base_message, diagnostic))
|
||||
}
|
||||
|
||||
fn top_level_error_diagnostic(ev: TopLevelErrorEnvelope) -> (Option<String>, String) {
|
||||
let code = ev.error.code.clone().or(ev.error.error_type.clone());
|
||||
let mut diagnostic = Map::new();
|
||||
diagnostic.insert("event".to_string(), Value::String("error".to_string()));
|
||||
if let Some(error_type) = ev.error.error_type {
|
||||
diagnostic.insert("error_type".to_string(), Value::String(error_type));
|
||||
}
|
||||
if let Some(error_code) = ev.error.code {
|
||||
diagnostic.insert("error_code".to_string(), Value::String(error_code));
|
||||
}
|
||||
if !ev.error.extra.is_empty() {
|
||||
diagnostic.insert(
|
||||
"error_extra".to_string(),
|
||||
diagnostic_object(ev.error.extra, DIAGNOSTIC_VALUE_LIMIT),
|
||||
);
|
||||
}
|
||||
if !ev.extra.is_empty() {
|
||||
diagnostic.insert(
|
||||
"event_extra".to_string(),
|
||||
diagnostic_object(ev.extra, DIAGNOSTIC_VALUE_LIMIT),
|
||||
);
|
||||
}
|
||||
|
||||
let message = ev
|
||||
.error
|
||||
.message
|
||||
.filter(|message| !message.trim().is_empty())
|
||||
.unwrap_or_else(|| "OpenAI Responses error".to_string());
|
||||
(code, append_diagnostic(message, diagnostic))
|
||||
}
|
||||
|
||||
const DIAGNOSTIC_VALUE_LIMIT: usize = 512;
|
||||
const UNHANDLED_SSE_DATA_PREVIEW_LIMIT: usize = 512;
|
||||
|
||||
fn capped_unhandled_sse_data_preview(data: &str) -> String {
|
||||
if data.len() <= UNHANDLED_SSE_DATA_PREVIEW_LIMIT {
|
||||
return data.to_string();
|
||||
}
|
||||
|
||||
let mut end = 0;
|
||||
for (idx, ch) in data.char_indices() {
|
||||
let next = idx + ch.len_utf8();
|
||||
if next > UNHANDLED_SSE_DATA_PREVIEW_LIMIT {
|
||||
break;
|
||||
}
|
||||
end = next;
|
||||
}
|
||||
data[..end].to_string()
|
||||
}
|
||||
|
||||
fn unhandled_sse_event(event_type: &str, data: &str) -> Event {
|
||||
Event::UnhandledSse(UnhandledSseEvent {
|
||||
provider: "openai_responses".to_string(),
|
||||
event_type: event_type.to_string(),
|
||||
data_preview: capped_unhandled_sse_data_preview(data),
|
||||
data_len: data.len(),
|
||||
})
|
||||
}
|
||||
|
||||
fn diagnostic_object(extra: BTreeMap<String, Value>, value_limit: usize) -> Value {
|
||||
Value::Object(
|
||||
extra
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key, cap_json_value(value, value_limit)))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn cap_json_value(value: Value, limit: usize) -> Value {
|
||||
let rendered = value.to_string();
|
||||
if rendered.len() <= limit {
|
||||
value
|
||||
} else {
|
||||
let capped: String = rendered.chars().take(limit).collect();
|
||||
Value::String(format!("{capped}…"))
|
||||
}
|
||||
}
|
||||
|
||||
fn append_diagnostic(message: String, diagnostic: Map<String, Value>) -> String {
|
||||
if diagnostic.len() <= 1 {
|
||||
return message;
|
||||
}
|
||||
format!("{} | diagnostic={}", message, Value::Object(diagnostic))
|
||||
}
|
||||
|
||||
/// 対応する BlockStart がまだ発行されていなければ発行しつつ、delta を流す。
|
||||
/// content_part.added を取りこぼしても delta 単独で復旧できるようにする。
|
||||
fn ensure_and_delta(
|
||||
|
|
@ -1033,88 +873,6 @@ mod tests {
|
|||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_response_preserves_incomplete_reason_without_error() {
|
||||
let data = r#"{
|
||||
"response": {
|
||||
"status": "incomplete",
|
||||
"incomplete_details": {"reason": "max_output_tokens"}
|
||||
}
|
||||
}"#;
|
||||
let (events, _) = run("response.incomplete", data);
|
||||
let Event::Error(err) = &events[0] else {
|
||||
panic!("expected error event")
|
||||
};
|
||||
assert_eq!(err.code.as_deref(), Some("max_output_tokens"));
|
||||
assert!(err.message.contains("OpenAI Responses response.incomplete"));
|
||||
assert!(err.message.contains("incomplete_reason"));
|
||||
assert!(err.message.contains("max_output_tokens"));
|
||||
assert!(!err.message.ends_with("response response.incomplete"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_response_preserves_unknown_response_fields() {
|
||||
let data = r#"{
|
||||
"response": {
|
||||
"status": "incomplete",
|
||||
"incomplete_details": {"reason": "content_filter"},
|
||||
"mystery_field": {"nested": true}
|
||||
},
|
||||
"sequence_number": 42
|
||||
}"#;
|
||||
let (events, _) = run("response.incomplete", data);
|
||||
let Event::Error(err) = &events[0] else {
|
||||
panic!("expected error event")
|
||||
};
|
||||
assert!(err.message.contains("mystery_field"));
|
||||
assert!(err.message.contains("sequence_number"));
|
||||
assert!(err.message.contains("content_filter"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_response_preserves_error_and_response_extra_fields() {
|
||||
let data = r#"{
|
||||
"response": {
|
||||
"error": {
|
||||
"type": "server_error",
|
||||
"code": "upstream_overloaded",
|
||||
"message": "try later",
|
||||
"param": "input"
|
||||
},
|
||||
"retry_hint": "short"
|
||||
}
|
||||
}"#;
|
||||
let (events, _) = run("response.failed", data);
|
||||
let Event::Error(err) = &events[0] else {
|
||||
panic!("expected error event")
|
||||
};
|
||||
assert_eq!(err.code.as_deref(), Some("upstream_overloaded"));
|
||||
assert!(err.message.contains("try later"));
|
||||
assert!(err.message.contains("param"));
|
||||
assert!(err.message.contains("retry_hint"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_error_preserves_unknown_fields() {
|
||||
let data = r#"{
|
||||
"error": {
|
||||
"type": "rate_limit_error",
|
||||
"code": "rate_limit_exceeded",
|
||||
"message": "slow down",
|
||||
"retry_after_ms": 1000
|
||||
},
|
||||
"request_id": "req_123"
|
||||
}"#;
|
||||
let (events, _) = run("error", data);
|
||||
let Event::Error(err) = &events[0] else {
|
||||
panic!("expected error event")
|
||||
};
|
||||
assert_eq!(err.code.as_deref(), Some("rate_limit_exceeded"));
|
||||
assert!(err.message.contains("slow down"));
|
||||
assert!(err.message.contains("retry_after_ms"));
|
||||
assert!(err.message.contains("request_id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_output_item_emits_reasoning_item_with_text_summary_encrypted() {
|
||||
// 完成済み reasoning wrapper が text + summary[] + encrypted_content を持って
|
||||
|
|
@ -1208,33 +966,8 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_event_emits_trace_visible_unhandled_sse() {
|
||||
let data = r#"{"sequence_number":7,"note":"debug me"}"#;
|
||||
let (events, _) = run("response.mystery", data);
|
||||
assert_eq!(events.len(), 1);
|
||||
let Event::UnhandledSse(unhandled) = &events[0] else {
|
||||
panic!("expected UnhandledSse, got {:?}", events[0]);
|
||||
};
|
||||
assert_eq!(unhandled.provider, "openai_responses");
|
||||
assert_eq!(unhandled.event_type, "response.mystery");
|
||||
assert_eq!(unhandled.data_preview, data);
|
||||
assert_eq!(unhandled.data_len, data.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_event_data_preview_is_bounded_and_data_len_is_original_bytes() {
|
||||
let data = format!("{}終端", "x".repeat(UNHANDLED_SSE_DATA_PREVIEW_LIMIT + 32));
|
||||
let (events, _) = run("response.mystery.large", &data);
|
||||
assert_eq!(events.len(), 1);
|
||||
let Event::UnhandledSse(unhandled) = &events[0] else {
|
||||
panic!("expected UnhandledSse, got {:?}", events[0]);
|
||||
};
|
||||
assert_eq!(unhandled.data_len, data.len());
|
||||
assert!(unhandled.data_preview.len() <= UNHANDLED_SSE_DATA_PREVIEW_LIMIT);
|
||||
assert_eq!(
|
||||
unhandled.data_preview,
|
||||
"x".repeat(UNHANDLED_SSE_DATA_PREVIEW_LIMIT)
|
||||
);
|
||||
assert!(unhandled.data_preview.len() < unhandled.data_len);
|
||||
fn unknown_event_is_ignored() {
|
||||
let (events, _) = run("response.in_progress", "{}");
|
||||
assert!(events.is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ use std::time::Duration;
|
|||
use async_trait::async_trait;
|
||||
use eventsource_stream::Eventsource;
|
||||
use futures::{Stream, StreamExt, TryStreamExt};
|
||||
use reqwest::header::{
|
||||
ACCEPT, CONTENT_ENCODING, CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue, RETRY_AFTER,
|
||||
};
|
||||
use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue, RETRY_AFTER};
|
||||
|
||||
use super::auth::{AuthProvider, AuthRequirement};
|
||||
use super::capability::ModelCapability;
|
||||
|
|
@ -23,9 +21,6 @@ use super::event::Event;
|
|||
use super::scheme::Scheme;
|
||||
use super::types::{Request, RequestConfig};
|
||||
|
||||
pub const DEFAULT_STREAM_OPEN_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
pub const DEFAULT_FIRST_STREAM_EVENT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
/// `AuthRef` を解決したランタイム表現。`crates/provider` が構築する。
|
||||
///
|
||||
/// - `None`: 認証ヘッダを送らない(Ollama 等の opt-out)
|
||||
|
|
@ -154,65 +149,6 @@ impl<S: Scheme> HttpTransport<S> {
|
|||
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
fn is_codex_backend(&self) -> bool {
|
||||
match &self.auth {
|
||||
ResolvedAuth::Custom(provider) => provider.is_codex_backend(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_stream_headers(
|
||||
&self,
|
||||
headers: &mut HeaderMap,
|
||||
request: &Request,
|
||||
) -> Result<(), ClientError> {
|
||||
headers.insert(ACCEPT, HeaderValue::from_static("text/event-stream"));
|
||||
|
||||
if self.is_codex_backend()
|
||||
&& let Some(cache_key) = request.cache_key.as_deref()
|
||||
{
|
||||
let value = HeaderValue::from_str(cache_key).map_err(|e| {
|
||||
ClientError::Config(format!("invalid Codex conversation header: {e}"))
|
||||
})?;
|
||||
headers.insert(HeaderName::from_static("session_id"), value.clone());
|
||||
headers.insert(HeaderName::from_static("x-client-request-id"), value);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encode_request_body(
|
||||
&self,
|
||||
body: &serde_json::Value,
|
||||
headers: &mut HeaderMap,
|
||||
) -> Result<RequestBody, ClientError> {
|
||||
if !self.is_codex_backend() {
|
||||
return Ok(RequestBody::Json(body.clone()));
|
||||
}
|
||||
|
||||
let raw = serde_json::to_vec(body)?;
|
||||
let compressed = zstd::stream::encode_all(std::io::Cursor::new(raw), 3)
|
||||
.map_err(|e| ClientError::Config(format!("failed to zstd-compress request: {e}")))?;
|
||||
headers.insert(CONTENT_ENCODING, HeaderValue::from_static("zstd"));
|
||||
Ok(RequestBody::CompressedJson(compressed))
|
||||
}
|
||||
}
|
||||
|
||||
enum RequestBody {
|
||||
Json(serde_json::Value),
|
||||
CompressedJson(Vec<u8>),
|
||||
}
|
||||
|
||||
async fn response_with_timeout(
|
||||
future: impl std::future::Future<Output = Result<reqwest::Response, reqwest::Error>>,
|
||||
timeout: Duration,
|
||||
phase: &'static str,
|
||||
) -> Result<reqwest::Response, ClientError> {
|
||||
tokio::time::timeout(timeout, future)
|
||||
.await
|
||||
.map_err(|_| ClientError::Timeout { phase, timeout })?
|
||||
.map_err(ClientError::Http)
|
||||
}
|
||||
|
||||
impl<S: Scheme + Clone> Clone for HttpTransport<S> {
|
||||
|
|
@ -274,21 +210,19 @@ impl<S: Scheme + Clone + 'static> LlmClient for HttpTransport<S> {
|
|||
|
||||
async fn stream(&self, request: Request) -> Result<ResponseStream, ClientError> {
|
||||
let url = self.build_url();
|
||||
let mut headers = self.build_headers().await?;
|
||||
self.apply_stream_headers(&mut headers, &request)?;
|
||||
let headers = self.build_headers().await?;
|
||||
let body = self
|
||||
.scheme
|
||||
.build_request_body(&self.model_id, &request, &self.capability);
|
||||
let request_body = self.encode_request_body(&body, &mut headers)?;
|
||||
|
||||
let builder = self.http_client.post(&url).headers(headers);
|
||||
let builder = match request_body {
|
||||
RequestBody::Json(body) => builder.json(&body),
|
||||
RequestBody::CompressedJson(body) => builder.body(body),
|
||||
};
|
||||
let response =
|
||||
response_with_timeout(builder.send(), DEFAULT_STREAM_OPEN_TIMEOUT, "stream_open")
|
||||
.await?;
|
||||
let response = self
|
||||
.http_client
|
||||
.post(&url)
|
||||
.headers(headers)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(ClientError::Http)?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(classify_error_response(response).await);
|
||||
|
|
@ -321,165 +255,3 @@ impl<S: Scheme + Clone + 'static> LlmClient for HttpTransport<S> {
|
|||
Ok(Box::pin(stream))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestAuthProvider {
|
||||
codex: bool,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AuthProvider for TestAuthProvider {
|
||||
async fn headers(&self) -> Result<Vec<(HeaderName, HeaderValue)>, ClientError> {
|
||||
Ok(vec![
|
||||
(
|
||||
HeaderName::from_static("authorization"),
|
||||
HeaderValue::from_static("Bearer test-token"),
|
||||
),
|
||||
(
|
||||
HeaderName::from_static("chatgpt-account-id"),
|
||||
HeaderValue::from_static("account-1"),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
fn is_codex_backend(&self) -> bool {
|
||||
self.codex
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestScheme;
|
||||
|
||||
impl Scheme for TestScheme {
|
||||
type State = ();
|
||||
|
||||
fn default_base_url(&self) -> &'static str {
|
||||
"https://example.test"
|
||||
}
|
||||
|
||||
fn path(&self, _model_id: &str) -> String {
|
||||
"/responses".to_string()
|
||||
}
|
||||
|
||||
fn required_auth(&self) -> AuthRequirement {
|
||||
AuthRequirement::Bearer
|
||||
}
|
||||
|
||||
fn build_request_body(
|
||||
&self,
|
||||
model_id: &str,
|
||||
request: &Request,
|
||||
_capability: &ModelCapability,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"model": model_id,
|
||||
"input_len": request.items.len(),
|
||||
"prompt_cache_key": request.cache_key,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_sse(
|
||||
&self,
|
||||
_event_type: &str,
|
||||
_data: &str,
|
||||
_state: &mut Self::State,
|
||||
) -> Result<Vec<Event>, ClientError> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn default_capability(&self) -> ModelCapability {
|
||||
ModelCapability::minimal()
|
||||
}
|
||||
}
|
||||
|
||||
fn transport(auth: ResolvedAuth) -> HttpTransport<TestScheme> {
|
||||
HttpTransport::new(
|
||||
TestScheme,
|
||||
"gpt-test",
|
||||
"https://example.test",
|
||||
auth,
|
||||
ModelCapability::minimal(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn response_timeout_returns_retryable_lifecycle_timeout() {
|
||||
let err = response_with_timeout(
|
||||
std::future::pending::<Result<reqwest::Response, reqwest::Error>>(),
|
||||
Duration::from_millis(5),
|
||||
"stream_open",
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(crate::llm_client::error::is_retryable(&err));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ClientError::Timeout {
|
||||
phase: "stream_open",
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_backend_adds_conversation_headers_and_zstd_body() {
|
||||
let transport = transport(ResolvedAuth::Custom(Arc::new(TestAuthProvider {
|
||||
codex: true,
|
||||
})));
|
||||
let request = Request::new().user("hello").cache_key("segment-123");
|
||||
let mut headers = transport.build_headers().await.unwrap();
|
||||
transport
|
||||
.apply_stream_headers(&mut headers, &request)
|
||||
.unwrap();
|
||||
let body = transport.scheme.build_request_body(
|
||||
&transport.model_id,
|
||||
&request,
|
||||
&transport.capability,
|
||||
);
|
||||
let encoded = transport.encode_request_body(&body, &mut headers).unwrap();
|
||||
|
||||
assert_eq!(headers.get(ACCEPT).unwrap(), "text/event-stream");
|
||||
assert_eq!(headers.get("session_id").unwrap(), "segment-123");
|
||||
assert_eq!(headers.get("x-client-request-id").unwrap(), "segment-123");
|
||||
assert_eq!(headers.get(CONTENT_ENCODING).unwrap(), "zstd");
|
||||
|
||||
let RequestBody::CompressedJson(compressed) = encoded else {
|
||||
panic!("Codex backend request body must be zstd-compressed");
|
||||
};
|
||||
let decoded = zstd::stream::decode_all(std::io::Cursor::new(compressed)).unwrap();
|
||||
let decoded: serde_json::Value = serde_json::from_slice(&decoded).unwrap();
|
||||
assert_eq!(decoded["prompt_cache_key"], "segment-123");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn non_codex_request_does_not_get_codex_only_headers_or_compression() {
|
||||
let transport = transport(ResolvedAuth::ApiKey("api-key".to_string()));
|
||||
let request = Request::new().user("hello").cache_key("segment-123");
|
||||
let mut headers = transport.build_headers().await.unwrap();
|
||||
transport
|
||||
.apply_stream_headers(&mut headers, &request)
|
||||
.unwrap();
|
||||
let body = transport.scheme.build_request_body(
|
||||
&transport.model_id,
|
||||
&request,
|
||||
&transport.capability,
|
||||
);
|
||||
let encoded = transport.encode_request_body(&body, &mut headers).unwrap();
|
||||
|
||||
assert_eq!(headers.get(ACCEPT).unwrap(), "text/event-stream");
|
||||
assert!(headers.get("session_id").is_none());
|
||||
assert!(headers.get("x-client-request-id").is_none());
|
||||
assert!(headers.get(CONTENT_ENCODING).is_none());
|
||||
|
||||
let RequestBody::Json(decoded) = encoded else {
|
||||
panic!("non-Codex request body must remain normal JSON");
|
||||
};
|
||||
assert_eq!(decoded["prompt_cache_key"], "segment-123");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -530,8 +530,6 @@ impl Timeline {
|
|||
Event::Ping(p) => self.dispatch_ping(p),
|
||||
Event::Status(s) => self.dispatch_status(s),
|
||||
Event::Error(e) => self.dispatch_error(e),
|
||||
// Observability-only event: stream trace records it before timeline dispatch.
|
||||
Event::UnhandledSse(_) => {}
|
||||
|
||||
// Block系: スコープ管理しながらディスパッチ
|
||||
Event::BlockStart(s) => self.handle_block_start(s),
|
||||
|
|
@ -680,36 +678,6 @@ mod tests {
|
|||
assert!(timeline.current_block().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unhandled_sse_is_ignored_by_timeline_handlers() {
|
||||
struct TestTextHandler {
|
||||
calls: Arc<Mutex<Vec<TextBlockEvent>>>,
|
||||
}
|
||||
|
||||
impl Handler<TextBlockKind> for TestTextHandler {
|
||||
type Scope = ();
|
||||
fn on_event(&mut self, _scope: &mut (), event: &TextBlockEvent) {
|
||||
self.calls.lock().unwrap().push(event.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let calls = Arc::new(Mutex::new(Vec::new()));
|
||||
let mut timeline = Timeline::new();
|
||||
timeline.on_text_block(TestTextHandler {
|
||||
calls: calls.clone(),
|
||||
});
|
||||
|
||||
timeline.dispatch(&Event::UnhandledSse(UnhandledSseEvent {
|
||||
provider: "openai_responses".to_string(),
|
||||
event_type: "response.mystery".to_string(),
|
||||
data_preview: "{}".to_string(),
|
||||
data_len: 2,
|
||||
}));
|
||||
|
||||
assert!(timeline.current_block().is_none());
|
||||
assert!(calls.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_meta_event_dispatch() {
|
||||
// シンプルなテスト用構造体
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ use std::collections::HashMap;
|
|||
use std::{marker::PhantomData, time::Instant};
|
||||
|
||||
use futures::StreamExt;
|
||||
use serde_json::{Value, json};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, info, trace, warn};
|
||||
|
||||
|
|
@ -19,8 +18,7 @@ use crate::{
|
|||
},
|
||||
llm_client::{
|
||||
ClientError, ConfigWarning, LlmClient, Request, RequestConfig, ResponseStream,
|
||||
ToolDefinition, error::is_retryable, event::Event, retry::RetryPolicy,
|
||||
transport::DEFAULT_FIRST_STREAM_EVENT_TIMEOUT, types::parse_tool_arguments,
|
||||
ToolDefinition, error::is_retryable, retry::RetryPolicy, types::parse_tool_arguments,
|
||||
},
|
||||
state::{Locked, Mutable, WorkerState},
|
||||
timeline::event::{ErrorEvent, StatusEvent, UsageEvent},
|
||||
|
|
@ -202,12 +200,6 @@ pub struct Worker<C: LlmClient, S: WorkerState = Mutable> {
|
|||
llm_retry_cbs: Vec<Box<dyn Fn(usize, &LlmRetryNotice) + Send + Sync>>,
|
||||
/// Stream continuation callbacks for a specific LlmCall.
|
||||
llm_continuation_cbs: Vec<Box<dyn Fn(usize, u32, u32, &str) + Send + Sync>>,
|
||||
/// Stream event callbacks. Fired for every normalized provider stream
|
||||
/// event before it enters the Timeline.
|
||||
stream_event_cbs: Vec<Box<dyn Fn(usize, usize, &Event) + Send + Sync>>,
|
||||
/// Pre-stream lifecycle callbacks for debugging stalls before provider
|
||||
/// stream events become visible.
|
||||
lifecycle_trace_cbs: Vec<Box<dyn Fn(usize, usize, &str, &Value) + Send + Sync>>,
|
||||
/// Non-fatal warning callbacks. Invoked when the Worker wants to
|
||||
/// surface an advisory message to the upper layer (e.g. Pod) so it
|
||||
/// can be forwarded to the user — distinct from `tracing::warn!`,
|
||||
|
|
@ -416,34 +408,6 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Register a raw normalized stream event callback.
|
||||
pub fn on_stream_event(
|
||||
&mut self,
|
||||
callback: impl Fn(usize, usize, &Event) + Send + Sync + 'static,
|
||||
) {
|
||||
self.stream_event_cbs.push(Box::new(callback));
|
||||
}
|
||||
|
||||
fn emit_stream_event(&self, turn: usize, llm_call: usize, event: &Event) {
|
||||
for cb in &self.stream_event_cbs {
|
||||
cb(turn, llm_call, event);
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a pre-stream lifecycle trace callback.
|
||||
pub fn on_lifecycle_trace(
|
||||
&mut self,
|
||||
callback: impl Fn(usize, usize, &str, &Value) + Send + Sync + 'static,
|
||||
) {
|
||||
self.lifecycle_trace_cbs.push(Box::new(callback));
|
||||
}
|
||||
|
||||
fn emit_lifecycle_trace(&self, turn: usize, llm_call: usize, label: &str, data: Value) {
|
||||
for cb in &self.lifecycle_trace_cbs {
|
||||
cb(turn, llm_call, label, &data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a non-fatal warning callback.
|
||||
///
|
||||
/// The callback is invoked with a short human-readable message
|
||||
|
|
@ -496,15 +460,6 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
|
|||
}
|
||||
}
|
||||
|
||||
fn request_trace_payload(&self, request: &Request) -> Value {
|
||||
items_trace_payload(
|
||||
&request.items,
|
||||
request.tools.len(),
|
||||
request.cache_anchor,
|
||||
request.cache_key.is_some(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Register an AgentTurn-end callback. See [`on_turn_start`](Self::on_turn_start)
|
||||
/// for the 1:1-vs-N relation with `LlmCall*`.
|
||||
pub fn on_turn_end(&mut self, callback: impl Fn(usize) + Send + Sync + 'static) {
|
||||
|
|
@ -1181,22 +1136,8 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
|
|||
}
|
||||
|
||||
// Stream LLM response
|
||||
self.emit_lifecycle_trace(
|
||||
current_turn,
|
||||
current_llm_call,
|
||||
"build_request_start",
|
||||
items_trace_payload(&request_context, tool_definitions.len(), None, false),
|
||||
);
|
||||
let request = self.build_request(&tool_definitions, &request_context);
|
||||
self.emit_lifecycle_trace(
|
||||
current_turn,
|
||||
current_llm_call,
|
||||
"build_request_done",
|
||||
self.request_trace_payload(&request),
|
||||
);
|
||||
let stream_outcome = self
|
||||
.stream_response(request, current_turn, current_llm_call)
|
||||
.await?;
|
||||
let stream_outcome = self.stream_response(request, current_llm_call).await?;
|
||||
|
||||
for cb in &self.llm_call_end_cbs {
|
||||
cb(current_llm_call);
|
||||
|
|
@ -1294,7 +1235,6 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
|
|||
async fn open_stream_with_retry(
|
||||
&mut self,
|
||||
request: Request,
|
||||
turn: usize,
|
||||
llm_call: usize,
|
||||
) -> Result<ResponseStream, WorkerError> {
|
||||
let policy = self.retry_policy.clone();
|
||||
|
|
@ -1302,133 +1242,69 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
|
|||
let mut failed_attempt: u32 = 0;
|
||||
|
||||
loop {
|
||||
let attempt = failed_attempt + 1;
|
||||
self.emit_lifecycle_trace(
|
||||
turn,
|
||||
llm_call,
|
||||
"stream_open_start",
|
||||
json!({
|
||||
"attempt": attempt,
|
||||
"request": self.request_trace_payload(&request),
|
||||
}),
|
||||
);
|
||||
let stream_started = Instant::now();
|
||||
let stream_result = tokio::select! {
|
||||
stream_result = self.client.stream(request.clone()) => stream_result,
|
||||
cancel = self.cancel_rx.recv() => {
|
||||
if cancel.is_some() {
|
||||
info!("Cancelled before stream started");
|
||||
}
|
||||
self.emit_lifecycle_trace(
|
||||
turn,
|
||||
llm_call,
|
||||
"stream_open_cancelled",
|
||||
json!({
|
||||
"attempt": attempt,
|
||||
"elapsed_ms": stream_started.elapsed().as_millis() as u64,
|
||||
}),
|
||||
);
|
||||
self.timeline.abort_current_block();
|
||||
self.last_run_interrupted = true;
|
||||
return Err(WorkerError::Cancelled);
|
||||
}
|
||||
};
|
||||
|
||||
let err = match stream_result {
|
||||
Ok(stream) => {
|
||||
self.emit_lifecycle_trace(
|
||||
turn,
|
||||
llm_call,
|
||||
"stream_open_success",
|
||||
json!({
|
||||
"attempt": attempt,
|
||||
"elapsed_ms": stream_started.elapsed().as_millis() as u64,
|
||||
}),
|
||||
match stream_result {
|
||||
Ok(stream) => return Ok(stream),
|
||||
Err(err) => {
|
||||
let next_failed_attempt = failed_attempt + 1;
|
||||
if next_failed_attempt >= policy.max_attempts || !is_retryable(&err) {
|
||||
self.last_run_interrupted = true;
|
||||
return Err(WorkerError::Client(err));
|
||||
}
|
||||
|
||||
let wait = err
|
||||
.retry_after()
|
||||
.unwrap_or_else(|| policy.backoff(failed_attempt));
|
||||
let elapsed = started.elapsed();
|
||||
if elapsed + wait > policy.total_timeout {
|
||||
self.last_run_interrupted = true;
|
||||
return Err(WorkerError::Client(err));
|
||||
}
|
||||
|
||||
warn!(
|
||||
error = %err,
|
||||
failed_attempt = next_failed_attempt,
|
||||
wait_ms = wait.as_millis() as u64,
|
||||
"transient LLM request error, retrying"
|
||||
);
|
||||
match wait_for_first_stream_event(stream, DEFAULT_FIRST_STREAM_EVENT_TIMEOUT)
|
||||
.await
|
||||
{
|
||||
Ok(FirstStreamEvent::Ready(stream)) => return Ok(stream),
|
||||
Ok(FirstStreamEvent::Empty(stream)) => return Ok(stream),
|
||||
Err(err) => {
|
||||
self.emit_lifecycle_trace(
|
||||
turn,
|
||||
llm_call,
|
||||
"stream_first_event_error",
|
||||
json!({
|
||||
"attempt": attempt,
|
||||
"elapsed_ms": stream_started.elapsed().as_millis() as u64,
|
||||
"retryable": is_retryable(&err),
|
||||
"error": err.to_string(),
|
||||
}),
|
||||
);
|
||||
err
|
||||
let notice = LlmRetryNotice {
|
||||
failed_attempt: next_failed_attempt,
|
||||
max_attempts: policy.max_attempts,
|
||||
wait,
|
||||
elapsed,
|
||||
status: err.status(),
|
||||
error: err.to_string(),
|
||||
};
|
||||
for cb in &self.llm_retry_cbs {
|
||||
cb(llm_call, ¬ice);
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(wait) => {}
|
||||
cancel = self.cancel_rx.recv() => {
|
||||
if cancel.is_some() {
|
||||
info!("Cancelled during LLM retry backoff");
|
||||
}
|
||||
self.timeline.abort_current_block();
|
||||
self.last_run_interrupted = true;
|
||||
return Err(WorkerError::Cancelled);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.emit_lifecycle_trace(
|
||||
turn,
|
||||
llm_call,
|
||||
"stream_open_error",
|
||||
json!({
|
||||
"attempt": attempt,
|
||||
"elapsed_ms": stream_started.elapsed().as_millis() as u64,
|
||||
"retryable": is_retryable(&err),
|
||||
"status": err.status(),
|
||||
"error": err.to_string(),
|
||||
}),
|
||||
);
|
||||
err
|
||||
}
|
||||
};
|
||||
|
||||
let next_failed_attempt = failed_attempt + 1;
|
||||
if next_failed_attempt >= policy.max_attempts || !is_retryable(&err) {
|
||||
self.last_run_interrupted = true;
|
||||
return Err(WorkerError::Client(err));
|
||||
}
|
||||
|
||||
let wait = err
|
||||
.retry_after()
|
||||
.unwrap_or_else(|| policy.backoff(failed_attempt));
|
||||
let elapsed = started.elapsed();
|
||||
if elapsed + wait > policy.total_timeout {
|
||||
self.last_run_interrupted = true;
|
||||
return Err(WorkerError::Client(err));
|
||||
}
|
||||
|
||||
warn!(
|
||||
error = %err,
|
||||
failed_attempt = next_failed_attempt,
|
||||
wait_ms = wait.as_millis() as u64,
|
||||
"transient LLM request error, retrying"
|
||||
);
|
||||
let notice = LlmRetryNotice {
|
||||
failed_attempt: next_failed_attempt,
|
||||
max_attempts: policy.max_attempts,
|
||||
wait,
|
||||
elapsed,
|
||||
status: err.status(),
|
||||
error: err.to_string(),
|
||||
};
|
||||
for cb in &self.llm_retry_cbs {
|
||||
cb(llm_call, ¬ice);
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(wait) => {}
|
||||
cancel = self.cancel_rx.recv() => {
|
||||
if cancel.is_some() {
|
||||
info!("Cancelled during LLM retry backoff");
|
||||
}
|
||||
self.timeline.abort_current_block();
|
||||
self.last_run_interrupted = true;
|
||||
return Err(WorkerError::Cancelled);
|
||||
failed_attempt = next_failed_attempt;
|
||||
}
|
||||
}
|
||||
|
||||
failed_attempt = next_failed_attempt;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1436,7 +1312,6 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
|
|||
async fn stream_response(
|
||||
&mut self,
|
||||
request: Request,
|
||||
turn: usize,
|
||||
llm_call: usize,
|
||||
) -> Result<StreamCompletion, WorkerError> {
|
||||
debug!(
|
||||
|
|
@ -1446,7 +1321,7 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
|
|||
"Sending request to LLM"
|
||||
);
|
||||
|
||||
let mut stream = self.open_stream_with_retry(request, turn, llm_call).await?;
|
||||
let mut stream = self.open_stream_with_retry(request, llm_call).await?;
|
||||
|
||||
let mut event_count: usize = 0;
|
||||
loop {
|
||||
|
|
@ -1474,15 +1349,6 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
|
|||
});
|
||||
}
|
||||
};
|
||||
if event_count == 1 {
|
||||
self.emit_lifecycle_trace(
|
||||
turn,
|
||||
llm_call,
|
||||
"stream_first_event",
|
||||
json!({}),
|
||||
);
|
||||
}
|
||||
self.emit_stream_event(turn, llm_call, &event);
|
||||
self.timeline.dispatch(&event);
|
||||
}
|
||||
None => break,
|
||||
|
|
@ -1575,8 +1441,6 @@ impl<C: LlmClient> Worker<C, Mutable> {
|
|||
llm_call_end_cbs: Vec::new(),
|
||||
llm_retry_cbs: Vec::new(),
|
||||
llm_continuation_cbs: Vec::new(),
|
||||
stream_event_cbs: Vec::new(),
|
||||
lifecycle_trace_cbs: Vec::new(),
|
||||
warning_cbs: Vec::new(),
|
||||
tool_result_cbs: Vec::new(),
|
||||
history_append_cbs: Vec::new(),
|
||||
|
|
@ -1839,8 +1703,6 @@ impl<C: LlmClient> Worker<C, Mutable> {
|
|||
llm_call_end_cbs: self.llm_call_end_cbs,
|
||||
llm_retry_cbs: self.llm_retry_cbs,
|
||||
llm_continuation_cbs: self.llm_continuation_cbs,
|
||||
stream_event_cbs: self.stream_event_cbs,
|
||||
lifecycle_trace_cbs: self.lifecycle_trace_cbs,
|
||||
warning_cbs: self.warning_cbs,
|
||||
tool_result_cbs: self.tool_result_cbs,
|
||||
history_append_cbs: self.history_append_cbs,
|
||||
|
|
@ -1931,8 +1793,6 @@ impl<C: LlmClient> Worker<C, Locked> {
|
|||
llm_call_end_cbs: self.llm_call_end_cbs,
|
||||
llm_retry_cbs: self.llm_retry_cbs,
|
||||
llm_continuation_cbs: self.llm_continuation_cbs,
|
||||
stream_event_cbs: self.stream_event_cbs,
|
||||
lifecycle_trace_cbs: self.lifecycle_trace_cbs,
|
||||
warning_cbs: self.warning_cbs,
|
||||
tool_result_cbs: self.tool_result_cbs,
|
||||
history_append_cbs: self.history_append_cbs,
|
||||
|
|
@ -1953,127 +1813,7 @@ impl<C: LlmClient> Worker<C, Locked> {
|
|||
}
|
||||
}
|
||||
|
||||
enum FirstStreamEvent {
|
||||
Ready(ResponseStream),
|
||||
Empty(ResponseStream),
|
||||
}
|
||||
|
||||
async fn wait_for_first_stream_event(
|
||||
mut stream: ResponseStream,
|
||||
timeout: std::time::Duration,
|
||||
) -> Result<FirstStreamEvent, ClientError> {
|
||||
match tokio::time::timeout(timeout, stream.next()).await {
|
||||
Ok(Some(first)) => {
|
||||
let first = first?;
|
||||
let stream = futures::stream::once(async move { Ok(first) }).chain(stream);
|
||||
Ok(FirstStreamEvent::Ready(Box::pin(stream)))
|
||||
}
|
||||
Ok(None) => Ok(FirstStreamEvent::Empty(stream)),
|
||||
Err(_) => Err(ClientError::Timeout {
|
||||
phase: "stream_first_event",
|
||||
timeout,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn items_trace_payload(
|
||||
items: &[Item],
|
||||
tools_len: usize,
|
||||
cache_anchor: Option<usize>,
|
||||
cache_key_present: bool,
|
||||
) -> Value {
|
||||
let last = items.last();
|
||||
let last_tool_result = match last {
|
||||
Some(Item::ToolResult {
|
||||
call_id,
|
||||
summary,
|
||||
content,
|
||||
is_error,
|
||||
..
|
||||
}) => {
|
||||
let tool_name = items.iter().rev().find_map(|item| match item {
|
||||
Item::ToolCall {
|
||||
call_id: candidate,
|
||||
name,
|
||||
..
|
||||
} if candidate == call_id => Some(name.as_str()),
|
||||
_ => None,
|
||||
});
|
||||
Some(json!({
|
||||
"call_id": call_id,
|
||||
"tool_name": tool_name,
|
||||
"summary": summary,
|
||||
"summary_bytes": summary.len(),
|
||||
"content_bytes": content.as_ref().map(|s| s.len()).unwrap_or(0),
|
||||
"is_error": is_error,
|
||||
}))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
json!({
|
||||
"items_len": items.len(),
|
||||
"items_json_bytes": serde_json::to_vec(items).map(|bytes| bytes.len()).ok(),
|
||||
"tools_len": tools_len,
|
||||
"cache_anchor": cache_anchor,
|
||||
"cache_key_present": cache_key_present,
|
||||
"last_item_kind": last.map(item_kind),
|
||||
"last_item_json_bytes": last.and_then(|item| serde_json::to_vec(item).ok().map(|bytes| bytes.len())),
|
||||
"last_tool_result": last_tool_result,
|
||||
})
|
||||
}
|
||||
|
||||
fn item_kind(item: &Item) -> &'static str {
|
||||
match item {
|
||||
Item::Message { .. } => "message",
|
||||
Item::ToolCall { .. } => "tool_call",
|
||||
Item::ToolResult { .. } => "tool_result",
|
||||
Item::Reasoning { .. } => "reasoning",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn first_stream_event_timeout_returns_retryable_timeout() {
|
||||
let stream: ResponseStream = Box::pin(futures::stream::pending());
|
||||
let err = match wait_for_first_stream_event(stream, Duration::from_millis(5)).await {
|
||||
Ok(_) => panic!("expected first event timeout"),
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
assert!(is_retryable(&err));
|
||||
assert!(matches!(
|
||||
err,
|
||||
ClientError::Timeout {
|
||||
phase: "stream_first_event",
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn first_stream_event_is_replayed_after_probe() {
|
||||
let first = Event::Status(crate::llm_client::event::StatusEvent {
|
||||
status: crate::llm_client::event::ResponseStatus::Started,
|
||||
});
|
||||
let stream: ResponseStream = Box::pin(futures::stream::once({
|
||||
let first = first.clone();
|
||||
async move { Ok(first) }
|
||||
}));
|
||||
|
||||
let FirstStreamEvent::Ready(mut stream) =
|
||||
wait_for_first_stream_event(stream, Duration::from_secs(1))
|
||||
.await
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("expected first event to be buffered");
|
||||
};
|
||||
|
||||
let replayed = stream.next().await.unwrap().unwrap();
|
||||
assert_eq!(replayed, first);
|
||||
}
|
||||
// Basic tests only. Tests using LlmClient are done in integration tests.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
//! Pod manifests are assembled from up to three on-disk layers (see
|
||||
//! `pod::PodFactory` for the full cascade story):
|
||||
//!
|
||||
//! 1. **User manifest** — Pod CLI uses
|
||||
//! [`crate::paths::user_manifest_path_with_env_override`]
|
||||
//! 1. **User manifest** — see [`crate::paths::user_manifest_path`]
|
||||
//! 2. **Project manifest** at the closest `.insomnia/manifest.toml`
|
||||
//! found by walking up from a starting directory (typically `cwd`)
|
||||
//! 3. **Programmatic overlay** supplied at the call site
|
||||
|
|
|
|||
|
|
@ -17,8 +17,7 @@ use crate::defaults;
|
|||
use crate::model::{AuthRef, ModelManifest, ReasoningControl};
|
||||
use crate::{
|
||||
CompactionConfig, FileUploadLimits, MemoryConfig, PodManifest, PodMeta, ScopeConfig,
|
||||
SessionConfig, SkillsConfig, ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule,
|
||||
WorkerManifest,
|
||||
SkillsConfig, ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule, WorkerManifest,
|
||||
};
|
||||
|
||||
/// Partial-form Pod manifest. Every field is optional; one or more
|
||||
|
|
@ -38,8 +37,6 @@ pub struct PodManifestConfig {
|
|||
pub worker: WorkerManifestConfig,
|
||||
#[serde(default)]
|
||||
pub scope: ScopeConfig,
|
||||
#[serde(default)]
|
||||
pub session: Option<SessionConfigPartial>,
|
||||
/// Optional `[permissions]` section. `None` means the permission layer
|
||||
/// is disabled; `Some` requires `default_action` during final resolve.
|
||||
#[serde(default)]
|
||||
|
|
@ -105,12 +102,6 @@ pub struct FileUploadLimitsPartial {
|
|||
pub max_bytes: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct SessionConfigPartial {
|
||||
#[serde(default)]
|
||||
pub record_event_trace: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct PermissionConfigPartial {
|
||||
#[serde(default)]
|
||||
|
|
@ -269,7 +260,6 @@ impl PodManifestConfig {
|
|||
model: self.model.merge(upper.model),
|
||||
worker: self.worker.merge(upper.worker),
|
||||
scope: merge_scope(self.scope, upper.scope),
|
||||
session: merge_option(self.session, upper.session, SessionConfigPartial::merge),
|
||||
permissions: merge_option(
|
||||
self.permissions,
|
||||
upper.permissions,
|
||||
|
|
@ -299,7 +289,6 @@ impl MemoryConfig {
|
|||
workspace_root: upper.workspace_root.or(self.workspace_root),
|
||||
query_result_limit: upper.query_result_limit.or(self.query_result_limit),
|
||||
query_excerpt_lines: upper.query_excerpt_lines.or(self.query_excerpt_lines),
|
||||
inject_summary: upper.inject_summary.or(self.inject_summary),
|
||||
language: upper.language.or(self.language),
|
||||
extract_model: upper.extract_model.or(self.extract_model),
|
||||
extract_threshold: upper.extract_threshold.or(self.extract_threshold),
|
||||
|
|
@ -363,14 +352,6 @@ impl FileUploadLimitsPartial {
|
|||
}
|
||||
}
|
||||
|
||||
impl SessionConfigPartial {
|
||||
fn merge(self, upper: Self) -> Self {
|
||||
Self {
|
||||
record_event_trace: upper.record_event_trace.or(self.record_event_trace),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PermissionConfigPartial {
|
||||
fn merge(mut self, upper: Self) -> Self {
|
||||
self.rules.extend(upper.rules);
|
||||
|
|
@ -514,12 +495,6 @@ impl TryFrom<PodManifestConfig> for PodManifest {
|
|||
for rule in &cfg.scope.deny {
|
||||
ensure_absolute("scope.deny.target", &rule.target)?;
|
||||
}
|
||||
let session = SessionConfig {
|
||||
record_event_trace: cfg
|
||||
.session
|
||||
.and_then(|s| s.record_event_trace)
|
||||
.unwrap_or(false),
|
||||
};
|
||||
|
||||
let permissions = cfg
|
||||
.permissions
|
||||
|
|
@ -574,7 +549,6 @@ impl TryFrom<PodManifestConfig> for PodManifest {
|
|||
model: cfg.model,
|
||||
worker,
|
||||
scope: cfg.scope,
|
||||
session,
|
||||
permissions,
|
||||
compaction,
|
||||
memory: cfg.memory,
|
||||
|
|
@ -621,7 +595,6 @@ mod tests {
|
|||
deny: Vec::new(),
|
||||
},
|
||||
permissions: None,
|
||||
session: None,
|
||||
compaction: None,
|
||||
memory: None,
|
||||
skills: None,
|
||||
|
|
@ -636,17 +609,6 @@ mod tests {
|
|||
assert!(manifest.permissions.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_session_record_event_trace() {
|
||||
let mut cfg = minimal_valid();
|
||||
cfg.session = Some(SessionConfigPartial {
|
||||
record_event_trace: Some(true),
|
||||
});
|
||||
|
||||
let manifest: PodManifest = cfg.try_into().unwrap();
|
||||
assert!(manifest.session.record_event_trace);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_permissions_requires_default_action_when_present() {
|
||||
let mut cfg = minimal_valid();
|
||||
|
|
|
|||
|
|
@ -13,9 +13,7 @@ pub use config::{
|
|||
pub use model::{
|
||||
AuthRef, ModelCapability, ModelManifest, ReasoningControl, ReasoningEffort, SchemeKind,
|
||||
};
|
||||
pub use paths::{
|
||||
user_manifest_path, user_manifest_path_from_env, user_manifest_path_with_env_override,
|
||||
};
|
||||
pub use paths::user_manifest_path;
|
||||
pub use protocol::{Permission, ScopeRule};
|
||||
pub use scope::{Scope, ScopeError, SharedScope};
|
||||
|
||||
|
|
@ -37,9 +35,6 @@ pub struct PodManifest {
|
|||
pub model: ModelManifest,
|
||||
pub worker: WorkerManifest,
|
||||
pub scope: ScopeConfig,
|
||||
/// Session/debug persistence settings. Defaults keep extra traces off.
|
||||
#[serde(default)]
|
||||
pub session: SessionConfig,
|
||||
/// Optional manifest-level tool permission policy. Absent means the
|
||||
/// permission layer is disabled and tool calls run as before.
|
||||
#[serde(default)]
|
||||
|
|
@ -101,10 +96,6 @@ pub struct MemoryConfig {
|
|||
/// Ignored when the request omits `query`. `None` ⇒ tool default (3).
|
||||
#[serde(default)]
|
||||
pub query_excerpt_lines: Option<usize>,
|
||||
/// Whether the body of `memory/summary.md` is exposed in the resident
|
||||
/// system-prompt section. `None` ⇒ enabled.
|
||||
#[serde(default)]
|
||||
pub inject_summary: Option<bool>,
|
||||
/// Language used by memory extraction / consolidation workers for durable
|
||||
/// memory and knowledge text. Free-form so workspaces can use names like
|
||||
/// `English`, `Japanese`, or locale tags. `None` ⇒
|
||||
|
|
@ -303,15 +294,6 @@ pub struct ScopeConfig {
|
|||
pub deny: Vec<ScopeRule>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub struct SessionConfig {
|
||||
/// Persist every provider stream event directly to `trace.jsonl` next to the
|
||||
/// segment log. Intended for debugging stalls between stream requests; off
|
||||
/// by default because it can be verbose.
|
||||
#[serde(default)]
|
||||
pub record_event_trace: bool,
|
||||
}
|
||||
|
||||
/// Manifest-level pattern-based tool permission policy.
|
||||
///
|
||||
/// Presence of `[permissions]` enables this layer. Rules are evaluated
|
||||
|
|
@ -687,15 +669,6 @@ model_id = "claude-sonnet-4-20250514"
|
|||
let manifest = PodManifest::from_toml(&toml).unwrap();
|
||||
let mem = manifest.memory.expect("memory section parsed");
|
||||
assert!(mem.workspace_root.is_none());
|
||||
assert_eq!(mem.inject_summary, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_section_with_inject_summary_false() {
|
||||
let toml = format!("{MINIMAL_REQUIRED}\n[memory]\ninject_summary = false\n");
|
||||
let manifest = PodManifest::from_toml(&toml).unwrap();
|
||||
let mem = manifest.memory.unwrap();
|
||||
assert_eq!(mem.inject_summary, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -23,16 +23,8 @@
|
|||
//! 解決された各 base が存在するか / ディレクトリかは保証しない —
|
||||
//! 呼び出し側がファイル操作の前に作成 / 検査する。
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Environment variable that points at an explicit user manifest.
|
||||
///
|
||||
/// Pod CLI treats a non-empty value as an explicit manifest path. Empty values
|
||||
/// are treated the same as an unset variable, so callers fall back to the
|
||||
/// auto-discovered user manifest path.
|
||||
pub const USER_MANIFEST_ENV: &str = "INSOMNIA_USER_MANIFEST";
|
||||
|
||||
/// 設定ディレクトリ。`manifest.toml`, `providers.toml`, `models.toml`,
|
||||
/// `prompts/` などが置かれる。
|
||||
pub fn config_dir() -> Option<PathBuf> {
|
||||
|
|
@ -77,38 +69,11 @@ pub fn runtime_dir() -> Option<PathBuf> {
|
|||
|
||||
// ---- well-known file getters ------------------------------------------------
|
||||
|
||||
/// `<config_dir>/manifest.toml` — user manifest の既定位置。
|
||||
///
|
||||
/// This deliberately ignores [`USER_MANIFEST_ENV`]. Use
|
||||
/// [`user_manifest_path_with_env_override`] when mirroring the Pod CLI cascade
|
||||
/// resolution rules.
|
||||
/// `<config_dir>/manifest.toml` — user manifest。
|
||||
pub fn user_manifest_path() -> Option<PathBuf> {
|
||||
Some(config_dir()?.join("manifest.toml"))
|
||||
}
|
||||
|
||||
/// Resolve an explicit user manifest override from an env value.
|
||||
///
|
||||
/// Non-empty values are paths. `None` and empty strings are both treated as no
|
||||
/// override, matching the Pod CLI's `INSOMNIA_USER_MANIFEST` handling.
|
||||
pub fn user_manifest_path_from_env(value: Option<OsString>) -> Option<PathBuf> {
|
||||
value.and_then(|value| {
|
||||
if value.as_os_str().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(PathBuf::from(value))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// User manifest path using the same env override rule as the Pod CLI cascade.
|
||||
///
|
||||
/// A non-empty [`USER_MANIFEST_ENV`] value wins. If the variable is unset or
|
||||
/// empty, this falls back to [`user_manifest_path`]. The returned path is not
|
||||
/// guaranteed to exist.
|
||||
pub fn user_manifest_path_with_env_override() -> Option<PathBuf> {
|
||||
user_manifest_path_from_env(std::env::var_os(USER_MANIFEST_ENV)).or_else(user_manifest_path)
|
||||
}
|
||||
|
||||
/// `<config_dir>/prompts/` — user prompts ライブラリ。
|
||||
pub fn user_prompts_dir() -> Option<PathBuf> {
|
||||
Some(config_dir()?.join("prompts"))
|
||||
|
|
@ -191,7 +156,6 @@ mod tests {
|
|||
"INSOMNIA_CONFIG_DIR",
|
||||
"INSOMNIA_DATA_DIR",
|
||||
"INSOMNIA_RUNTIME_DIR",
|
||||
"INSOMNIA_USER_MANIFEST",
|
||||
"INSOMNIA_HOME",
|
||||
"XDG_CONFIG_HOME",
|
||||
"XDG_RUNTIME_DIR",
|
||||
|
|
@ -283,7 +247,7 @@ mod tests {
|
|||
]);
|
||||
assert_eq!(
|
||||
runtime_dir().unwrap(),
|
||||
PathBuf::from("<runtime-dir>")
|
||||
PathBuf::from("/run/user/1000/insomnia")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -317,37 +281,6 @@ mod tests {
|
|||
assert!(runtime_dir().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_manifest_env_override_wins_when_non_empty() {
|
||||
let _g = EnvGuard::new(&[
|
||||
("HOME", Some("/h")),
|
||||
("INSOMNIA_USER_MANIFEST", Some("/tmp/user.toml")),
|
||||
]);
|
||||
assert_eq!(
|
||||
user_manifest_path_with_env_override().unwrap(),
|
||||
PathBuf::from("/tmp/user.toml")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_user_manifest_env_falls_back_to_default_path() {
|
||||
let _g = EnvGuard::new(&[("HOME", Some("/h")), ("INSOMNIA_USER_MANIFEST", Some(""))]);
|
||||
assert_eq!(
|
||||
user_manifest_path_with_env_override().unwrap(),
|
||||
PathBuf::from("/h/.config/insomnia/manifest.toml")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_manifest_path_from_env_treats_empty_as_unset() {
|
||||
assert_eq!(user_manifest_path_from_env(None), None);
|
||||
assert_eq!(user_manifest_path_from_env(Some(OsString::from(""))), None);
|
||||
assert_eq!(
|
||||
user_manifest_path_from_env(Some(OsString::from("/tmp/u.toml"))).unwrap(),
|
||||
PathBuf::from("/tmp/u.toml")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn well_known_files_compose_off_base_dirs() {
|
||||
let _g = EnvGuard::new(&[("INSOMNIA_HOME", Some("/sand"))]);
|
||||
|
|
|
|||
|
|
@ -150,26 +150,13 @@ mod tests {
|
|||
let layout = WorkspaceLayout::new(tmp.path().to_path_buf());
|
||||
let (_id, _) = write_staging(&layout, source("s", [0, 1]), empty_payload()).unwrap();
|
||||
|
||||
// Drop a non-UUID json file, an unparsable UUID-named json file, an
|
||||
// old-schema UUID-named json file, and a bare lock file alongside.
|
||||
// Lock files are not `.json`; invalid `.json` files are surfaced
|
||||
// separately instead of being mistaken for an empty staging directory.
|
||||
// Drop a non-UUID json file, an unparsable UUID-named json file, and
|
||||
// a bare lock file alongside. Lock files are not `.json`; invalid
|
||||
// `.json` files are surfaced separately instead of being mistaken for
|
||||
// an empty staging directory.
|
||||
std::fs::write(layout.staging_dir().join("not-a-uuid.json"), "{}").unwrap();
|
||||
let bad_id = Uuid::now_v7();
|
||||
std::fs::write(layout.staging_dir().join(format!("{bad_id}.json")), "{").unwrap();
|
||||
let old_schema_id = Uuid::now_v7();
|
||||
std::fs::write(
|
||||
layout.staging_dir().join(format!("{old_schema_id}.json")),
|
||||
serde_json::json!({
|
||||
"source": {
|
||||
"session_id": "legacy-session",
|
||||
"range": [0, 1]
|
||||
},
|
||||
"requests": []
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(layout.staging_dir().join(".consolidation.lock"), "{}").unwrap();
|
||||
|
||||
let entries = list_staging_entries(&layout);
|
||||
|
|
@ -177,7 +164,7 @@ mod tests {
|
|||
|
||||
let snapshot = list_staging_entries_snapshot(&layout);
|
||||
assert_eq!(snapshot.entries.len(), 1);
|
||||
assert_eq!(snapshot.invalid_count, 3);
|
||||
assert_eq!(snapshot.invalid_count, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -22,10 +22,7 @@ pub use error::{LintError, LintWarning, MemoryError};
|
|||
pub use extract::ExtractPointerPayload;
|
||||
pub use lint_common::{RecordLintError, Slug, is_valid_slug};
|
||||
pub use linter::{LintReport, Linter};
|
||||
pub use resident::{
|
||||
ResidentKnowledgeEntry, collect_resident_knowledge, collect_resident_summary,
|
||||
list_knowledge_slugs,
|
||||
};
|
||||
pub use resident::{ResidentKnowledgeEntry, collect_resident_knowledge, list_knowledge_slugs};
|
||||
pub use scope::deny_write_rules;
|
||||
pub use usage::{
|
||||
UsageEvent, UsageEventKind, UsageRecordSnapshot, UsageReport, UsageReportRecord, UsageSource,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
//! Workspace memory resident-enumeration helpers.
|
||||
//! Workspace knowledge enumeration helpers.
|
||||
//!
|
||||
//! Surfaces used by the Pod system-prompt assembler:
|
||||
//! Two surfaces, both walking `<workspace>/.insomnia/knowledge/*.md`:
|
||||
//!
|
||||
//! - [`collect_resident_knowledge`] — resident-injection candidates
|
||||
//! (`model_invokation: true`) returned as `(slug, description)` pairs.
|
||||
//! - [`collect_resident_summary`] — the body of
|
||||
//! `<workspace>/.insomnia/memory/summary.md` when it parses as a summary
|
||||
//! record and has non-empty body.
|
||||
//! (`model_invokation: true`) returned as `(slug, description)` pairs
|
||||
//! for the Pod system-prompt assembler.
|
||||
//! - [`list_knowledge_slugs`] — every slug whose file parses, regardless
|
||||
//! of `model_invokation`. Used by the Pod IPC layer to answer TUI `#`
|
||||
//! completion (`model_invokation` is a resident-injection flag, not a
|
||||
|
|
@ -16,7 +14,7 @@
|
|||
//! enforces shape on write, so a malformed file here means external
|
||||
//! tampering and we'd rather degrade than panic.
|
||||
|
||||
use crate::schema::{KnowledgeFrontmatter, SummaryFrontmatter, split_frontmatter};
|
||||
use crate::schema::{KnowledgeFrontmatter, split_frontmatter};
|
||||
use crate::workspace::WorkspaceLayout;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -42,21 +40,6 @@ pub fn collect_resident_knowledge(layout: &WorkspaceLayout) -> Vec<ResidentKnowl
|
|||
out
|
||||
}
|
||||
|
||||
/// Read `<workspace>/.insomnia/memory/summary.md` for resident prompt
|
||||
/// injection. Returns only the markdown body (frontmatter stripped), and
|
||||
/// degrades to `None` for missing, unreadable, malformed, or empty records.
|
||||
pub fn collect_resident_summary(layout: &WorkspaceLayout) -> Option<String> {
|
||||
let raw = std::fs::read_to_string(layout.summary_path()).ok()?;
|
||||
let (yaml, body) = split_frontmatter(&raw).ok()?;
|
||||
let _fm: SummaryFrontmatter = serde_yaml::from_str(yaml).ok()?;
|
||||
let body = body.trim_matches(&['\n', '\r'][..]);
|
||||
if body.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(body.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk `<workspace>/knowledge/*.md` and return every slug whose
|
||||
/// frontmatter parses, sorted ascending. Does not filter on
|
||||
/// `model_invokation`. A missing `knowledge/` directory yields an empty
|
||||
|
|
@ -114,12 +97,6 @@ mod tests {
|
|||
Utc::now().to_rfc3339()
|
||||
}
|
||||
|
||||
fn write_summary(dir: &Path, body: &str) {
|
||||
let path = dir.join(".insomnia/memory/summary.md");
|
||||
let content = format!("---\nupdated_at: {n}\n---\n{body}", n = now());
|
||||
std::fs::write(path, content).unwrap();
|
||||
}
|
||||
|
||||
fn write_knowledge(
|
||||
dir: &Path,
|
||||
slug: &str,
|
||||
|
|
@ -139,48 +116,10 @@ mod tests {
|
|||
fn setup() -> (TempDir, WorkspaceLayout) {
|
||||
let dir = TempDir::new().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".insomnia/knowledge")).unwrap();
|
||||
std::fs::create_dir_all(dir.path().join(".insomnia/memory")).unwrap();
|
||||
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
|
||||
(dir, layout)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_summary_returns_none() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
|
||||
assert!(collect_resident_summary(&layout).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_returns_body_without_frontmatter() {
|
||||
let (dir, layout) = setup();
|
||||
write_summary(dir.path(), "remember this\n");
|
||||
|
||||
let got = collect_resident_summary(&layout).unwrap();
|
||||
assert_eq!(got, "remember this");
|
||||
assert!(!got.contains("updated_at"));
|
||||
assert!(!got.contains("---"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_summary_returns_none() {
|
||||
let (dir, layout) = setup();
|
||||
std::fs::write(
|
||||
dir.path().join(".insomnia/memory/summary.md"),
|
||||
"---\nthis is not yaml: : :\n---\nbody\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(collect_resident_summary(&layout).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_summary_body_returns_none() {
|
||||
let (dir, layout) = setup();
|
||||
write_summary(dir.path(), " \n");
|
||||
assert!(collect_resident_summary(&layout).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_knowledge_dir_returns_empty() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
|
|
|||
|
|
@ -10,10 +10,6 @@
|
|||
//!
|
||||
//! - ローカルトークナイザは持たない。実測値があればそれを採用し、
|
||||
//! measurement 間はバイト数で按分、最新 measurement より先は最終 rate で外挿する
|
||||
//! - Compact の retained split では、request-time pruning / projection 後の
|
||||
//! `UsageRecord` を persisted history prefix の単調系列として扱わない。
|
||||
//! 現在の prompt occupancy 推定を raw serialized bytes に配分し、末尾の
|
||||
//! persisted tail サイズで cut を決める。
|
||||
//! - 推定の出どころは [`EstimateSource`] で呼び出し側に明示する。
|
||||
//! 課金判断には使えないが、compact / prune の閾値判定には十分な精度
|
||||
|
||||
|
|
@ -44,61 +40,26 @@ fn split_for_retained_impl(history: &[Item], records: &[UsageRecord], retained:
|
|||
source: current.source,
|
||||
};
|
||||
}
|
||||
let target = current.tokens - retained;
|
||||
|
||||
let cut_index = split_index_by_retained_bytes(&prefix, current.tokens, retained);
|
||||
SplitPoint {
|
||||
index: balance_to_pair_boundary(history, cut_index),
|
||||
source: current.source,
|
||||
}
|
||||
}
|
||||
|
||||
fn split_index_by_retained_bytes(prefix: &[u64], total_tokens: u64, retained_tokens: u64) -> usize {
|
||||
debug_assert!(!prefix.is_empty());
|
||||
|
||||
let len = prefix.len() - 1;
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
if retained_tokens == 0 {
|
||||
return len;
|
||||
}
|
||||
|
||||
let total_bytes = *prefix.last().unwrap_or(&0);
|
||||
if total_bytes == 0 || total_tokens == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let raw_fallback_tokens = ceil_div_u128(total_bytes as u128, 4) as u64;
|
||||
let rate_tokens = total_tokens.max(raw_fallback_tokens);
|
||||
let target_retained_bytes = ceil_div_u128(
|
||||
retained_tokens as u128 * total_bytes as u128,
|
||||
rate_tokens as u128,
|
||||
)
|
||||
.min(total_bytes as u128) as u64;
|
||||
|
||||
// Drop as many complete Items as possible while keeping the raw persisted
|
||||
// suffix at or above the retained budget. This is monotonic in serialized
|
||||
// history size and intentionally does not inspect per-history_len
|
||||
// UsageRecords: request-time usage can move up and down after pruning /
|
||||
// projection, so it is not a valid prefix series for retained split. The
|
||||
// byte/4 fallback is kept as a lower bound for raw persisted size so a
|
||||
// heavily-pruned request measurement cannot justify retaining megabytes of
|
||||
// history.
|
||||
let mut cut = 0;
|
||||
for (idx, bytes_before) in prefix.iter().enumerate().take(len + 1) {
|
||||
let suffix_bytes = total_bytes.saturating_sub(*bytes_before);
|
||||
if suffix_bytes >= target_retained_bytes {
|
||||
cut = idx;
|
||||
} else {
|
||||
// `tokens_at` が target 以上になる最小の idx を線形探索。
|
||||
// prefix を使い回すので 1 回の split 呼び出しあたり O(n) で済む
|
||||
// (内部で毎回再計算すると O(n²) になる)。将来ボトルネックになれば
|
||||
// record 境界で二分探索に置き換える。
|
||||
let mut chosen_source = current.source;
|
||||
let mut cut_index = history.len();
|
||||
for idx in 1..=history.len() {
|
||||
let est = tokens_at(history, records, idx, &prefix);
|
||||
if est.tokens >= target {
|
||||
chosen_source = est.source;
|
||||
cut_index = idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cut
|
||||
}
|
||||
|
||||
fn ceil_div_u128(n: u128, d: u128) -> u128 {
|
||||
debug_assert!(d > 0);
|
||||
if n == 0 { 0 } else { ((n - 1) / d) + 1 }
|
||||
SplitPoint {
|
||||
index: balance_to_pair_boundary(history, cut_index),
|
||||
source: chosen_source,
|
||||
}
|
||||
}
|
||||
|
||||
/// `history[cut..]` が `ToolCall` / `ToolResult` のペア境界を尊重するよう
|
||||
|
|
@ -298,44 +259,23 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn split_uses_current_occupancy_as_raw_byte_rate() {
|
||||
// Compact retained split does not treat the intermediate record at
|
||||
// len=2 as a raw prefix boundary. It uses the current occupancy
|
||||
// estimate (len=4 → 300) as a serialized-byte rate and keeps the
|
||||
// smallest item-granular suffix whose raw size covers retained=200.
|
||||
fn split_at_exact_measurement_boundary() {
|
||||
// 4 items。measurements: len=2 → 100, len=4 → 300。
|
||||
// retained=200 → target_drop = 100 → record[0] にぴったり一致 → index=2。
|
||||
let history = vec![msg("a"), msg("b"), msg("c"), msg("d")];
|
||||
let records = vec![record(2, 100), record(4, 300)];
|
||||
let cut = split_for_retained_impl(&history, &records, 200);
|
||||
assert_eq!(cut.index, 1);
|
||||
assert_eq!(cut.index, 2);
|
||||
assert_eq!(cut.source, EstimateSource::Measured);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_does_not_use_non_current_measurements_as_cut_boundaries() {
|
||||
fn split_interpolated_between_measurements() {
|
||||
let history = vec![msg("aaaaaa"), msg("bbbbbb"), msg("cccccc"), msg("dddddd")];
|
||||
let records = vec![record(1, 50), record(4, 400)];
|
||||
let cut = split_for_retained_impl(&history, &records, 250);
|
||||
assert_eq!(cut.index, 1);
|
||||
assert_eq!(cut.source, EstimateSource::Measured);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_ignores_non_monotonic_usage_spike_for_retained_tail() {
|
||||
let history: Vec<Item> = (0..20)
|
||||
.map(|idx| msg(&format!("message-{idx}-{}", "x".repeat(100))))
|
||||
.collect();
|
||||
let records = vec![
|
||||
record(2, 900), // request-time spike after pruning/projection
|
||||
record(20, 1000),
|
||||
];
|
||||
let cut = split_for_retained_impl(&history, &records, 100);
|
||||
|
||||
// The old prefix-crossing logic picked index 2 because 900 >=
|
||||
// 1000-100, retaining almost the whole persisted history. The compact
|
||||
// split must instead use raw suffix size and keep only the tail needed
|
||||
// for the retained budget.
|
||||
assert!(cut.index > 10, "cut.index = {}", cut.index);
|
||||
assert_eq!(cut.source, EstimateSource::Measured);
|
||||
assert!(cut.index > 1 && cut.index <= 4);
|
||||
assert_eq!(cut.source, EstimateSource::Interpolated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -647,7 +647,6 @@ permission = "write"
|
|||
scope: &scope,
|
||||
tool_names: Vec::new(),
|
||||
agents_md: None,
|
||||
resident_summary: None,
|
||||
resident_knowledge: None,
|
||||
resident_workflows: None,
|
||||
prompts: &catalog,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ use manifest::{PodManifest, PodManifestConfig, paths};
|
|||
use pod::{Pod, PodController, PodFactory, PromptLoader};
|
||||
use session_store::{FsStore, PodMetadataStore, SegmentId, Store};
|
||||
|
||||
const USER_MANIFEST_ENV: &str = "INSOMNIA_USER_MANIFEST";
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
name = "pod",
|
||||
|
|
@ -66,20 +68,19 @@ struct Cli {
|
|||
}
|
||||
|
||||
fn resolve_manifest(cli: &Cli) -> Result<(PodManifest, PromptLoader), String> {
|
||||
resolve_manifest_with_user_manifest_env(cli, std::env::var_os(paths::USER_MANIFEST_ENV))
|
||||
resolve_manifest_with_user_manifest_env(cli, std::env::var_os(USER_MANIFEST_ENV))
|
||||
}
|
||||
|
||||
fn resolve_manifest_with_user_manifest_env(
|
||||
cli: &Cli,
|
||||
user_manifest_env: Option<OsString>,
|
||||
) -> Result<(PodManifest, PromptLoader), String> {
|
||||
let user_manifest = paths::user_manifest_path_from_env(user_manifest_env);
|
||||
let user_manifest = user_manifest_path_from_env(user_manifest_env);
|
||||
|
||||
if let Some(path) = &cli.manifest {
|
||||
if user_manifest.is_some() {
|
||||
return Err(format!(
|
||||
"--manifest cannot be used when {} is set",
|
||||
paths::USER_MANIFEST_ENV
|
||||
"--manifest cannot be used when {USER_MANIFEST_ENV} is set"
|
||||
));
|
||||
}
|
||||
return load_single_manifest(path, cli.pod.as_deref());
|
||||
|
|
@ -91,6 +92,16 @@ fn resolve_manifest_with_user_manifest_env(
|
|||
.map_err(|e| format!("failed to resolve manifest cascade: {e}"))
|
||||
}
|
||||
|
||||
fn user_manifest_path_from_env(value: Option<OsString>) -> Option<PathBuf> {
|
||||
value.and_then(|value| {
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(PathBuf::from(value))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn load_single_manifest(
|
||||
path: &Path,
|
||||
pod_name_override: Option<&str>,
|
||||
|
|
@ -397,7 +408,7 @@ permission = "write"
|
|||
.unwrap_err();
|
||||
|
||||
assert!(err.contains("--manifest cannot be used"));
|
||||
assert!(err.contains(paths::USER_MANIFEST_ENV));
|
||||
assert!(err.contains(USER_MANIFEST_ENV));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -167,15 +167,6 @@ where
|
|||
self.sink.publish(entry);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Append a debug trace record alongside the current segment log. Trace
|
||||
/// writes deliberately do not affect the segment entry counter or live
|
||||
/// replay sink because they are not conversation history.
|
||||
pub fn append_trace(&self, entry: &session_store::TraceEntry) -> Result<(), StoreError> {
|
||||
let loc = self.state.location();
|
||||
self.store
|
||||
.append_trace(loc.session_id, loc.segment_id, entry)
|
||||
}
|
||||
}
|
||||
|
||||
/// Type-erased commit handle for the interceptor. Lets the
|
||||
|
|
@ -323,18 +314,12 @@ pub struct Pod<C: LlmClient, St: Store> {
|
|||
/// Memory workspace layout used by the workflow resolver to load required
|
||||
/// Knowledge records by exact slug.
|
||||
memory_layout: Option<memory::WorkspaceLayout>,
|
||||
/// When true (default), the system-prompt assembler may append the
|
||||
/// workspace memory summary (`memory/summary.md`). Internal disposable
|
||||
/// workers disable this so resident memory exposure is opt-in per Pod.
|
||||
inject_resident_summary: bool,
|
||||
/// When true (default), the system-prompt assembler may append resident
|
||||
/// Knowledge descriptions. This is intentionally independent from
|
||||
/// summary and workflow residency: each section has its own gate.
|
||||
/// When true (default), the system-prompt assembler walks
|
||||
/// `<workspace>/knowledge/*` and appends a `## Resident knowledge`
|
||||
/// section listing records with `model_invokation: true`.
|
||||
/// consolidation workers set this to false so the
|
||||
/// agentic worker pulls knowledge through the search tools instead.
|
||||
inject_resident_knowledge: bool,
|
||||
/// When true (default), the system-prompt assembler may append resident
|
||||
/// Workflow descriptions. This is intentionally independent from
|
||||
/// summary and Knowledge residency: each section has its own gate.
|
||||
inject_resident_workflows: bool,
|
||||
/// Latest runtime scope snapshot queued by dynamic scope changes.
|
||||
/// Drained into the session log before the next turn result is
|
||||
/// persisted, so resume never silently reclaims delegated writes.
|
||||
|
|
@ -440,9 +425,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
|
|||
prompts: self.prompts.clone(),
|
||||
workflow_registry: self.workflow_registry.clone(),
|
||||
memory_layout: self.memory_layout.clone(),
|
||||
inject_resident_summary: self.inject_resident_summary,
|
||||
inject_resident_knowledge: self.inject_resident_knowledge,
|
||||
inject_resident_workflows: self.inject_resident_workflows,
|
||||
pending_scope_snapshot: self.pending_scope_snapshot.clone(),
|
||||
extract_in_flight: self.extract_in_flight.clone(),
|
||||
consolidation_in_flight: self.consolidation_in_flight.clone(),
|
||||
|
|
@ -511,39 +494,6 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
|
|||
warn!(error = %err, "history append commit failed; dropping");
|
||||
}
|
||||
});
|
||||
if self.manifest.session.record_event_trace {
|
||||
let writer = self.log_writer_handle();
|
||||
self.worker_mut()
|
||||
.on_stream_event(move |turn, llm_call, event| {
|
||||
let entry = session_store::TraceEntry {
|
||||
ts: segment_log::now_millis(),
|
||||
turn,
|
||||
llm_call: Some(llm_call),
|
||||
payload: session_store::TracePayload::StreamEvent {
|
||||
event: event.clone(),
|
||||
},
|
||||
};
|
||||
if let Err(err) = writer.append_trace(&entry) {
|
||||
warn!(error = %err, "stream event trace commit failed; dropping");
|
||||
}
|
||||
});
|
||||
let writer = self.log_writer_handle();
|
||||
self.worker_mut()
|
||||
.on_lifecycle_trace(move |turn, llm_call, label, data| {
|
||||
let entry = session_store::TraceEntry {
|
||||
ts: segment_log::now_millis(),
|
||||
turn,
|
||||
llm_call: Some(llm_call),
|
||||
payload: session_store::TracePayload::Lifecycle {
|
||||
label: label.to_string(),
|
||||
data: data.clone(),
|
||||
},
|
||||
};
|
||||
if let Err(err) = writer.append_trace(&entry) {
|
||||
warn!(error = %err, "lifecycle trace commit failed; dropping");
|
||||
}
|
||||
});
|
||||
}
|
||||
self.history_persistence_wired = true;
|
||||
}
|
||||
|
||||
|
|
@ -619,9 +569,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
prompts,
|
||||
workflow_registry: workflow_crate::WorkflowRegistry::empty(),
|
||||
memory_layout: None,
|
||||
inject_resident_summary: true,
|
||||
inject_resident_knowledge: true,
|
||||
inject_resident_workflows: true,
|
||||
pending_scope_snapshot: Arc::new(Mutex::new(None)),
|
||||
extract_in_flight: Arc::new(AtomicBool::new(false)),
|
||||
consolidation_in_flight: Arc::new(AtomicBool::new(false)),
|
||||
|
|
@ -645,33 +593,20 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
self.system_prompt_template = Some(template);
|
||||
}
|
||||
|
||||
/// Toggle all resident sections in the system prompt.
|
||||
/// Toggle the resident-knowledge section of the system prompt.
|
||||
///
|
||||
/// Default `true`: normal Pods may expose each resident section according
|
||||
/// to its own gate and manifest settings. Internal disposable workers set
|
||||
/// this to `false` so summary, Knowledge, and Workflow residency are all
|
||||
/// suppressed while explicit tools remain available.
|
||||
pub fn set_resident_injection(&mut self, enabled: bool) {
|
||||
self.inject_resident_summary = enabled;
|
||||
self.inject_resident_knowledge = enabled;
|
||||
self.inject_resident_workflows = enabled;
|
||||
}
|
||||
|
||||
/// Toggle `memory/summary.md` resident injection in the system prompt.
|
||||
pub fn set_resident_summary_injection(&mut self, enabled: bool) {
|
||||
self.inject_resident_summary = enabled;
|
||||
}
|
||||
|
||||
/// Toggle resident Knowledge injection in the system prompt.
|
||||
/// Default `true`: when memory is enabled in the manifest, the
|
||||
/// assembler walks `<workspace>/knowledge/*` and lists records with
|
||||
/// `model_invokation: true`. consolidation workers and
|
||||
/// other agentic memory paths set this to `false` so the worker
|
||||
/// pulls knowledge through the search tools instead of riding on
|
||||
/// the resident system-prompt budget. Idempotent if called multiple
|
||||
/// times before the first turn; ineffective once the system prompt
|
||||
/// has been materialised.
|
||||
pub fn set_resident_knowledge_injection(&mut self, enabled: bool) {
|
||||
self.inject_resident_knowledge = enabled;
|
||||
}
|
||||
|
||||
/// Toggle resident Workflow injection in the system prompt.
|
||||
pub fn set_resident_workflow_injection(&mut self, enabled: bool) {
|
||||
self.inject_resident_workflows = enabled;
|
||||
}
|
||||
|
||||
/// Shared handle to the prompt catalog. Cheap to clone (`Arc`).
|
||||
pub fn prompts(&self) -> &Arc<PromptCatalog> {
|
||||
&self.prompts
|
||||
|
|
@ -1224,48 +1159,32 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
n.alert(AlertLevel::Warn, AlertSource::AgentsMd, warning);
|
||||
}
|
||||
}
|
||||
// Resident-injection collection. Each resident section has its own
|
||||
// gate so summary, Knowledge, and Workflow residency remain
|
||||
// conceptually independent. Internal workers can still opt out of all
|
||||
// resident sections by flipping all three gates.
|
||||
// Owned values live for the duration of `render` below; the
|
||||
// context borrows from them.
|
||||
let memory_layout = self.memory_layout.as_ref();
|
||||
let inject_summary = self.inject_resident_summary
|
||||
&& memory_layout.is_some()
|
||||
&& self
|
||||
.manifest
|
||||
.memory
|
||||
// Resident-injection collection: only when memory is enabled in
|
||||
// the manifest AND this Pod opts in (consolidation workers opt out).
|
||||
// Owned `Vec` lives for the duration of `render` below; the
|
||||
// context borrows a slice into it.
|
||||
let resident: Vec<memory::ResidentKnowledgeEntry> = if self.inject_resident_knowledge {
|
||||
self.memory_layout
|
||||
.as_ref()
|
||||
.and_then(|m| m.inject_summary)
|
||||
.unwrap_or(true);
|
||||
let resident_summary: Option<String> = if inject_summary {
|
||||
memory_layout.and_then(memory::collect_resident_summary)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let inject_resident_knowledge = self.inject_resident_knowledge && memory_layout.is_some();
|
||||
let resident: Vec<memory::ResidentKnowledgeEntry> = if inject_resident_knowledge {
|
||||
memory_layout
|
||||
.map(memory::collect_resident_knowledge)
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let resident_slice: Option<&[memory::ResidentKnowledgeEntry]> = if inject_resident_knowledge
|
||||
{
|
||||
Some(&resident)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let resident_slice: Option<&[memory::ResidentKnowledgeEntry]> =
|
||||
if self.inject_resident_knowledge && self.memory_layout.is_some() {
|
||||
Some(&resident)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let resident_workflows: Vec<workflow_crate::ResidentWorkflowEntry> =
|
||||
if self.inject_resident_workflows {
|
||||
if self.inject_resident_knowledge && self.memory_layout.is_some() {
|
||||
self.workflow_registry.resident_entries()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let resident_workflow_slice: Option<&[workflow_crate::ResidentWorkflowEntry]> =
|
||||
if self.inject_resident_workflows {
|
||||
if self.inject_resident_knowledge && self.memory_layout.is_some() {
|
||||
Some(&resident_workflows)
|
||||
} else {
|
||||
None
|
||||
|
|
@ -1281,7 +1200,6 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
scope: &scope_snapshot,
|
||||
tool_names,
|
||||
agents_md: agents_md_read.body,
|
||||
resident_summary: resident_summary.as_deref(),
|
||||
resident_knowledge: resident_slice,
|
||||
resident_workflows: resident_workflow_slice,
|
||||
prompts: &self.prompts,
|
||||
|
|
@ -3323,11 +3241,12 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
});
|
||||
|
||||
// Memory tools are self-contained — they bypass ScopedFs and write
|
||||
// directly under the workspace via WorkspaceLayout. Resident section
|
||||
// injection is a Pod-level concern; this disposable Worker is built
|
||||
// without it by construction, in keeping with `docs/plan/memory.md`
|
||||
// §Consolidation のKnowledgeアクセス (agent pulls knowledge through
|
||||
// the search tool instead of via system-prompt residency).
|
||||
// directly under the workspace via WorkspaceLayout. Resident
|
||||
// knowledge injection (`Pod::set_resident_knowledge_injection`) is
|
||||
// a Pod-level concern; this disposable Worker is built without it
|
||||
// by construction, in keeping with `docs/plan/memory.md` §Consolidation
|
||||
// のKnowledgeアクセス (agent pulls knowledge through the search
|
||||
// tool instead of via system-prompt residency).
|
||||
let query_cfg = memory::tool::QueryConfig::from(memory_cfg);
|
||||
worker.register_tool(memory::tool::read_tool_with_usage(
|
||||
layout.clone(),
|
||||
|
|
@ -3644,9 +3563,7 @@ where
|
|||
prompts: common.prompts,
|
||||
workflow_registry: common.workflow_registry,
|
||||
memory_layout: common.memory_layout,
|
||||
inject_resident_summary: true,
|
||||
inject_resident_knowledge: true,
|
||||
inject_resident_workflows: true,
|
||||
pending_scope_snapshot: Arc::new(Mutex::new(None)),
|
||||
extract_in_flight: Arc::new(AtomicBool::new(false)),
|
||||
consolidation_in_flight: Arc::new(AtomicBool::new(false)),
|
||||
|
|
@ -3723,9 +3640,7 @@ where
|
|||
prompts: common.prompts,
|
||||
workflow_registry: common.workflow_registry,
|
||||
memory_layout: common.memory_layout,
|
||||
inject_resident_summary: true,
|
||||
inject_resident_knowledge: true,
|
||||
inject_resident_workflows: true,
|
||||
pending_scope_snapshot: Arc::new(Mutex::new(None)),
|
||||
extract_in_flight: Arc::new(AtomicBool::new(false)),
|
||||
consolidation_in_flight: Arc::new(AtomicBool::new(false)),
|
||||
|
|
@ -3901,9 +3816,7 @@ where
|
|||
prompts: common.prompts,
|
||||
workflow_registry: common.workflow_registry,
|
||||
memory_layout: common.memory_layout,
|
||||
inject_resident_summary: true,
|
||||
inject_resident_knowledge: true,
|
||||
inject_resident_workflows: true,
|
||||
pending_scope_snapshot: Arc::new(Mutex::new(None)),
|
||||
extract_in_flight: Arc::new(AtomicBool::new(false)),
|
||||
consolidation_in_flight: Arc::new(AtomicBool::new(false)),
|
||||
|
|
@ -4578,239 +4491,6 @@ mod build_summary_prompt_tests {
|
|||
assert_eq!(interrupt_system_count, 1);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ResidentInjectionGates {
|
||||
summary: bool,
|
||||
knowledge: bool,
|
||||
workflows: bool,
|
||||
}
|
||||
|
||||
impl ResidentInjectionGates {
|
||||
fn all(enabled: bool) -> Self {
|
||||
Self {
|
||||
summary: enabled,
|
||||
knowledge: enabled,
|
||||
workflows: enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn render_system_prompt_with_summary(
|
||||
summary_doc: Option<&str>,
|
||||
memory_config: Option<manifest::MemoryConfig>,
|
||||
resident_injection: bool,
|
||||
) -> String {
|
||||
render_system_prompt_with_resident_sections(
|
||||
summary_doc,
|
||||
memory_config,
|
||||
ResidentInjectionGates::all(resident_injection),
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn render_system_prompt_with_resident_sections(
|
||||
summary_doc: Option<&str>,
|
||||
memory_config: Option<manifest::MemoryConfig>,
|
||||
gates: ResidentInjectionGates,
|
||||
include_knowledge: bool,
|
||||
include_workflow: bool,
|
||||
) -> String {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
|
||||
let pwd = dir.path().join("workspace");
|
||||
std::fs::create_dir_all(&pwd).unwrap();
|
||||
if let Some(doc) = summary_doc {
|
||||
std::fs::create_dir_all(pwd.join(".insomnia/memory")).unwrap();
|
||||
std::fs::write(pwd.join(".insomnia/memory/summary.md"), doc).unwrap();
|
||||
}
|
||||
if include_knowledge {
|
||||
std::fs::create_dir_all(pwd.join(".insomnia/knowledge")).unwrap();
|
||||
std::fs::write(
|
||||
pwd.join(".insomnia/knowledge/resident-policy.md"),
|
||||
knowledge_doc("knowledge resident desc"),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
if include_workflow {
|
||||
std::fs::create_dir_all(pwd.join(".insomnia/workflow")).unwrap();
|
||||
std::fs::write(
|
||||
pwd.join(".insomnia/workflow/resident-flow.md"),
|
||||
workflow_doc("workflow resident desc"),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let mut manifest = minimal_manifest_with_skills(vec![]);
|
||||
manifest.memory = memory_config;
|
||||
let scope = Scope::writable(&pwd).unwrap();
|
||||
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, pwd.clone(), scope)
|
||||
.await
|
||||
.unwrap();
|
||||
pod.memory_layout = pod
|
||||
.manifest
|
||||
.memory
|
||||
.as_ref()
|
||||
.map(|mem| memory::WorkspaceLayout::resolve(mem, &pwd));
|
||||
if let Some(layout) = pod.memory_layout.as_ref() {
|
||||
pod.workflow_registry = workflow_crate::load_workflows(layout).unwrap();
|
||||
}
|
||||
if gates.summary == gates.knowledge && gates.summary == gates.workflows {
|
||||
pod.set_resident_injection(gates.summary);
|
||||
} else {
|
||||
pod.set_resident_summary_injection(gates.summary);
|
||||
pod.set_resident_knowledge_injection(gates.knowledge);
|
||||
pod.set_resident_workflow_injection(gates.workflows);
|
||||
}
|
||||
let template = SystemPromptTemplate::parse(
|
||||
"$insomnia/default",
|
||||
crate::prompt::loader::PromptLoader::builtins_only(),
|
||||
)
|
||||
.unwrap();
|
||||
pod.set_system_prompt_template(template);
|
||||
pod.ensure_system_prompt_materialized().unwrap();
|
||||
pod.worker().get_system_prompt().unwrap().to_string()
|
||||
}
|
||||
|
||||
fn summary_doc(body: &str) -> String {
|
||||
format!("---\nupdated_at: 2026-01-01T00:00:00Z\n---\n{body}")
|
||||
}
|
||||
|
||||
fn knowledge_doc(description: &str) -> String {
|
||||
format!(
|
||||
"---\ncreated_at: 2026-01-01T00:00:00Z\nupdated_at: 2026-01-01T00:00:00Z\nkind: policy\ndescription: \"{description}\"\nmodel_invokation: true\nuser_invocable: true\nlast_sources: []\n---\nbody\n",
|
||||
)
|
||||
}
|
||||
|
||||
fn workflow_doc(description: &str) -> String {
|
||||
format!("---\ndescription: {description}\nmodel_invokation: true\n---\nbody\n")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resident_summary_body_is_injected_without_frontmatter() {
|
||||
let rendered = render_system_prompt_with_summary(
|
||||
Some(&summary_doc("summary body for resident prompt\n")),
|
||||
Some(manifest::MemoryConfig::default()),
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(rendered.contains("## Resident memory summary"));
|
||||
assert!(rendered.contains("summary body for resident prompt"));
|
||||
assert!(!rendered.contains("updated_at: 2026-01-01T00:00:00Z"));
|
||||
assert!(!rendered.contains("---\nupdated_at"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resident_summary_injection_can_be_disabled_by_manifest() {
|
||||
let memory = manifest::MemoryConfig {
|
||||
inject_summary: Some(false),
|
||||
..manifest::MemoryConfig::default()
|
||||
};
|
||||
let rendered = render_system_prompt_with_summary(
|
||||
Some(&summary_doc("disabled summary body\n")),
|
||||
Some(memory),
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(!rendered.contains("Resident memory summary"));
|
||||
assert!(!rendered.contains("disabled summary body"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resident_summary_is_absent_without_memory_config() {
|
||||
let rendered = render_system_prompt_with_summary(
|
||||
Some(&summary_doc("memory-disabled summary body\n")),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(!rendered.contains("Resident memory summary"));
|
||||
assert!(!rendered.contains("memory-disabled summary body"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn malformed_resident_summary_does_not_fail_render() {
|
||||
let rendered = render_system_prompt_with_summary(
|
||||
Some("---\nthis is not yaml: : :\n---\nbad summary body\n"),
|
||||
Some(manifest::MemoryConfig::default()),
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(rendered.contains("## Working boundaries"));
|
||||
assert!(!rendered.contains("Resident memory summary"));
|
||||
assert!(!rendered.contains("bad summary body"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resident_summary_gate_false_omits_only_summary() {
|
||||
let prompt = render_system_prompt_with_resident_sections(
|
||||
Some(&summary_doc("resident summary marker")),
|
||||
Some(manifest::MemoryConfig::default()),
|
||||
ResidentInjectionGates {
|
||||
summary: false,
|
||||
knowledge: true,
|
||||
workflows: true,
|
||||
},
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(!prompt.contains("Resident memory summary"));
|
||||
assert!(!prompt.contains("resident summary marker"));
|
||||
assert!(prompt.contains("Resident knowledge"));
|
||||
assert!(prompt.contains("knowledge resident desc"));
|
||||
assert!(prompt.contains("Resident workflows"));
|
||||
assert!(prompt.contains("workflow resident desc"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn knowledge_and_workflow_gates_false_keep_resident_summary() {
|
||||
let prompt = render_system_prompt_with_resident_sections(
|
||||
Some(&summary_doc("resident summary marker")),
|
||||
Some(manifest::MemoryConfig::default()),
|
||||
ResidentInjectionGates {
|
||||
summary: true,
|
||||
knowledge: false,
|
||||
workflows: false,
|
||||
},
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(prompt.contains("Resident memory summary"));
|
||||
assert!(prompt.contains("resident summary marker"));
|
||||
assert!(!prompt.contains("Resident knowledge"));
|
||||
assert!(!prompt.contains("knowledge resident desc"));
|
||||
assert!(!prompt.contains("Resident workflows"));
|
||||
assert!(!prompt.contains("workflow resident desc"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resident_injection_opt_out_omits_all_resident_sections() {
|
||||
let prompt = render_system_prompt_with_resident_sections(
|
||||
Some(&summary_doc("resident summary marker")),
|
||||
Some(manifest::MemoryConfig::default()),
|
||||
ResidentInjectionGates::all(false),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(!prompt.contains("Resident memory summary"));
|
||||
assert!(!prompt.contains("resident summary marker"));
|
||||
assert!(!prompt.contains("Resident knowledge"));
|
||||
assert!(!prompt.contains("knowledge resident desc"));
|
||||
assert!(!prompt.contains("Resident workflows"));
|
||||
assert!(!prompt.contains("workflow resident desc"));
|
||||
}
|
||||
|
||||
fn minimal_manifest_with_skills(dirs: Vec<PathBuf>) -> PodManifest {
|
||||
// Construct the smallest possible PodManifest that resolves; only
|
||||
// the `skills` field matters for `skill_dir_read_rules`.
|
||||
|
|
|
|||
|
|
@ -79,18 +79,13 @@ pub enum PodPrompt {
|
|||
/// Trailing `## Project instructions (AGENTS.md)` section, appended
|
||||
/// after the scope summary when an AGENTS.md is present.
|
||||
AgentsMdSection,
|
||||
/// Trailing `## Resident memory summary` section, appended after the
|
||||
/// AGENTS.md section when memory is enabled, summary injection is enabled,
|
||||
/// and `memory/summary.md` has a valid non-empty body.
|
||||
ResidentMemorySummarySection,
|
||||
/// Trailing `## Resident knowledge` section, appended after the
|
||||
/// resident memory summary when memory is enabled, Knowledge resident
|
||||
/// injection is enabled, and at least one `knowledge/*` record advertises
|
||||
/// `model_invokation: true`.
|
||||
/// AGENTS.md section when memory is enabled and at least one
|
||||
/// `knowledge/*` record advertises `model_invokation: true`.
|
||||
ResidentKnowledgeSection,
|
||||
/// Trailing `## Resident workflows` section, appended after resident
|
||||
/// knowledge when Workflow resident injection is enabled and at least one
|
||||
/// workflow advertises `model_invokation: true`.
|
||||
/// knowledge when memory is enabled and at least one workflow advertises
|
||||
/// `model_invokation: true`.
|
||||
ResidentWorkflowsSection,
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +100,6 @@ impl PodPrompt {
|
|||
Self::InterruptSystemNote => "interrupt_system_note",
|
||||
Self::WorkingBoundariesSection => "working_boundaries_section",
|
||||
Self::AgentsMdSection => "agents_md_section",
|
||||
Self::ResidentMemorySummarySection => "resident_memory_summary_section",
|
||||
Self::ResidentKnowledgeSection => "resident_knowledge_section",
|
||||
Self::ResidentWorkflowsSection => "resident_workflows_section",
|
||||
}
|
||||
|
|
@ -123,7 +117,6 @@ impl PodPrompt {
|
|||
PodPrompt::InterruptSystemNote,
|
||||
PodPrompt::WorkingBoundariesSection,
|
||||
PodPrompt::AgentsMdSection,
|
||||
PodPrompt::ResidentMemorySummarySection,
|
||||
PodPrompt::ResidentKnowledgeSection,
|
||||
PodPrompt::ResidentWorkflowsSection,
|
||||
];
|
||||
|
|
@ -137,7 +130,6 @@ impl PodPrompt {
|
|||
"interrupt_system_note",
|
||||
"working_boundaries_section",
|
||||
"agents_md_section",
|
||||
"resident_memory_summary_section",
|
||||
"resident_knowledge_section",
|
||||
"resident_workflows_section",
|
||||
];
|
||||
|
|
@ -360,14 +352,6 @@ impl PromptCatalog {
|
|||
self.render(PodPrompt::AgentsMdSection, single("agents_md", agents_md))
|
||||
}
|
||||
|
||||
/// Render `PodPrompt::ResidentMemorySummarySection` with `{{ summary }}`.
|
||||
pub fn resident_memory_summary_section(&self, summary: &str) -> Result<String, CatalogError> {
|
||||
self.render(
|
||||
PodPrompt::ResidentMemorySummarySection,
|
||||
single("summary", summary),
|
||||
)
|
||||
}
|
||||
|
||||
/// Render `PodPrompt::ResidentKnowledgeSection` with `{{ entries }}`
|
||||
/// (a pre-formatted list block authored by the caller).
|
||||
pub fn resident_knowledge_section(&self, entries: &str) -> Result<String, CatalogError> {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@
|
|||
//! prompt is materialised exactly once just before the first LLM turn:
|
||||
//! the rendered body is appended with a fixed trailing section carrying
|
||||
//! the Pod's `Scope` summary and (if present) the project's `AGENTS.md`
|
||||
//! contents plus resident memory sections, and the whole string is handed
|
||||
//! to the Worker via `set_system_prompt`. Subsequent turns and compactions
|
||||
//! reuse that materialised string verbatim.
|
||||
//! contents, and the whole string is handed to the Worker via
|
||||
//! `set_system_prompt`. Subsequent turns and compactions reuse that
|
||||
//! materialised string verbatim.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
|
@ -122,7 +122,6 @@ impl SystemPromptTemplate {
|
|||
ctx.prompts,
|
||||
ctx.scope,
|
||||
ctx.agents_md.as_deref(),
|
||||
ctx.resident_summary,
|
||||
ctx.resident_knowledge,
|
||||
ctx.resident_workflows,
|
||||
)
|
||||
|
|
@ -153,10 +152,6 @@ pub struct SystemPromptContext<'a> {
|
|||
/// Not visible from the template; consumed by the trailing-section
|
||||
/// formatter in [`SystemPromptTemplate::render`].
|
||||
pub agents_md: Option<String>,
|
||||
/// The body of `<workspace>/.insomnia/memory/summary.md`, with
|
||||
/// frontmatter stripped. `None` disables the resident summary section;
|
||||
/// empty strings are ignored by the trailing-section formatter.
|
||||
pub resident_summary: Option<&'a str>,
|
||||
/// Resident-injection candidates from `<workspace>/knowledge/*` whose
|
||||
/// frontmatter has `model_invokation: true`. `None` disables the
|
||||
/// section entirely (memory disabled, or a consolidation worker that opts
|
||||
|
|
@ -214,7 +209,6 @@ pub fn append_trailing_section(
|
|||
prompts: &PromptCatalog,
|
||||
scope: &Scope,
|
||||
agents_md: Option<&str>,
|
||||
resident_summary: Option<&str>,
|
||||
resident_knowledge: Option<&[ResidentKnowledgeEntry]>,
|
||||
resident_workflows: Option<&[ResidentWorkflowEntry]>,
|
||||
) -> Result<String, SystemPromptError> {
|
||||
|
|
@ -234,15 +228,6 @@ pub fn append_trailing_section(
|
|||
out.push_str(section.trim_end_matches(&['\n', ' '][..]));
|
||||
out.push('\n');
|
||||
}
|
||||
if let Some(summary) = resident_summary {
|
||||
let summary = summary.trim_matches(&['\n', '\r'][..]);
|
||||
if !summary.trim().is_empty() {
|
||||
out.push('\n');
|
||||
let section = prompts.resident_memory_summary_section(summary)?;
|
||||
out.push_str(section.trim_end_matches(&['\n', ' '][..]));
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
if let Some(entries) = resident_knowledge {
|
||||
if !entries.is_empty() {
|
||||
out.push('\n');
|
||||
|
|
@ -350,26 +335,6 @@ mod tests {
|
|||
scope,
|
||||
tool_names: tools,
|
||||
agents_md,
|
||||
resident_summary: None,
|
||||
resident_knowledge: None,
|
||||
resident_workflows: None,
|
||||
prompts: test_prompts(),
|
||||
}
|
||||
}
|
||||
|
||||
fn ctx_with_summary<'a>(
|
||||
cwd: &'a Path,
|
||||
scope: &'a Scope,
|
||||
summary: Option<&'a str>,
|
||||
) -> SystemPromptContext<'a> {
|
||||
SystemPromptContext {
|
||||
now: fixed_now(),
|
||||
cwd,
|
||||
language: manifest::defaults::WORKER_LANGUAGE,
|
||||
scope,
|
||||
tool_names: Vec::new(),
|
||||
agents_md: None,
|
||||
resident_summary: summary,
|
||||
resident_knowledge: None,
|
||||
resident_workflows: None,
|
||||
prompts: test_prompts(),
|
||||
|
|
@ -388,32 +353,12 @@ mod tests {
|
|||
scope,
|
||||
tool_names: Vec::new(),
|
||||
agents_md: None,
|
||||
resident_summary: None,
|
||||
resident_knowledge: Some(resident),
|
||||
resident_workflows: None,
|
||||
prompts: test_prompts(),
|
||||
}
|
||||
}
|
||||
|
||||
fn ctx_with_resident_workflows<'a>(
|
||||
cwd: &'a Path,
|
||||
scope: &'a Scope,
|
||||
resident: &'a [ResidentWorkflowEntry],
|
||||
) -> SystemPromptContext<'a> {
|
||||
SystemPromptContext {
|
||||
now: fixed_now(),
|
||||
cwd,
|
||||
language: manifest::defaults::WORKER_LANGUAGE,
|
||||
scope,
|
||||
tool_names: Vec::new(),
|
||||
agents_md: None,
|
||||
resident_summary: None,
|
||||
resident_knowledge: None,
|
||||
resident_workflows: Some(resident),
|
||||
prompts: test_prompts(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Lazily-initialised builtin catalog shared across system-prompt
|
||||
/// tests, so every `ctx()` can hand out a `&'static PromptCatalog`
|
||||
/// reference without forcing test bodies to create one per call.
|
||||
|
|
@ -623,40 +568,6 @@ mod tests {
|
|||
assert!(!rendered.contains("Project instructions"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_section_renders_resident_summary_body() {
|
||||
let (_tmp, loader) = user_loader_with("body.md", "BODY");
|
||||
let tmpl = SystemPromptTemplate::parse("$user/body", loader).unwrap();
|
||||
let dir = TempDir::new().unwrap();
|
||||
let scope = build_scope(dir.path());
|
||||
let rendered = tmpl
|
||||
.render(&ctx_with_summary(
|
||||
dir.path(),
|
||||
&scope,
|
||||
Some("Persistent summary body"),
|
||||
))
|
||||
.unwrap();
|
||||
assert!(rendered.contains("## Resident memory summary"));
|
||||
assert!(rendered.contains("Persistent summary body"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_section_omits_resident_summary_when_none_or_empty() {
|
||||
let (_tmp, loader) = user_loader_with("body.md", "BODY");
|
||||
let tmpl = SystemPromptTemplate::parse("$user/body", loader).unwrap();
|
||||
let dir = TempDir::new().unwrap();
|
||||
let scope = build_scope(dir.path());
|
||||
let rendered = tmpl
|
||||
.render(&ctx_with_summary(dir.path(), &scope, None))
|
||||
.unwrap();
|
||||
assert!(!rendered.contains("Resident memory summary"));
|
||||
|
||||
let rendered = tmpl
|
||||
.render(&ctx_with_summary(dir.path(), &scope, Some(" \n")))
|
||||
.unwrap();
|
||||
assert!(!rendered.contains("Resident memory summary"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_section_omits_resident_knowledge_when_none() {
|
||||
let (_tmp, loader) = user_loader_with("body.md", "BODY");
|
||||
|
|
@ -707,39 +618,4 @@ mod tests {
|
|||
let pos_resident = rendered.find("## Resident knowledge").unwrap();
|
||||
assert!(pos_resident > pos_boundaries);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_section_renders_resident_workflows() {
|
||||
let (_tmp, loader) = user_loader_with("body.md", "BODY");
|
||||
let tmpl = SystemPromptTemplate::parse("$user/body", loader).unwrap();
|
||||
let dir = TempDir::new().unwrap();
|
||||
let scope = build_scope(dir.path());
|
||||
let workflows = [ResidentWorkflowEntry {
|
||||
slug: "resident-flow".to_string(),
|
||||
description: "workflow resident desc\nwith newline".to_string(),
|
||||
}];
|
||||
let rendered = tmpl
|
||||
.render(&ctx_with_resident_workflows(dir.path(), &scope, &workflows))
|
||||
.unwrap();
|
||||
|
||||
assert!(rendered.contains("## Resident workflows"));
|
||||
assert!(rendered.contains("- resident-flow: workflow resident desc with newline"));
|
||||
let pos_boundaries = rendered.find("## Working boundaries").unwrap();
|
||||
let pos_resident = rendered.find("## Resident workflows").unwrap();
|
||||
assert!(pos_resident > pos_boundaries);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_section_omits_empty_resident_workflows() {
|
||||
let (_tmp, loader) = user_loader_with("body.md", "BODY");
|
||||
let tmpl = SystemPromptTemplate::parse("$user/body", loader).unwrap();
|
||||
let dir = TempDir::new().unwrap();
|
||||
let scope = build_scope(dir.path());
|
||||
let workflows: [ResidentWorkflowEntry; 0] = [];
|
||||
let rendered = tmpl
|
||||
.render(&ctx_with_resident_workflows(dir.path(), &scope, &workflows))
|
||||
.unwrap();
|
||||
|
||||
assert!(!rendered.contains("Resident workflows"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -288,18 +288,7 @@ async fn invalid_only_staging_is_distinct_from_no_staging() {
|
|||
std::fs::create_dir_all(layout.staging_dir()).unwrap();
|
||||
let invalid_id = uuid::Uuid::now_v7();
|
||||
let invalid_path = layout.staging_dir().join(format!("{invalid_id}.json"));
|
||||
std::fs::write(
|
||||
&invalid_path,
|
||||
serde_json::json!({
|
||||
"source": {
|
||||
"session_id": "legacy-session",
|
||||
"range": [0, 1]
|
||||
},
|
||||
"requests": []
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(&invalid_path, "{").unwrap();
|
||||
|
||||
let client = MockClient::new(vec![]);
|
||||
let mut pod = make_pod_with(FILES_THRESHOLD_TOML, pwd.path().to_path_buf(), client).await;
|
||||
|
|
@ -331,11 +320,6 @@ async fn below_threshold_skip_is_audit_only() {
|
|||
pod.try_post_run_consolidate().await.unwrap();
|
||||
|
||||
assert!(collect_memory_worker_reasons(&mut rx).is_empty());
|
||||
let audit = read_audit_jsonl(&layout);
|
||||
let reason = audit.last().unwrap()["reason"]
|
||||
.as_str()
|
||||
.expect("audit reason must be a string");
|
||||
assert!(reason.starts_with("threshold_not_reached "));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ reqwest = { version = "0.13", features = ["json", "native-tls"] }
|
|||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync", "fs", "rt", "time"] }
|
||||
tokio = { workspace = true, features = ["sync", "fs", "rt"] }
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
|
|
|
|||
|
|
@ -188,10 +188,6 @@ impl AuthProvider for CodexAuthProvider {
|
|||
.map_err(CodexAuthError::to_client_error)?;
|
||||
Self::build_headers(&snap).map_err(CodexAuthError::to_client_error)
|
||||
}
|
||||
|
||||
fn is_codex_backend(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// `access_token` の JWT `exp` を見て、期限切れなら true。
|
||||
|
|
|
|||
|
|
@ -4,13 +4,11 @@
|
|||
//! 401 + `error.code` で永続失敗を分類する。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::error::{CodexAuthError, PermanentReason};
|
||||
|
||||
pub const CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann";
|
||||
pub const REFRESH_URL: &str = "https://auth.openai.com/oauth/token";
|
||||
pub const DEFAULT_REFRESH_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RefreshRequest<'a> {
|
||||
|
|
@ -43,15 +41,13 @@ pub async fn request_refresh(
|
|||
grant_type: "refresh_token",
|
||||
refresh_token,
|
||||
};
|
||||
let response = response_with_timeout(
|
||||
client
|
||||
.post(endpoint)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&body)
|
||||
.send(),
|
||||
DEFAULT_REFRESH_TIMEOUT,
|
||||
)
|
||||
.await?;
|
||||
let response = client
|
||||
.post(endpoint)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| CodexAuthError::RefreshTransient(format!("send: {e}")))?;
|
||||
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
|
|
@ -72,21 +68,6 @@ pub async fn request_refresh(
|
|||
}
|
||||
}
|
||||
|
||||
async fn response_with_timeout(
|
||||
future: impl std::future::Future<Output = Result<reqwest::Response, reqwest::Error>>,
|
||||
timeout: Duration,
|
||||
) -> Result<reqwest::Response, CodexAuthError> {
|
||||
tokio::time::timeout(timeout, future)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
CodexAuthError::RefreshTransient(format!(
|
||||
"codex_oauth_refresh timed out after {}s",
|
||||
timeout.as_secs()
|
||||
))
|
||||
})?
|
||||
.map_err(|e| CodexAuthError::RefreshTransient(format!("send: {e}")))
|
||||
}
|
||||
|
||||
fn classify_permanent(body: &str) -> (PermanentReason, String) {
|
||||
let code = extract_error_code(body);
|
||||
let reason = match code.as_deref() {
|
||||
|
|
@ -126,23 +107,6 @@ fn extract_error_code(body: &str) -> Option<String> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_response_timeout_is_transient() {
|
||||
let err = match response_with_timeout(
|
||||
std::future::pending::<Result<reqwest::Response, reqwest::Error>>(),
|
||||
Duration::from_millis(5),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => panic!("expected refresh timeout"),
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
assert!(
|
||||
matches!(err, CodexAuthError::RefreshTransient(message) if message.contains("timed out"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_expired() {
|
||||
let body = r#"{"error":{"code":"refresh_token_expired"}}"#;
|
||||
|
|
|
|||
|
|
@ -1,38 +1,21 @@
|
|||
//! Debug-only LLM request/stream trace recording.
|
||||
//! Debug-only raw stream event recording.
|
||||
//!
|
||||
//! [`TraceEntry`] captures stream lifecycle markers and raw provider stream
|
||||
//! events for debugging stalls. Written to a separate `.trace.jsonl` file,
|
||||
//! [`TraceEntry`] captures every LLM stream event verbatim for debugging
|
||||
//! and post-hoc analysis. Written to a separate `.trace.jsonl` file,
|
||||
//! completely independent of the segment log used for state restoration.
|
||||
//!
|
||||
//! Disabled by default. Enable via `SessionConfig::record_event_trace`.
|
||||
|
||||
use llm_worker::llm_client::event::Event;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
/// A single trace entry recording either a lifecycle marker or raw stream event.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
/// A single trace entry recording a raw stream event.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TraceEntry {
|
||||
/// Timestamp in milliseconds since Unix epoch.
|
||||
pub ts: u64,
|
||||
/// Turn number at the time of recording.
|
||||
pub turn: usize,
|
||||
/// LLM call index within the worker, when known.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub llm_call: Option<usize>,
|
||||
#[serde(flatten)]
|
||||
pub payload: TracePayload,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum TracePayload {
|
||||
/// Normalized provider stream event.
|
||||
StreamEvent { event: Event },
|
||||
/// Marker for code that runs before/around provider stream events.
|
||||
Lifecycle {
|
||||
label: String,
|
||||
#[serde(default, skip_serializing_if = "Value::is_null")]
|
||||
data: Value,
|
||||
},
|
||||
/// The raw stream event.
|
||||
pub event: Event,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ pub mod segment_log;
|
|||
pub mod store;
|
||||
pub mod system_item;
|
||||
|
||||
pub use event_trace::{TraceEntry, TracePayload};
|
||||
pub use event_trace::TraceEntry;
|
||||
pub use fs_store::FsStore;
|
||||
pub use llm_worker::UsageRecord;
|
||||
pub use llm_worker::llm_client::types::{ContentPart, Item, Role};
|
||||
|
|
|
|||
|
|
@ -174,12 +174,9 @@ fn trace_entries_in_separate_file() {
|
|||
let trace = TraceEntry {
|
||||
ts: 1500,
|
||||
turn: 0,
|
||||
llm_call: Some(0),
|
||||
payload: session_store::TracePayload::StreamEvent {
|
||||
event: llm_worker::llm_client::event::Event::Ping(
|
||||
llm_worker::llm_client::event::PingEvent { timestamp: None },
|
||||
),
|
||||
},
|
||||
event: llm_worker::llm_client::event::Event::Ping(
|
||||
llm_worker::llm_client::event::PingEvent { timestamp: None },
|
||||
),
|
||||
};
|
||||
store.append_trace(sid, segid, &trace).unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
//! The viewport's last frame stays in the terminal's scrollback so the
|
||||
//! user has a record of what was spawned (or why a spawn failed).
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
|
@ -21,7 +20,6 @@ use client::{SpawnConfig, spawn_pod};
|
|||
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
|
||||
use manifest::{
|
||||
PodManifestConfig, ScopeConfig, find_project_manifest_from, load_layer, user_manifest_path,
|
||||
user_manifest_path_from_env,
|
||||
};
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
|
|
@ -212,15 +210,9 @@ fn load_spawn_defaults() -> Result<SpawnDefaults, SpawnError> {
|
|||
// Run the same merge pod itself uses, then read what's missing off the
|
||||
// result. We only look at `scope.allow` here — `pod.name` is an
|
||||
// instance-level identifier and is supplied by the dialog or `--pod`.
|
||||
// TUI must pre-read the same user manifest path that the pod CLI will use,
|
||||
// including a non-empty INSOMNIA_USER_MANIFEST override; empty values fall
|
||||
// back to the auto-discovered path.
|
||||
let user_layer = user_manifest_path_for_spawn(
|
||||
std::env::var_os(manifest::paths::USER_MANIFEST_ENV),
|
||||
user_manifest_path(),
|
||||
)
|
||||
.filter(|p| p.is_file())
|
||||
.and_then(|p| load_layer(&p).ok());
|
||||
let user_layer = user_manifest_path()
|
||||
.filter(|p| p.is_file())
|
||||
.and_then(|p| load_layer(&p).ok());
|
||||
let project_layer = find_project_manifest_from(&cwd).and_then(|p| load_layer(&p).ok());
|
||||
|
||||
let mut cascade = PodManifestConfig::builtin_defaults();
|
||||
|
|
@ -260,13 +252,6 @@ fn load_spawn_defaults() -> Result<SpawnDefaults, SpawnError> {
|
|||
})
|
||||
}
|
||||
|
||||
fn user_manifest_path_for_spawn(
|
||||
env_value: Option<OsString>,
|
||||
default_user_manifest: Option<PathBuf>,
|
||||
) -> Option<PathBuf> {
|
||||
user_manifest_path_from_env(env_value).or(default_user_manifest)
|
||||
}
|
||||
|
||||
fn form_for_pod_name(pod_name: String, defaults: SpawnDefaults) -> Form {
|
||||
Form {
|
||||
cwd: defaults.cwd,
|
||||
|
|
@ -727,28 +712,6 @@ permission = "write"
|
|||
assert!(empty_cascade.scope.allow.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_manifest_path_for_spawn_prefers_non_empty_env_override() {
|
||||
assert_eq!(
|
||||
user_manifest_path_for_spawn(
|
||||
Some(OsString::from("/tmp/override.toml")),
|
||||
Some(PathBuf::from("/default/manifest.toml")),
|
||||
),
|
||||
Some(PathBuf::from("/tmp/override.toml")),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_manifest_path_for_spawn_treats_empty_env_as_unset() {
|
||||
assert_eq!(
|
||||
user_manifest_path_for_spawn(
|
||||
Some(OsString::from("")),
|
||||
Some(PathBuf::from("/default/manifest.toml")),
|
||||
),
|
||||
Some(PathBuf::from("/default/manifest.toml")),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn name_input_handles_insert_backspace_and_cursor() {
|
||||
let mut f = form("", false);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ FileRef resolution and file tools follow symlinks only after the resolved target
|
|||
|
||||
Recommended external-reference workflow:
|
||||
|
||||
- Prefer adding the real external project path, such as a local external checkout, to the Pod read scope when the Pod is started or spawned.
|
||||
- Prefer adding the real external project path, such as a local `ghq` clone, to the Pod read scope when the Pod is started or spawned.
|
||||
- If a workspace symlink is used, the symlink target still must be inside readable scope. For writes, the resolved target must be inside writable scope.
|
||||
- If a relative symlink is broken, recreate it with the correct relative target from the symlink's parent directory, or use an absolute symlink.
|
||||
- Directory traversal tools such as Glob and Grep do not follow symlink directories. Use the resolved target directory directly when it is in read scope.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ INSOMNIA が利用する LLM プロバイダとその認証方式を決める。
|
|||
| プロバイダ | scheme | 認証 | 用途 |
|
||||
|---|---|---|---|
|
||||
| **Ollama** | scheme/anthropic 流用(v0.14+ `/v1/messages`) | なし(ダミー) | ローカル + `:cloud` サフィックスでクラウド中継。`localhost:11434` で統一 |
|
||||
| **Codex OAuth** | scheme/openai_responses | `~/.codex/auth.json` | Codex CLI と同じ認証ストアを使う Responses 経路 |
|
||||
| **Codex OAuth 流用** | scheme/openai_responses | `~/.codex/auth.json` | ChatGPT Plus/Pro の定額枠を利用 |
|
||||
| **Anthropic API** | scheme/anthropic | API key | 従量課金経路のみ |
|
||||
|
||||
Ollama は独自 scheme を作らず `scheme/anthropic` を base_url 差し替えで流用。`/v1/chat/completions` は stream+tools バグ (#9092) のため使わない。`cache_control` / `tool_choice` / `metadata` / `count_tokens` は Ollama 非対応のため送らない。
|
||||
|
|
@ -31,13 +31,13 @@ Ollama は独自 scheme を作らず `scheme/anthropic` を base_url 差し替
|
|||
|
||||
### 非サポート
|
||||
|
||||
- **Claude Pro/Max OAuth 経路** — Anthropic が 2026-01-09 にサーバ側でブロック、2026-02-19 に第三者ツール経由の利用制限を明文化。第一級機能としては採用しない
|
||||
- **`claude -p` CLI fork** — 専用 API integration ではないため実装しない
|
||||
- **Claude Pro/Max OAuth 流用** — Anthropic が 2026-01-09 にサーバ側でブロック、2026-02-19 に ToS で第三者ツール経由を明文禁止。リスクが第一級機能に見合わない
|
||||
- **`claude -p` CLI fork** — 同様にグレー。実装しない
|
||||
|
||||
## 根拠
|
||||
|
||||
- **Codex OAuth は Codex CLI 互換の認証経路として扱う**: Codex CLI は Apache-2.0 で公開されており、同じ Responses 系 wire behavior に寄せる
|
||||
- **Anthropic API は従量だが代替なし**: Pro/Max OAuth 経路の制限後、Claude 系を使うには API key 経路のみ
|
||||
- **Codex OAuth は OpenAI 黙認**: Codex CLI は Apache-2.0、openai/codex #8338 で OpenAI 社員が fork 自由と明言、service terms に名指し禁止なし
|
||||
- **Anthropic API は従量だが代替なし**: Pro/Max OAuth 封鎖後、Claude 系を使うには API key 経路のみ
|
||||
- **Ollama は `:cloud` で透過**: `ollama signin` で Ed25519 鍵登録後、`localhost:11434` 経由でクラウドモデルが使える。ローカルデーモンが署名付き中継
|
||||
- **OpenAI 互換は汎用アダプタ 1 本**: ルーター系は後追いで数を増やしやすい宣言型設計、実装コスト最小
|
||||
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
# AI maintainer 用 WorkItem / Thread 抽象メモ
|
||||
|
||||
## 位置づけ
|
||||
|
||||
AI maintainer が単なる coding agent ではなく、作業の発見・分解・実装委譲・review・完了判断まで扱うには、現在の `TODO.md` / `tickets/*.md` だけでは足りない。必要なのは、個々の file path ではなく「作業単位」と「その会話・判断・成果物」を扱う抽象である。
|
||||
|
||||
このメモは実装 ticket ではなく、将来こういうものが必要になるという設計メモとして置く。具体的な最初の実装は `tickets.sh` / `work-items/` の MVP で試す。
|
||||
|
||||
## 必要になりそうな概念
|
||||
|
||||
- WorkItem
|
||||
- 作業単位。title / status / kind / priority / labels / acceptance criteria / links を持つ。
|
||||
- Thread
|
||||
- WorkItem に紐づく append-only な会話・判断・review・実装報告の流れ。
|
||||
- Event
|
||||
- comment / plan / decision / implementation_report / review / status_change など。
|
||||
- Artifact
|
||||
- review log、test log、設計メモ、branch / commit / worktree への link など。
|
||||
- Lease / Run
|
||||
- どの Pod / agent がどの worktree / scope で作業中かを表す runtime coordination 情報。
|
||||
|
||||
## 配置の分担
|
||||
|
||||
repo-managed な project-visible 領域には、作業の正本として人間が読める coordination data を置く。
|
||||
|
||||
```text
|
||||
repo/
|
||||
work-items/ # WorkItem / Thread / Artifact
|
||||
tickets/ # 当面の既存 ticket。将来 WorkItem view に寄せる候補
|
||||
docs/ # plan / report
|
||||
```
|
||||
|
||||
`.insomnia` は local runtime state として扱い、project coordination の正本にはしない。
|
||||
|
||||
```text
|
||||
.insomnia/
|
||||
memory/
|
||||
workflow/
|
||||
maintainer/
|
||||
leases/
|
||||
runs/
|
||||
inbox/
|
||||
```
|
||||
|
||||
## ID と backend の考え方
|
||||
|
||||
WorkItem ID は中央 `SEQUENCE` にしない。複数 branch / worktree / Pod が同時に作業を作ると conflict しやすいため、timestamp + slug などの衝突しにくい ID にする。
|
||||
|
||||
```text
|
||||
YYYYMMDD-HHMMSS-<slug>
|
||||
YYYYMMDD-HHMMSS-<short-rand>-<slug>
|
||||
```
|
||||
|
||||
backend は最初は repo 内 file backend でよいが、抽象としては Git directory 固定にしない。将来、remote maintainer hub や GitHub Issues などへ移る可能性を潰さない。
|
||||
|
||||
## 移行イメージ
|
||||
|
||||
1. 既存の `TODO.md` / `tickets/` 運用は維持する。
|
||||
2. `tickets.sh` MVP で `work-items/` の file backend と thread 操作を試す。
|
||||
3. 既存 `TODO.md` / `tickets/` を手動で `work-items/` に寄せ、`doctor` が通る状態にする。
|
||||
4. その運用が安定したら、`TODO.md` を generated view にするか、廃止するかを判断する。
|
||||
5. 必要になった段階で Rust crate / Store interface / LeaseStore / remote backend を検討する。
|
||||
|
||||
## 当面やらないこと
|
||||
|
||||
- remote maintainer hub の実装。
|
||||
- SQLite / index / search daemon の導入。
|
||||
- Pod lifecycle / completion tracking の完全実装。
|
||||
- LeaseStore の本格実装。
|
||||
- TUI 統合。
|
||||
|
||||
## 参照
|
||||
|
||||
- `tickets/tickets-sh-workitem-thread-mvp.md`
|
||||
- `docs/plan/ai-maintainer.md`
|
||||
- `tickets/auto-maintain-workflow.md`
|
||||
|
|
@ -54,7 +54,7 @@ consolidation が以下を検出した場合、Client に Notification を投げ
|
|||
|
||||
- DSL 化や step 粒度の制約 — 初期は Markdown 本文そのまま実行
|
||||
- Workflow 実行中の中断・再開・トランザクション管理
|
||||
- 品質検証フロー: empirical prompt tuning pattern(`docs/ref/memory-systems.md` §6)相当の**新規 subagent 試走 + 構造化報告**を Workflow に適用。判定対象は本文の不明瞭点・裁量補完・要件達成率。Knowledge 単体の検証は設けず、`requires` 経由で Workflow から使われる前提で間接回収。SKILL 的用途(Workflow 経由しない `#knowledge`)は人間レビューに委ねる
|
||||
- 品質検証フロー: mizchi empirical-prompt-tuning(`docs/ref/memory-systems.md` §6)相当の**新規 subagent 試走 + 構造化報告**を Workflow に適用。判定対象は本文の不明瞭点・裁量補完・要件達成率。Knowledge 単体の検証は設けず、`requires` 経由で Workflow から使われる前提で間接回収。SKILL 的用途(Workflow 経由しない `#knowledge`)は人間レビューに委ねる
|
||||
|
||||
## 関連
|
||||
|
||||
|
|
|
|||
312
docs/ref/claude-code-deferred-tools.md
Normal file
312
docs/ref/claude-code-deferred-tools.md
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
# リファレンス: Claude Code の deferred tools 機構
|
||||
|
||||
調査日: 2026-04-30。Claude Code(このセッションのハーネス)が採用しているツール提示・遅延ロード方式について、自身の system prompt と system-reminder の内容から観察できた事実と、そこから合理的に推測される実装方針をまとめる。Pod / insomnia でハーネス側のツール抽象を設計する際の参考資料。
|
||||
|
||||
ハーネス内部の実装は確認していないため、観察事実と推測を分けて記載する。
|
||||
|
||||
---
|
||||
|
||||
## 1. 観察事実
|
||||
|
||||
### 1.1 ツール定義の表現
|
||||
|
||||
system prompt 冒頭に、ツール定義が以下の形式の **テキストブロック** として埋め込まれている:
|
||||
|
||||
```
|
||||
<functions>
|
||||
<function>{"description": "...", "name": "Read", "parameters": {...JSONSchema...}}</function>
|
||||
<function>{"description": "...", "name": "Edit", "parameters": {...}}</function>
|
||||
...
|
||||
</functions>
|
||||
```
|
||||
|
||||
各 `<function>` は JSONSchema を含む単行 JSON。Anthropic API の `tools` パラメータに渡る構造化データではなく、**プロンプトの一部としてレンダリングされたテキスト**である。
|
||||
|
||||
### 1.2 ツール呼び出しの表現
|
||||
|
||||
モデル側の出力も XML タグ列で行う(`function_calls` / `invoke` / `parameter` などのタグ)。ネイティブの tool_use content block ではない。ハーネスがこのテキストをパースし、対応するツールを実行する。
|
||||
|
||||
### 1.3 deferred tools の宣言
|
||||
|
||||
特定のツール群は最初の `<functions>` ブロックに含まれず、代わりに system-reminder で **名前リストだけ** が提示される:
|
||||
|
||||
```
|
||||
The following deferred tools are now available via ToolSearch.
|
||||
Their schemas are NOT loaded — calling them directly will fail with InputValidationError.
|
||||
Use ToolSearch with query "select:<name>" to load tool schemas before calling them:
|
||||
AskUserQuestion
|
||||
CronCreate
|
||||
...
|
||||
```
|
||||
|
||||
このリストには名前のみで schema は無い。
|
||||
|
||||
### 1.4 ToolSearch によるロード
|
||||
|
||||
`ToolSearch` 自体は最初から完全なスキーマで利用可能(通常のツールとして system prompt 冒頭の `<functions>` に含まれる)。`select:<name>` クエリを投げると、tool_result の本文として:
|
||||
|
||||
```
|
||||
<functions>
|
||||
<function>{"description": "...", "name": "AskUserQuestion", "parameters": {...}}</function>
|
||||
</functions>
|
||||
```
|
||||
|
||||
が返ってくる。「上のツール定義と同じエンコーディング」と明示されており、以降そのツールは通常通り呼べるようになる。
|
||||
|
||||
### 1.5 パラメータ値のエンコーディング規約
|
||||
|
||||
ツール呼び出しの `<parameter>` タグの中身は、値の型に応じて異なるエンコーディングを使う:
|
||||
|
||||
- プリミティブ (string / number / boolean): そのままテキスト
|
||||
- 配列・オブジェクト: JSON 文字列としてシリアライズしてテキストに
|
||||
|
||||
system prompt 末尾にも以下のように明記されている:
|
||||
|
||||
```
|
||||
When making function calls using tools that accept array or object parameters
|
||||
ensure those are structured using JSON.
|
||||
```
|
||||
|
||||
つまり「XML が外側の骨格、中身は型に応じてテキスト/JSON」という二層構造。
|
||||
|
||||
---
|
||||
|
||||
## 2. パラダイムの推測: prompted tool use
|
||||
|
||||
観察事実から、Claude Code は Anthropic API の **structured tool use ではなく prompted tool use** を採用している(あるいはハイブリッド)と判断できる。
|
||||
|
||||
| 項目 | structured | prompted |
|
||||
|---|---|---|
|
||||
| ツール定義 | API リクエストの `tools` 配列 | system prompt 内のテキスト |
|
||||
| ツール呼び出し | `tool_use` content block | テキスト中の特定タグ |
|
||||
| 検証 | API レイヤ(schema 強制) | ハーネスレイヤ(自前パース&検証) |
|
||||
| 拡張性 | API の制約に縛られる | 自由(XML/JSON/独自形式) |
|
||||
|
||||
Claude Code の振る舞いはすべて prompted 側に寄っている。API は単に「テキストを生成するモデル」として使われ、ツール抽象は完全にハーネスのレイヤにある。
|
||||
|
||||
---
|
||||
|
||||
## 3. deferred tools が成立する仕組み(推測)
|
||||
|
||||
prompted tool use 前提で考えると、deferred tools は素直に説明できる:
|
||||
|
||||
1. ハーネスは **全ツールのレジストリ** を内部に持つ
|
||||
2. リクエスト時、初期 `<functions>` ブロックには「コアツール + ToolSearch」だけを描画。残りは system-reminder で名前のみ列挙
|
||||
3. モデルが `ToolSearch` を呼ぶと、ハーネスはレジストリから該当 schema を引き、tool_result の **テキスト** として `<function>...</function>` を返す
|
||||
4. モデルはそのテキストを参照しつつ、対応する tool 呼び出しタグを生成
|
||||
5. ハーネスはタグをパースし、**レジストリで再度バリデーション**して実行
|
||||
|
||||
検証の真実は **レジストリ** であり、context にスキーマテキストが現れたかどうかではない。スキーマテキストは「モデルが正しい引数を生成するためのプロンプト材料」として機能する。
|
||||
|
||||
---
|
||||
|
||||
## 4. 設計上のメリット
|
||||
|
||||
### 4.1 prompt cache のプレフィックス安定化
|
||||
|
||||
Anthropic の prompt caching はプレフィックスマッチで効く。`tools` パラメータや system プロンプト前半が変わると、それ以降の cache が一括無効化される。
|
||||
|
||||
deferred tools 方式では:
|
||||
- API リクエストの `tools` パラメータは終始固定(あるいは空)
|
||||
- 初期 system prompt の `<functions>` ブロックも固定
|
||||
- ToolSearch の結果は **会話末尾の tool_result に積まれるだけ** → 前方プレフィックスは揺らがない
|
||||
|
||||
→ ツール群が大量にあってもプレフィックスキャッシュが安定する。
|
||||
|
||||
### 4.2 context 圧縮
|
||||
|
||||
全ツールの schema をいきなり system prompt に展開すると、肥大化して入力トークンを浪費する。MCP サーバが大量のツールを expose する世界では現実的でない。deferred 方式なら **そのセッションで実際に使うツールの schema だけ** が context に乗る。
|
||||
|
||||
### 4.3 ツール数のスケーラビリティ
|
||||
|
||||
レジストリに登録するだけなら理論上数百〜数千ツールでも扱える。モデルには「使えるツール名リスト」だけ見せ、必要に応じて schema を取り寄せる構造。
|
||||
|
||||
---
|
||||
|
||||
## 5. トレードオフ
|
||||
|
||||
- **1ターンの遅延**: ツールを呼ぶ前に ToolSearch が必要。初回だけだが UX 上のレイテンシは増える
|
||||
- **モデルの認知負荷**: 「使う前にロードする」を学習・指示する必要がある(system-reminder で明示している)
|
||||
- **ハルシネーション余地**: 名前を知っているが schema を知らない状態で呼ぼうとして InputValidationError を起こすケースが発生しうる
|
||||
- **ハーネス側の責務増**: パース・検証・レジストリ管理がすべてハーネス側に乗る。バグると安全性に直結
|
||||
|
||||
---
|
||||
|
||||
## 6. Pod / insomnia への示唆
|
||||
|
||||
Pod でローカル LLM をエージェントとして動かす場合、同様の課題が発生する:
|
||||
|
||||
- 提供したいツールが増えると context が肥大化
|
||||
- ローカルモデルは structured tool use の精度が API モデルに劣ることが多い → prompted 方式の方が安定する場合がある
|
||||
- KV cache の効きを最大化したい(ローカルだと特に prefill コストが重い)
|
||||
|
||||
deferred tools 方式は **prompted tool use を前提とする限り**、これらの課題への自然な解になる。具体的には:
|
||||
|
||||
1. ハーネス内にツールレジストリを持ち、`tools` メタデータと実装を分離
|
||||
2. system prompt には固定の core tools だけ展開、それ以外は名前で示唆
|
||||
3. `tool_search` 相当のツールで schema を引ける動線を用意
|
||||
4. パース・検証はハーネス側で完結、モデルへの API 呼び出しは text-in/text-out に統一
|
||||
|
||||
特に「prompt cache のプレフィックス安定化」は、ローカル推論でも KV cache 再利用に直接効く。
|
||||
|
||||
---
|
||||
|
||||
## 7. 未確認事項
|
||||
|
||||
- Anthropic API の `tools` パラメータが実際に空なのか、core tools だけ入っているのか、ハイブリッドなのかは確認できていない
|
||||
- ToolSearch の結果テキストが context に残り続けるのか、後続の compaction で削られるのか
|
||||
- レジストリのスコープ(セッション固定 / プラグインで動的追加 / MCP 経由など)の境界
|
||||
- system-reminder で名前リストが提示されるタイミングが固定なのか動的なのか
|
||||
|
||||
これらを確認するには Claude Code のソース(公開部分)か、API リクエストのキャプチャが必要。
|
||||
|
||||
---
|
||||
|
||||
## 8. Codex による Web 検証
|
||||
|
||||
検証日: 2026-04-30。Codex で Web 上の公開情報を確認した範囲では、本ドキュメントの「deferred tools の目的・メリット・トレードオフ」は概ね妥当。ただし、「Claude Code が structured tool use ではなく prompted tool use を採用している」という断定は、公開情報だけでは裏取りできない。
|
||||
|
||||
### 8.1 公式情報で確認できたこと
|
||||
|
||||
- Claude Code / Claude Agent SDK には `ToolSearch` / tool search が存在する。公式ドキュメントでは、すべての tool definition を upfront に context window へ入れる代わりに、必要な tool を動的に発見・ロードする仕組みとして説明されている。
|
||||
- https://code.claude.com/docs/en/agent-sdk/tool-search
|
||||
- tool search は大量ツール環境向けの context 効率化として説明されている。Claude Code docs では、50 tools で 10-20K tokens を消費しうること、30-50 tools を超えると tool selection accuracy が落ちることが述べられている。
|
||||
- Claude Code docs では、tool search はデフォルト有効で、`ENABLE_TOOL_SEARCH` により `true` / `auto` / `auto:N` / `false` を設定できるとされている。
|
||||
- tool search は MCP server 由来の tool や custom SDK MCP server 由来の tool にも適用される。
|
||||
- 初回 discovery には search step の追加 round-trip が発生する。ツール数が 10 未満程度なら、全 tool を upfront に読む方が速い場合がある。
|
||||
- Claude API docs には、tool definition property として `defer_loading` が記載されている。`defer_loading: true` は「初期 system prompt から tool を除外し、tool search が `tool_reference` を返した時に on demand でロードする」ものとして説明されている。
|
||||
- https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference
|
||||
- https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool
|
||||
- prompt caching との関係も公式に説明されている。`defer_loading: true` の tools は rendered tools section から除外され、cache key 計算前の prefix に現れない。発見後の full definition は conversation body 側に展開されるため、prompt cache を保ちやすい。
|
||||
- Anthropic の engineering blog でも Tool Search Tool は紹介されている。そこでは「Tool Search Tool だけを upfront にロードし、3-5 個程度の relevant tools を on demand に発見する」設計として説明され、token 使用量削減と tool selection accuracy 改善が述べられている。
|
||||
- https://www.anthropic.com/engineering/advanced-tool-use
|
||||
|
||||
### 8.2 本ドキュメントの推測と食い違う可能性がある点
|
||||
|
||||
- 公開されている Claude API の説明では、tool search は `tools` 配列、`defer_loading: true`、`tool_reference`、`tool_use` block を使う structured tool use の仕組みとして説明されている。そのため、「API は単に text-in/text-out で、ツール抽象は完全にハーネスのレイヤにある」と断定するのは強すぎる。
|
||||
- 公式情報上は、deferred tool も API request の `tools` parameter に定義として渡し、その tool definition に `defer_loading: true` を付ける設計である。したがって「API リクエストの `tools` パラメータは終始固定(あるいは空)」という推測は、少なくとも公開 API の設計とは一致しない。
|
||||
- `ToolSearch` が `select:<name>` で schema text を返す、という観察は、このセッションのハーネス上の事実としては扱えるが、公式 API docs の表現とは異なる。公式 API では search tool が `tool_reference` を返し、それが conversation body 内で full tool definition に展開されると説明されている。
|
||||
- `<functions><function>...` や `function_calls` / `invoke` / `parameter` の XML タグ列は、観察対象のハーネスで見えている表現として記録できる。ただし Claude Code 内部 system prompt は公式に公開されていないため、Web 上の公式情報だけで Claude Code 全体の内部実装形式として確認することはできない。
|
||||
- https://code.claude.com/docs/en/configuration
|
||||
|
||||
### 8.3 現時点での整理
|
||||
|
||||
公開情報と観察事実を両立させるなら、次の程度に弱めて理解するのが安全:
|
||||
|
||||
> Claude Code の観察上は、ツール定義や呼び出しが prompt 内テキストとして見えている。ただし、公開されている Anthropic API の tool search は `tools` 配列、`defer_loading`、`tool_reference`、`tool_use` を使う structured tool use として説明されている。したがって、Claude Code 内部が完全な prompted tool use なのか、API の structured tool use を CLI / ハーネス側で別表現にレンダリングしているのか、あるいはそのハイブリッドなのかは未確認。
|
||||
|
||||
Pod / insomnia への示唆としては、deferred tools の設計目的である context 圧縮、tool selection accuracy の維持、prefix cache の安定化は公式情報でも裏付けられる。一方で、Anthropic API の現在の公開設計を参考にするなら、`tool_search` 相当の実装は「単なる schema text の注入」だけでなく、内部 registry 上の tool reference、ロード済み tool の状態管理、検証レイヤを明確に分けて設計する方がよい。
|
||||
|
||||
---
|
||||
|
||||
## 9. ツール I/O の実際のフォーマット
|
||||
|
||||
(2026-05-01 追記)
|
||||
|
||||
deferred tools の本論からはやや脇道だが、ToolSearch がスキーマテキストを context に注入することで「ツールが使える状態」になる仕組みを理解するためには、ツール定義・呼び出しの実フォーマットと、Anthropic API の公開 surface との対応関係を押さえておく必要がある。
|
||||
|
||||
### 9.1 ツール定義の入力フォーマット
|
||||
|
||||
system prompt 冒頭に置かれる:
|
||||
|
||||
```
|
||||
<functions>
|
||||
<function>{"description": "...", "name": "Read", "parameters": {...JSONSchema...}}</function>
|
||||
</functions>
|
||||
```
|
||||
|
||||
外側は XML、`<function>` の中身は単行 JSON。JSON 部分は `name`, `description`, `parameters` の 3 フィールドで、`parameters` は標準的な JSONSchema (`type: "object"`, `properties`, `required`, `additionalProperties` 等)。
|
||||
|
||||
### 9.2 ツール呼び出しの出力フォーマット
|
||||
|
||||
モデル側の生成は完全に XML タグ列:
|
||||
|
||||
```
|
||||
<function_calls>
|
||||
<invoke name="Read">
|
||||
<parameter name="file_path">/foo/bar</parameter>
|
||||
</invoke>
|
||||
</function_calls>
|
||||
```
|
||||
|
||||
`<parameter>` の中身は §1.5 のエンコード規則に従う。
|
||||
|
||||
### 9.3 標準 Anthropic API との関係
|
||||
|
||||
開発者から見える API surface は完全に JSON ベース:
|
||||
|
||||
- リクエスト: `tools: [{name, description, input_schema}]`
|
||||
- レスポンス: `tool_use` content block (JSON)
|
||||
|
||||
ところが Claude Code 上の観察では XML+JSON のハイブリッド表現が実際に流れている。両者の整合は次のように理解できる:
|
||||
|
||||
| | 標準 API (structured tool use) | Claude Code (prompted tool use) |
|
||||
|---|---|---|
|
||||
| 開発者が渡す形式 | JSON (`tools` 配列) | — (ハーネス内製) |
|
||||
| モデルが受け取る prompt | 非公開 (推測: XML+JSON) | XML+JSON (観察可) |
|
||||
| モデルが返す表現 | 非公開 (推測: XML タグ) → API が parse | XML タグ (観察可) |
|
||||
| 開発者が受け取る形式 | `tool_use` block (JSON) | — |
|
||||
|
||||
標準 API では JSON ↔ モデル内部表現の変換が API サーバ側で隠蔽されている。Claude Code が観察できるのはその「裸の」表現で、Anthropic がモデル訓練に用いているフォーマットそのものと推測される (同じモデルなので)。
|
||||
|
||||
### 9.4 バリデーションとリトライの内製化
|
||||
|
||||
この構造を見ると、Tool Call API は実質「フォーマット規約 + schema validation + retry」をプロバイダー側に押し込めた仕様と読める:
|
||||
|
||||
1. **フォーマット規約**: XML 骨格と parameter エンコード規則
|
||||
2. **バリデーション**: schema 違反の検出
|
||||
3. **リトライ**: malformed なら API 内部で再生成し、開発者には完成品だけ返す
|
||||
4. **訓練投資**: そのフォーマットで RLHF / SFT 済み
|
||||
|
||||
開発者が `tool_use` block を常に正しい JSON として受け取れるのは、(4) のおかげで失敗率が低く、(1)-(3) のおかげで失敗時も隠蔽されているから。Cline 等の prompted tool use 実装が同じことをやろうとしても、(4) が効かないため精度・安定性で見劣りしていたのは、この訓練投資の差で説明できる。
|
||||
|
||||
ただし Claude Code のハーネスは **token-level 制約 (grammar-based sampling) を入れていない**ことが、§10 の実演から推測できる。「自由に生成 → パース失敗なら error を tool_result で返して retry」という設計で、token 制約は使っていない。これは inference サーバ側の実装コストを避けつつ、(4) の訓練品質に依存する方針。
|
||||
|
||||
### 9.5 ローカル LLM への含意
|
||||
|
||||
Pod / insomnia でローカル LLM を使う場合、(4) が効かない。最近のローカル向けエージェントモデルは tool use 用に訓練されているので XML パースのような原始的処理は不要だが、各モデルが訓練された自前のフォーマット (Hermes / Llama / Qwen / Mistral 等で異なる) があり、それに合わせてレンダリングする必要がある。
|
||||
|
||||
なお、Anthropic と OpenAI のツール呼び出しアプローチの比較 (XML タグ vs 特殊制御トークン、JSON Schema vs TypeScript namespace、thinking block vs 3 チャネル分離) は [tool_approach_comparison.md](./tool_approach_comparison.md) を参照。ローカルモデル向けの設計では、Claude 系の知見 (本書: deferred / registry / context 圧縮) と OpenAI 系の知見 (Harmony: トークン保証 / チャネル分離 / 公開仕様) を役割で使い分けるのが自然。
|
||||
|
||||
具体的な責務分担は以下:
|
||||
|
||||
- ツール定義のレンダリング: モデル固有のテンプレート (chat template の `tools` 拡張等) に合わせる
|
||||
- 出力パース: モデルが生成した形式 (タグ / JSON / 独自トークン) をハーネスでパース
|
||||
- バリデーション: 自前で schema 照合
|
||||
- リトライ: パース失敗・schema 違反時に error を返してモデル側に修正させる
|
||||
|
||||
これは Claude Code が内製化しているもののローカル版そのもの。
|
||||
|
||||
---
|
||||
|
||||
## 10. 実演: schema 未ロードでの呼び出し
|
||||
|
||||
(2026-05-01 実演)
|
||||
|
||||
§3 の推測「検証の真実はレジストリであり、context にスキーマテキストが現れたかどうかではない」を確認するため、deferred tool である `TaskList` を ToolSearch せずに直接呼び出した。
|
||||
|
||||
### 10.1 結果
|
||||
|
||||
受理された (`No tasks found` が返ってきた)。InputValidationError は発生しなかった。
|
||||
|
||||
### 10.2 含意
|
||||
|
||||
1. **schema 未ロード = 呼び出せない、ではない**。少なくとも引数なしで呼べるツールでは、schema text が context に無くても通る
|
||||
2. ハーネスのバリデーションは「schema text が context にあるか」ではなく、**実引数が registry の schema に合致するか**で判定している
|
||||
3. system-reminder の "calling them directly will fail with InputValidationError" は厳密には常に真ではない。引数が schema と矛盾しないケース (特に必須引数のないツールに引数なしで呼ぶ場合) では素通りする
|
||||
4. context に積まれる schema text は、モデルが正しい引数を生成するための **誘導 / プロンプト材料** であって、validation の入力ではない
|
||||
|
||||
§3 の推測がそのまま裏付けられた形になる。
|
||||
|
||||
### 10.3 system-reminder の役割の再解釈
|
||||
|
||||
警告が「常に fail する」と読めるのは過剰表現で、実際には「**引数 schema が必要なツールは引数指定が必須なので、schema を知らずに呼べば事実上 fail する**」というモデルへの誘導と理解するのが正確。registry 側のバリデーションは引数の中身を見ているだけで、context に schema text があるかは見ていない。
|
||||
|
||||
### 10.4 設計上の含意
|
||||
|
||||
Pod / insomnia 側で同様の機構を作る場合:
|
||||
|
||||
- 「schema text が context にあるか」を validation 条件にする必要はない (むしろしない方が単純)
|
||||
- registry に常時全 tool を登録しておき、context へのレンダリングだけ deferred にする
|
||||
- モデルが (誘導を無視して) schema 未ロードのツールを呼んでも、引数が合っていれば実行してよい
|
||||
- この方が registry の真実性が一本化されて実装が単純になる
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# LLM プロバイダ統合の外部事例
|
||||
|
||||
調査日: 2026-04-19。プロバイダ認証経路・`ollama launch` 等の時事的項目は陳腐化が早い。数値・URLは一次ソースで再確認すること。
|
||||
調査日: 2026-04-19。認証流用・`ollama launch` 等の時事的項目は陳腐化が早い。数値・URLは一次ソースで再確認すること。
|
||||
|
||||
## 各ハーネスのプロバイダ対応方式
|
||||
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
- Vercel AI SDK + Models.dev で 75+ プロバイダ
|
||||
- 認証は `~/.local/share/opencode/auth.json` に統一保存(OAuth / APIキー / その他の3種別)
|
||||
- **2026-03-19 に Anthropic OAuth 対応を削除**(PR #18186)。詳細は後述
|
||||
- ChatGPT ブラウザ認証 (`/connect`) は存続
|
||||
- ChatGPT Plus/Pro のブラウザ OAuth (`/connect`) は存続
|
||||
- https://opencode.ai/docs/providers/
|
||||
|
||||
### OpenClaw
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
- Aider は LiteLLM 経由、他は直叩き
|
||||
- BLACKBOX 等マイナー系は OpenAI 互換枠で収容するのが一般パターン
|
||||
|
||||
## 認証経路の現状
|
||||
## 認証流用の現状
|
||||
|
||||
### Anthropic (Claude Pro / Max) ── 封鎖済み
|
||||
- 2026-01-09: Anthropic がサーバ側で Pro/Max OAuth トークンに `This credential is only authorized for use with Claude Code and cannot be used for other API requests` の制限導入
|
||||
|
|
@ -38,14 +38,14 @@
|
|||
- `packages/opencode/src/session/prompt/anthropic-20250930.txt`(Claude Code 風システムプロンプト)
|
||||
- `opencode-anthropic-auth@0.0.13` ビルトインプラグイン
|
||||
- `claude-code-20250219` beta ヘッダ
|
||||
- 代替検討: `claude -p` (Claude Code の headless mode) を subprocess で呼ぶ方式。ACP ではなく素朴な CLI fork であり、insomnia では採用しない
|
||||
- 生き残り手段: `claude -p` (Claude Code の headless mode) を subprocess で呼ぶ方式。ACP ではなく素朴な CLI fork。Anthropic ToS 的にはグレー(明確な裁定なし)
|
||||
- https://code.claude.com/docs/en/legal-and-compliance
|
||||
- https://github.com/sst/opencode/pull/18186
|
||||
|
||||
### OpenAI (Codex CLI / Responses)
|
||||
- Codex CLI は Apache-2.0 で公開されている。insomnia の Codex OAuth 経路は、Codex CLI と同じ Responses 系 wire behavior に寄せる
|
||||
- Codex CLI の認証ストアと conversation header / request compression / SSE behavior を参考にする
|
||||
- OpenCode の `/connect` で ChatGPT ブラウザ認証が通る
|
||||
### OpenAI (ChatGPT Plus / Pro via Codex CLI) ── 黙認
|
||||
- Codex CLI は Apache-2.0。openai/codex Discussion #8338 で OpenAI 社員が fork・改変自由と明言
|
||||
- ChatGPT OAuth を他ツールから使う行為を service terms で名指し禁止する条項は未確認
|
||||
- OpenCode の `/connect` で ChatGPT Plus/Pro ブラウザ認証が通る
|
||||
- コミュニティ評価: 「Anthropic は walled garden、OpenAI はむしろ取り込みに来た」
|
||||
- https://github.com/openai/codex/discussions/8338
|
||||
- https://developers.openai.com/codex/auth
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
- `claude --print` / `claude -p` は Claude Code の非対話(headless)モード。プロンプトを stdin/引数で受け stdout に返す
|
||||
- **ACP ではなく素朴な subprocess 呼び出し**
|
||||
- OpenClaw と OpenCode コミュニティフォーク (`griffinmartin/opencode-claude-auth`) が採用
|
||||
- insomnia では専用 API integration ではないため採用しない
|
||||
- OAuth 流用ではないため 2026-01-09 のブロックは回避できるが、Anthropic ToS の「第三者ツールでの資格情報経由」禁止条項に抵触する可能性(明確な裁定なし)
|
||||
|
||||
## Ollama の統合機構
|
||||
|
||||
|
|
@ -225,7 +225,7 @@ parallel tool calls 可否、tool_choice 対応度。DeepSeek reasoner のよう
|
|||
|
||||
### 第一級サポート(専用アダプタ)
|
||||
- **Ollama API** — ローカル + `:cloud` サフィックスで透過的にクラウド中継。エンドポイントは `localhost:11434` で統一
|
||||
- **Codex OAuth** — `~/.codex/auth.json` を読み、Codex CLI 互換の Responses 経路として扱う。conversation header / compression / SSE behavior は公開実装に合わせる
|
||||
- **Codex OAuth 流用** — `~/.codex/auth.json` を読み ChatGPT Plus/Pro 枠を利用。OpenAI 黙認(Apache-2.0、社員が fork 自由と明言、ToS に名指し禁止なし)
|
||||
- **Anthropic API** — 従量 API key 経路のみ
|
||||
|
||||
### 二次サポート(共通 OpenAI 互換枠)
|
||||
|
|
@ -233,8 +233,8 @@ parallel tool calls 可否、tool_choice 対応度。DeepSeek reasoner のよう
|
|||
- ルーター系は後追いで追加しやすい宣言型設計
|
||||
|
||||
### 非サポート
|
||||
- **Claude Pro/Max OAuth 経路** — 2026-01-09 サーバ側ブロック、2026-02-19 に第三者ツール経由の利用制限を明文化。第一級機能としては採用しない
|
||||
- `claude -p` CLI fork も専用 API integration ではないため実装しない
|
||||
- **Claude Pro/Max OAuth 流用** — 2026-01-09 サーバ側ブロック、2026-02-19 ToS で明文禁止。リスクが第一級機能に見合わない
|
||||
- `claude -p` CLI fork も同様にグレーなので実装しない
|
||||
|
||||
### 実装原則
|
||||
- 認証アダプタ(外部 CLI の認証ストアを読む類)は llm-worker 直下ではなく上位アダプタ層に配置。llm-worker は低レベル基盤に留める原則(project memory)と整合
|
||||
|
|
|
|||
|
|
@ -380,9 +380,9 @@ Skill content のライフサイクルは重要で、**一度発動すると ren
|
|||
|
||||
---
|
||||
|
||||
## 6. プロンプト・スキルの継続的チューニング (empirical prompt tuning pattern)
|
||||
## 6. プロンプト・スキルの継続的チューニング (mizchi empirical-prompt-tuning)
|
||||
|
||||
メモリや skill の**中身を腐らせない**側の話。公開されている prompt tuning pattern として、agent-facing な指示を新規 subagent に実行させ、実行者の自己申告と指示側メトリクスを突き合わせて反復改善する方法がある。insomnia のように skill を蓄積する設計では、**書いた直後に客観的に試す**仕組みが無いと品質が崩れていく。ここへの素直な当てはめ材料として記録。
|
||||
メモリや skill の**中身を腐らせない**側の話。mizchi が 2025-07 頃に公開し、その後 Claude Code の SKILL として整備したメソッドで、「暗黙知の排除」を自動化する。insomnia のように skill を蓄積する設計では、**書いた直後に客観的に試す**仕組みが無いと品質が崩れていく。ここへの素直な当てはめ材料として記録。
|
||||
|
||||
### 基本思想
|
||||
|
||||
|
|
@ -430,9 +430,11 @@ Claude Code の Task tool 戻り値から:
|
|||
- **skill や lessons を新規追加した直後に、同じ insomnia ハーネス内の別 pod で実行して評価**する自動フロー("skill doctor" 的な存在)を作れる。これは insomnia が pod factory を持っている点と相性がいい。
|
||||
- 失敗ログを書いた後、「同じ失敗が再現しないか」を新規 pod で試走する検証ステップが、構造的に**メモリ整備の一部**に組み込める。skill 化しない失敗ログでも有効。
|
||||
- 評価指標を自前で定義しておくと、後で他人(or 未来の自分)が skill を更新した時に腐敗検知できる。
|
||||
- 実体は skill 自身として配布されている例がある。insomnia のメンテ用 skill セットのテンプレにも応用できる。
|
||||
- 実体は skill 自身として配布されている(`mizchi/chezmoi-dotfiles/dot_claude/skills/empirical-prompt-tuning/SKILL.md`)。insomnia のメンテ用 skill セットのテンプレにそのまま借りられる。
|
||||
|
||||
一次ソースは公開 sanitize branch では省略する。
|
||||
一次ソース:
|
||||
- https://zenn.dev/mizchi/articles/empirical-prompt-tuning
|
||||
- https://github.com/mizchi/chezmoi-dotfiles/blob/main/dot_claude/skills/empirical-prompt-tuning/SKILL.md
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -541,14 +543,14 @@ insomnia は **git 管理下に memory を置く**前提なので、物理削除
|
|||
6. **削除は git commit 単位で可逆**という前提を明示する。プロジェクトメモリは git 管理下なので、GC が誤って drop してもユーザーは revert できる。これは Codex が持っていない利点で、GC agent の判断を多少攻めても安全マージンがある。
|
||||
|
||||
一次ソース:
|
||||
- Codex consolidation: `github.com/openai/codex/codex-rs/core/src/memories/consolidation implementation`, `core/templates/memories/consolidation.md`
|
||||
- Codex retention / stage1 pruning: `github.com/openai/codex/codex-rs/core/src/memories/extensions.rs:11-139`, `codex-rs/state/src/runtime/memories.rs:290-331, 333-464`
|
||||
- Hermes char limit reject: `github.com/NousResearch/hermes-agent/tools/memory_tool.py:211-266`
|
||||
- Hermes review spawn: `github.com/NousResearch/hermes-agent/run_agent.py:2727-2830`
|
||||
- OpenClaw promotion apply: `github.com/openclaw/openclaw/extensions/memory-core/src/short-term-promotion.ts:1518-1652`
|
||||
- OpenClaw temporal decay: `github.com/openclaw/openclaw/extensions/memory-core/src/memory/temporal-decay.ts`
|
||||
- OpenClaw dreaming-repair: `github.com/openclaw/openclaw/extensions/memory-core/src/dreaming-repair.ts:70-165`
|
||||
- memory-wiki lint: `github.com/openclaw/openclaw/extensions/memory-wiki/src/lint.ts`, `claim-health.ts:6-7`(`WIKI_AGING_DAYS = 30`, `WIKI_STALE_DAYS = 90`)
|
||||
- Codex consolidation: `~/ghq/github.com/openai/codex/codex-rs/core/src/memories/consolidation implementation`, `core/templates/memories/consolidation.md`
|
||||
- Codex retention / stage1 pruning: `~/ghq/github.com/openai/codex/codex-rs/core/src/memories/extensions.rs:11-139`, `codex-rs/state/src/runtime/memories.rs:290-331, 333-464`
|
||||
- Hermes char limit reject: `~/ghq/github.com/NousResearch/hermes-agent/tools/memory_tool.py:211-266`
|
||||
- Hermes review spawn: `~/ghq/github.com/NousResearch/hermes-agent/run_agent.py:2727-2830`
|
||||
- OpenClaw promotion apply: `~/ghq/github.com/openclaw/openclaw/extensions/memory-core/src/short-term-promotion.ts:1518-1652`
|
||||
- OpenClaw temporal decay: `~/ghq/github.com/openclaw/openclaw/extensions/memory-core/src/memory/temporal-decay.ts`
|
||||
- OpenClaw dreaming-repair: `~/ghq/github.com/openclaw/openclaw/extensions/memory-core/src/dreaming-repair.ts:70-165`
|
||||
- memory-wiki lint: `~/ghq/github.com/openclaw/openclaw/extensions/memory-wiki/src/lint.ts`, `claim-health.ts:6-7`(`WIKI_AGING_DAYS = 30`, `WIKI_STALE_DAYS = 90`)
|
||||
- Cloudflare supersession: https://blog.cloudflare.com/introducing-agent-memory/
|
||||
- Letta sleep-time: https://docs.letta.com/guides/agents/memory/, https://www.letta.com/blog/sleep-time-compute, arxiv 2504.13171
|
||||
- Letta recursive summary: https://www.letta.com/blog/agent-memory
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ insomnia を使って insomnia を開発する運用では、ファイルベー
|
|||
実装 Pod には worktree への write scope を渡したため、親 Pod から見るとその worktree は委譲中の領域になった。その後、同じ worktree に対して ticket review workflow を行い、`tickets/permission-extension-point.review.md` の作成と `tickets/permission-extension-point.md` の Review section 追記をしようとしたところ、親 Pod の `Write` tool は次のように拒否された。
|
||||
|
||||
```text
|
||||
path is read-only in this scope: <repo>/.worktree/permission-extension-point/tickets/permission-extension-point.review.md
|
||||
path is read-only in this scope: /home/hare/Projects/insomnia/.worktree/permission-extension-point/tickets/permission-extension-point.review.md
|
||||
```
|
||||
|
||||
実装 Pod を `StopPod` して scope を回収した後に review artifact を書く必要があった。
|
||||
|
||||
また、最初に SpawnPod した際、write scope を worktree のみに絞ると spawned Pod の cwd である `<repo>` が readable scope 外になり、起動に失敗した。実装用 Pod には worktree write に加えて親 project root の read scope も渡す必要があった。
|
||||
また、最初に SpawnPod した際、write scope を worktree のみに絞ると spawned Pod の cwd である `/home/hare/Projects/insomnia` が readable scope 外になり、起動に失敗した。実装用 Pod には worktree write に加えて親 project root の read scope も渡す必要があった。
|
||||
|
||||
## 障壁
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
親 session:
|
||||
|
||||
- `<insomnia-sessions>/019e5769-73fa-72a0-b501-b657a8976dd3`
|
||||
- `/home/hare/.insomnia/sessions/019e5769-73fa-72a0-b501-b657a8976dd3`
|
||||
|
||||
関連 segment:
|
||||
|
||||
|
|
@ -59,8 +59,8 @@ compact 成功後は `compact_state.record_compact_success()` により `just_co
|
|||
|
||||
runtime state に segment 表示の不一致があった。
|
||||
|
||||
- `<runtime-dir>/insomnia/status.json` は古い segment id を指していた
|
||||
- `<runtime-dir>/pods.json` は新しい segment id を指していた
|
||||
- `/run/user/1000/insomnia/insomnia/status.json` は古い segment id を指していた
|
||||
- `/run/user/1000/insomnia/pods.json` は新しい segment id を指していた
|
||||
|
||||
今回の context 超過の主因ではなさそうだが、attach/restore 時に混乱要因になり得る。
|
||||
|
||||
|
|
|
|||
|
|
@ -1,129 +0,0 @@
|
|||
# SpawnPod 実行後に assistant 応答が返らなかった観測
|
||||
|
||||
## 対象セッション
|
||||
|
||||
Main Pod session log:
|
||||
|
||||
```text
|
||||
<insomnia-sessions>/019e5d30-f7ad-7fb1-bdec-c592e888e290/019e5d30-f7ad-7fb1-bdec-c5a41394e6b1.jsonl
|
||||
```
|
||||
|
||||
Session id:
|
||||
|
||||
```text
|
||||
019e5d30-f7ad-7fb1-bdec-c592e888e290
|
||||
```
|
||||
|
||||
Segment id:
|
||||
|
||||
```text
|
||||
019e5d30-f7ad-7fb1-bdec-c5a41394e6b1
|
||||
```
|
||||
|
||||
## 発生箇所
|
||||
|
||||
`tui-actionbar-transient-notice-api` の実装委譲で `SpawnPod` tool を呼んだ直後、tool result は session log に記録されているが、その後 assistant message が続かず、次の user message `okay?` まで約25分空いた。
|
||||
|
||||
Spawn した Pod:
|
||||
|
||||
```text
|
||||
impl-tui-actionbar-transient-notice-api
|
||||
```
|
||||
|
||||
Worktree:
|
||||
|
||||
```text
|
||||
<repo>/.worktree/tui-actionbar-transient-notice-api
|
||||
```
|
||||
|
||||
Socket:
|
||||
|
||||
```text
|
||||
<runtime-dir>/impl-tui-actionbar-transient-notice-api/sock
|
||||
```
|
||||
|
||||
## session log 上の時系列
|
||||
|
||||
該当 session log の行番号:
|
||||
|
||||
```text
|
||||
4242: assistant_item tool_call SpawnPod
|
||||
4243: tool_result for SpawnPod
|
||||
4265: user message "okay?"
|
||||
4266: assistant response to "okay?"
|
||||
```
|
||||
|
||||
時刻:
|
||||
|
||||
```text
|
||||
SpawnPod tool_call ts: 1779769846743 ms = 2026-05-26T13:30:46+09:00
|
||||
SpawnPod tool_result ts: 1779769846957 ms = 2026-05-26T13:30:46+09:00
|
||||
next user "okay?" ts: 1779771369887 ms = 2026-05-26T13:56:09+09:00
|
||||
assistant response ts: 1779771389978 ms = 2026-05-26T13:56:29+09:00
|
||||
```
|
||||
|
||||
Duration:
|
||||
|
||||
```text
|
||||
SpawnPod tool_call -> tool_result: 214 ms
|
||||
SpawnPod tool_result -> next user message: 1,522,930 ms = 25m22.930s
|
||||
next user message -> assistant response: 20,091 ms = 20.091s
|
||||
```
|
||||
|
||||
## SpawnPod tool_call payload
|
||||
|
||||
Tool call id:
|
||||
|
||||
```text
|
||||
call_vzIr5gYbIPz3ey34gkkQ5s0X
|
||||
```
|
||||
|
||||
Tool name:
|
||||
|
||||
```text
|
||||
SpawnPod
|
||||
```
|
||||
|
||||
Arguments summary:
|
||||
|
||||
```text
|
||||
name: impl-tui-actionbar-transient-notice-api
|
||||
scope:
|
||||
read: <repo>
|
||||
write: <repo>/.worktree/tui-actionbar-transient-notice-api
|
||||
task: implementation request for tickets/tui-actionbar-transient-notice-api.md
|
||||
```
|
||||
|
||||
## SpawnPod tool_result
|
||||
|
||||
The tool result is recorded immediately after the call and reports success:
|
||||
|
||||
```text
|
||||
spawned pod `impl-tui-actionbar-transient-notice-api` listening on <runtime-dir>/impl-tui-actionbar-transient-notice-api/sock
|
||||
```
|
||||
|
||||
This means the observed pause is not visible in the session log as a long-running `SpawnPod` tool call. The tool call/result pair itself is 214 ms apart.
|
||||
|
||||
## 観測事実
|
||||
|
||||
- `SpawnPod` returned a successful tool result in the main session log.
|
||||
- No assistant message appears immediately after that tool result.
|
||||
- The next recorded input is the user message `okay?` about 25 minutes later.
|
||||
- After `okay?`, the assistant responded normally.
|
||||
- Therefore, from the recorded main session log alone, the gap is after the `SpawnPod` tool result and before the next assistant message, not inside the recorded `SpawnPod` tool execution.
|
||||
|
||||
## 関連する実装状態
|
||||
|
||||
At this point `SpawnPod` had recently been changed so that initial task delivery uses confirmation (`send_run_and_confirm`) rather than fire-and-forget. However, this report does not establish that confirmation caused the pause; the log data above only shows that the `SpawnPod` tool result was recorded quickly.
|
||||
|
||||
## 調査に必要な追加データ
|
||||
|
||||
To identify the actual wait point next time, record or inspect:
|
||||
|
||||
- LLM request lifecycle immediately after the `SpawnPod` tool result.
|
||||
- `LlmCallStart` / `LlmRetry` / `LlmCallEnd` if present.
|
||||
- provider HTTP status / retry state if present.
|
||||
- runtime log around the same timestamps for the main Pod.
|
||||
- child Pod session log for `impl-tui-actionbar-transient-notice-api`.
|
||||
- whether assistant generation was started after the tool result or never scheduled.
|
||||
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
調査日: 2026-04-26
|
||||
対象: `crates/llm-worker` (本プロジェクト) / `rig` / `genai` / `swiftide`
|
||||
|
||||
調査方法: 各リポジトリを ローカルに取得し、ソース(README, Cargo.toml, src/, examples/)を直接読解。
|
||||
調査方法: 各リポジトリを ghq で取得し、ソース(README, Cargo.toml, src/, examples/)を直接読解。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -227,17 +227,17 @@ Worker<C, Mutable> Worker<C, Locked>
|
|||
- `crates/llm-worker/src/tool_server.rs:27-52` — Deferred 登録
|
||||
|
||||
### rig
|
||||
- `github.com/0xPlaygrounds/rig/rig/rig-core/src/{agent,completion,tool,pipeline,vector_store,streaming}/mod.rs`
|
||||
- `/home/hare/ghq/github.com/0xPlaygrounds/rig/rig/rig-core/src/{agent,completion,tool,pipeline,vector_store,streaming}/mod.rs`
|
||||
|
||||
### genai
|
||||
- `github.com/jeremychone/rust-genai/src/lib.rs:1-25`
|
||||
- `/home/hare/ghq/github.com/jeremychone/rust-genai/src/lib.rs:1-25`
|
||||
- `.../src/client/client_impl.rs:62-67`
|
||||
- `.../src/chat/chat_request.rs:10-34`
|
||||
- `.../src/chat/chat_stream.rs:11-86`
|
||||
- `.../src/adapter/adapter_types.rs:11-59`
|
||||
|
||||
### swiftide
|
||||
- `github.com/bosun-ai/swiftide/swiftide-agents/src/agent.rs:45-100`
|
||||
- `/home/hare/ghq/github.com/bosun-ai/swiftide/swiftide-agents/src/agent.rs:45-100`
|
||||
- `.../swiftide-agents/src/hooks.rs`
|
||||
- `.../swiftide-core/src/chat_completion/traits.rs`
|
||||
- `.../swiftide-indexing/src/pipeline.rs`
|
||||
|
|
|
|||
|
|
@ -36,11 +36,11 @@
|
|||
- コスト制御目的には `reasoning.effort` (`"low"` など) の使用が推奨される。`max_output_tokens` はあくまで暴走抑止のガードとして位置づける。
|
||||
- o シリーズなど reasoning モデルでは `reasoning.max_tokens` (別パラメータ) で reasoning 専用の上限を設定できる場合もある。
|
||||
|
||||
## 5. Codex CLI 互換 Responses 経路における取り扱い
|
||||
## 5. ChatGPT backend (`https://chatgpt.com/backend-api/codex/responses`) における取り扱い
|
||||
|
||||
この経路は公式 Responses API のパラメータをすべて受け付けるわけではなく、`max_output_tokens` を **サポートしないパラメータとして 400 エラーで拒否する**。
|
||||
このエンドポイントは公式 Responses API のサブセットのみをサポートする非公式 backend であり、`max_output_tokens` を **サポートしないパラメータとして 400 エラーで拒否する**。
|
||||
|
||||
LiteLLM の調査 (https://github.com/BerriAI/litellm/issues/21193) によれば、この経路が受け付けるパラメータは以下に限られる:
|
||||
LiteLLM の調査 (https://github.com/BerriAI/litellm/issues/21193) によれば、ChatGPT Codex backend が受け付けるパラメータは以下に限られる:
|
||||
|
||||
```
|
||||
model, input, instructions, stream, store, include,
|
||||
|
|
|
|||
|
|
@ -39,15 +39,6 @@ agents_md_section = """\
|
|||
{{ agents_md }}\
|
||||
"""
|
||||
|
||||
resident_memory_summary_section = """\
|
||||
---
|
||||
## Resident memory summary
|
||||
|
||||
The following is the current durable session/workspace summary. Treat it as background context; it is not a user request.
|
||||
|
||||
{{ summary }}\
|
||||
"""
|
||||
|
||||
resident_knowledge_section = """\
|
||||
---
|
||||
## Resident knowledge
|
||||
|
|
|
|||
539
tickets.sh
539
tickets.sh
|
|
@ -1,539 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
WORK_ITEMS_DIR=${WORK_ITEMS_DIR:-work-items}
|
||||
STATUSES="open pending closed"
|
||||
REQUIRED_FIELDS="id slug title status kind priority labels created_at updated_at assignee legacy_ticket"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
tickets.sh - repository-local WorkItem / Thread helper
|
||||
|
||||
Usage:
|
||||
./tickets.sh help
|
||||
./tickets.sh --help
|
||||
./tickets.sh list [--status open|pending|closed|all]
|
||||
./tickets.sh show <id-or-slug>
|
||||
./tickets.sh create --title <title> [--slug <slug>] [--kind <kind>] [--priority P2] [--label a,b]
|
||||
./tickets.sh comment <id-or-slug> [--role comment|plan|decision|implementation_report] [--author <name>] [--file <path>]
|
||||
./tickets.sh review <id-or-slug> --approve|--request-changes [--author <name>] [--file <path>]
|
||||
./tickets.sh status <id-or-slug> open|pending|closed
|
||||
./tickets.sh close <id-or-slug> [--resolution <text>|--file <path>]
|
||||
./tickets.sh doctor
|
||||
|
||||
Backend:
|
||||
work-items/{open,pending,closed}/<id>/item.md
|
||||
work-items/{open,pending,closed}/<id>/thread.md
|
||||
work-items/{open,pending,closed}/<id>/artifacts/
|
||||
|
||||
Migration policy:
|
||||
work-items/ is the canonical backend after migration. TODO.md is only a
|
||||
legacy/generated-view notice. Open items must not remain as tickets/*.md, and
|
||||
review notes must be appended to thread.md instead of tickets/*.review.md.
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
printf 'error: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
now_utc() {
|
||||
date -u '+%Y-%m-%dT%H:%M:%SZ'
|
||||
}
|
||||
|
||||
compact_date() {
|
||||
date -u '+%Y%m%d-%H%M%S'
|
||||
}
|
||||
|
||||
ensure_backend_dirs() {
|
||||
mkdir -p "$WORK_ITEMS_DIR/open" "$WORK_ITEMS_DIR/pending" "$WORK_ITEMS_DIR/closed"
|
||||
}
|
||||
|
||||
is_status() {
|
||||
case "$1" in
|
||||
open|pending|closed) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
slugify() {
|
||||
# ASCII-focused slugifier for POSIX shell. Non-ASCII titles should pass --slug.
|
||||
printf '%s' "$1" |
|
||||
tr '[:upper:]' '[:lower:]' |
|
||||
sed 's/[^a-z0-9][^a-z0-9]*/-/g; s/^-//; s/-$//; s/--*/-/g'
|
||||
}
|
||||
|
||||
field_value() {
|
||||
file=$1
|
||||
field=$2
|
||||
awk -v key="$field" '
|
||||
NR == 1 && $0 == "---" { in_fm = 1; next }
|
||||
in_fm && $0 == "---" { exit }
|
||||
in_fm {
|
||||
prefix = key ":"
|
||||
if (index($0, prefix) == 1) {
|
||||
value = substr($0, length(prefix) + 1)
|
||||
sub(/^[[:space:]]*/, "", value)
|
||||
print value
|
||||
exit
|
||||
}
|
||||
}
|
||||
' "$file"
|
||||
}
|
||||
|
||||
set_frontmatter_field() {
|
||||
file=$1
|
||||
field=$2
|
||||
value=$3
|
||||
tmp=${TMPDIR:-/tmp}/tickets-sh.$$.tmp
|
||||
awk -v key="$field" -v new_value="$value" '
|
||||
NR == 1 && $0 == "---" { in_fm = 1; print; next }
|
||||
in_fm && $0 == "---" { in_fm = 0; print; next }
|
||||
in_fm {
|
||||
prefix = key ":"
|
||||
if (index($0, prefix) == 1) {
|
||||
print key ": " new_value
|
||||
next
|
||||
}
|
||||
}
|
||||
{ print }
|
||||
' "$file" > "$tmp"
|
||||
mv "$tmp" "$file"
|
||||
}
|
||||
|
||||
find_item_dir() {
|
||||
query=$1
|
||||
matches=${TMPDIR:-/tmp}/tickets-sh.matches.$$
|
||||
: > "$matches"
|
||||
for status in $STATUSES; do
|
||||
for dir in "$WORK_ITEMS_DIR/$status"/*; do
|
||||
[ -d "$dir" ] || continue
|
||||
item=$dir/item.md
|
||||
[ -f "$item" ] || continue
|
||||
id=$(field_value "$item" id || true)
|
||||
slug=$(field_value "$item" slug || true)
|
||||
if [ "$query" = "$id" ] || [ "$query" = "$slug" ]; then
|
||||
printf '%s\n' "$dir" >> "$matches"
|
||||
fi
|
||||
done
|
||||
done
|
||||
count=$(wc -l < "$matches" | tr -d ' ')
|
||||
if [ "$count" -eq 0 ]; then
|
||||
rm -f "$matches"
|
||||
die "work item not found: $query"
|
||||
fi
|
||||
if [ "$count" -gt 1 ]; then
|
||||
cat "$matches" >&2
|
||||
rm -f "$matches"
|
||||
die "ambiguous work item: $query"
|
||||
fi
|
||||
sed -n '1p' "$matches"
|
||||
rm -f "$matches"
|
||||
}
|
||||
|
||||
item_status_from_dir() {
|
||||
case "$1" in
|
||||
*/open/*) printf 'open\n' ;;
|
||||
*/pending/*) printf 'pending\n' ;;
|
||||
*/closed/*) printf 'closed\n' ;;
|
||||
*) printf 'unknown\n' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
labels_yaml() {
|
||||
labels=$1
|
||||
if [ -z "$labels" ]; then
|
||||
printf '[]'
|
||||
return
|
||||
fi
|
||||
old_ifs=$IFS
|
||||
IFS=,
|
||||
first=1
|
||||
printf '['
|
||||
for label in $labels; do
|
||||
clean=$(printf '%s' "$label" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
||||
[ -n "$clean" ] || continue
|
||||
if [ "$first" -eq 0 ]; then
|
||||
printf ', '
|
||||
fi
|
||||
printf '%s' "$clean"
|
||||
first=0
|
||||
done
|
||||
IFS=$old_ifs
|
||||
printf ']'
|
||||
}
|
||||
|
||||
read_body_to_file() {
|
||||
input_file=$1
|
||||
output_file=$2
|
||||
if [ -n "$input_file" ]; then
|
||||
[ -f "$input_file" ] || die "file not found: $input_file"
|
||||
cat "$input_file" > "$output_file"
|
||||
return
|
||||
fi
|
||||
if [ -t 0 ]; then
|
||||
printf 'No body provided.\n' > "$output_file"
|
||||
else
|
||||
cat > "$output_file"
|
||||
fi
|
||||
}
|
||||
|
||||
append_thread_event() {
|
||||
dir=$1
|
||||
event=$2
|
||||
heading=$3
|
||||
author=$4
|
||||
status_value=$5
|
||||
body_file=$6
|
||||
at=$(now_utc)
|
||||
thread=$dir/thread.md
|
||||
[ -f "$thread" ] || : > "$thread"
|
||||
{
|
||||
printf '\n<!-- event: %s author: %s at: %s' "$event" "$author" "$at"
|
||||
if [ -n "$status_value" ]; then
|
||||
printf ' status: %s' "$status_value"
|
||||
fi
|
||||
printf ' -->\n\n'
|
||||
printf '## %s\n\n' "$heading"
|
||||
cat "$body_file"
|
||||
printf '\n\n---\n'
|
||||
} >> "$thread"
|
||||
set_frontmatter_field "$dir/item.md" updated_at "$at"
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
status_filter=open
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--status)
|
||||
[ "$#" -ge 2 ] || die "--status requires a value"
|
||||
status_filter=$2
|
||||
shift 2
|
||||
;;
|
||||
*) die "unknown list argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
case "$status_filter" in
|
||||
open|pending|closed|all) ;;
|
||||
*) die "invalid status: $status_filter" ;;
|
||||
esac
|
||||
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' status id slug title kind priority updated_at
|
||||
for status in $STATUSES; do
|
||||
if [ "$status_filter" != all ] && [ "$status_filter" != "$status" ]; then
|
||||
continue
|
||||
fi
|
||||
for dir in "$WORK_ITEMS_DIR/$status"/*; do
|
||||
[ -d "$dir" ] || continue
|
||||
item=$dir/item.md
|
||||
[ -f "$item" ] || continue
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$(field_value "$item" status)" \
|
||||
"$(field_value "$item" id)" \
|
||||
"$(field_value "$item" slug)" \
|
||||
"$(field_value "$item" title)" \
|
||||
"$(field_value "$item" kind)" \
|
||||
"$(field_value "$item" priority)" \
|
||||
"$(field_value "$item" updated_at)"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
cmd_show() {
|
||||
[ "$#" -eq 1 ] || die "show requires <id-or-slug>"
|
||||
dir=$(find_item_dir "$1")
|
||||
item=$dir/item.md
|
||||
thread=$dir/thread.md
|
||||
printf '# %s\n\n' "$(field_value "$item" title)"
|
||||
printf 'Path: %s\n' "$dir"
|
||||
printf 'Status: %s\n' "$(field_value "$item" status)"
|
||||
printf 'ID: %s\n' "$(field_value "$item" id)"
|
||||
printf 'Slug: %s\n\n' "$(field_value "$item" slug)"
|
||||
printf '## item.md\n\n'
|
||||
cat "$item"
|
||||
printf '\n\n## thread.md\n\n'
|
||||
if [ -f "$thread" ]; then
|
||||
tail -n 80 "$thread"
|
||||
else
|
||||
printf '(missing thread.md)\n'
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_create() {
|
||||
title=
|
||||
slug=
|
||||
kind=task
|
||||
priority=P2
|
||||
labels=
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--title) [ "$#" -ge 2 ] || die "--title requires a value"; title=$2; shift 2 ;;
|
||||
--slug) [ "$#" -ge 2 ] || die "--slug requires a value"; slug=$2; shift 2 ;;
|
||||
--kind) [ "$#" -ge 2 ] || die "--kind requires a value"; kind=$2; shift 2 ;;
|
||||
--priority) [ "$#" -ge 2 ] || die "--priority requires a value"; priority=$2; shift 2 ;;
|
||||
--label) [ "$#" -ge 2 ] || die "--label requires a value"; labels=$2; shift 2 ;;
|
||||
*) die "unknown create argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$title" ] || die "create requires --title"
|
||||
if [ -z "$slug" ]; then
|
||||
slug=$(slugify "$title")
|
||||
else
|
||||
slug=$(slugify "$slug")
|
||||
fi
|
||||
[ -n "$slug" ] || slug=item
|
||||
ensure_backend_dirs
|
||||
stamp=$(compact_date)
|
||||
id=$stamp-$slug
|
||||
dir=$WORK_ITEMS_DIR/open/$id
|
||||
if [ -e "$dir" ]; then
|
||||
id=$id-$$
|
||||
dir=$WORK_ITEMS_DIR/open/$id
|
||||
fi
|
||||
created=$(now_utc)
|
||||
mkdir -p "$dir/artifacts"
|
||||
: > "$dir/artifacts/.gitkeep"
|
||||
cat > "$dir/item.md" <<EOF
|
||||
---
|
||||
id: $id
|
||||
slug: $slug
|
||||
title: $title
|
||||
status: open
|
||||
kind: $kind
|
||||
priority: $priority
|
||||
labels: $(labels_yaml "$labels")
|
||||
created_at: $created
|
||||
updated_at: $created
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
Created by tickets.sh.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- TBD
|
||||
EOF
|
||||
cat > "$dir/thread.md" <<EOF
|
||||
<!-- event: create author: tickets.sh at: $created -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
EOF
|
||||
printf '%s\n' "$id"
|
||||
}
|
||||
|
||||
cmd_comment() {
|
||||
[ "$#" -ge 1 ] || die "comment requires <id-or-slug>"
|
||||
query=$1
|
||||
shift
|
||||
role=comment
|
||||
author=${USER:-unknown}
|
||||
file=
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--role) [ "$#" -ge 2 ] || die "--role requires a value"; role=$2; shift 2 ;;
|
||||
--author) [ "$#" -ge 2 ] || die "--author requires a value"; author=$2; shift 2 ;;
|
||||
--file) [ "$#" -ge 2 ] || die "--file requires a value"; file=$2; shift 2 ;;
|
||||
*) die "unknown comment argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
dir=$(find_item_dir "$query")
|
||||
body=${TMPDIR:-/tmp}/tickets-sh.body.$$
|
||||
read_body_to_file "$file" "$body"
|
||||
heading=$role
|
||||
case "$role" in
|
||||
comment) heading="Comment" ;;
|
||||
plan) heading="Plan" ;;
|
||||
decision) heading="Decision" ;;
|
||||
implementation_report) heading="Implementation report" ;;
|
||||
esac
|
||||
append_thread_event "$dir" "$role" "$heading" "$author" "" "$body"
|
||||
rm -f "$body"
|
||||
}
|
||||
|
||||
cmd_review() {
|
||||
[ "$#" -ge 1 ] || die "review requires <id-or-slug>"
|
||||
query=$1
|
||||
shift
|
||||
author=${USER:-unknown}
|
||||
file=
|
||||
review_status=
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--approve) review_status=approve; shift ;;
|
||||
--request-changes) review_status=request_changes; shift ;;
|
||||
--author) [ "$#" -ge 2 ] || die "--author requires a value"; author=$2; shift 2 ;;
|
||||
--file) [ "$#" -ge 2 ] || die "--file requires a value"; file=$2; shift 2 ;;
|
||||
*) die "unknown review argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$review_status" ] || die "review requires --approve or --request-changes"
|
||||
dir=$(find_item_dir "$query")
|
||||
body=${TMPDIR:-/tmp}/tickets-sh.body.$$
|
||||
read_body_to_file "$file" "$body"
|
||||
if [ "$review_status" = approve ]; then
|
||||
heading="Review: approve"
|
||||
else
|
||||
heading="Review: request changes"
|
||||
fi
|
||||
append_thread_event "$dir" review "$heading" "$author" "$review_status" "$body"
|
||||
rm -f "$body"
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
[ "$#" -eq 2 ] || die "status requires <id-or-slug> <open|pending|closed>"
|
||||
query=$1
|
||||
new_status=$2
|
||||
is_status "$new_status" || die "invalid status: $new_status"
|
||||
dir=$(find_item_dir "$query")
|
||||
id=$(field_value "$dir/item.md" id)
|
||||
new_dir=$WORK_ITEMS_DIR/$new_status/$id
|
||||
ensure_backend_dirs
|
||||
if [ "$dir" != "$new_dir" ]; then
|
||||
[ ! -e "$new_dir" ] || die "target already exists: $new_dir"
|
||||
mv "$dir" "$new_dir"
|
||||
dir=$new_dir
|
||||
fi
|
||||
set_frontmatter_field "$dir/item.md" status "$new_status"
|
||||
set_frontmatter_field "$dir/item.md" updated_at "$(now_utc)"
|
||||
}
|
||||
|
||||
cmd_close() {
|
||||
[ "$#" -ge 1 ] || die "close requires <id-or-slug>"
|
||||
query=$1
|
||||
shift
|
||||
resolution=
|
||||
file=
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--resolution) [ "$#" -ge 2 ] || die "--resolution requires a value"; resolution=$2; shift 2 ;;
|
||||
--file) [ "$#" -ge 2 ] || die "--file requires a value"; file=$2; shift 2 ;;
|
||||
*) die "unknown close argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
cmd_status "$query" closed
|
||||
dir=$(find_item_dir "$query")
|
||||
body=${TMPDIR:-/tmp}/tickets-sh.body.$$
|
||||
if [ -n "$file" ]; then
|
||||
read_body_to_file "$file" "$body"
|
||||
elif [ -n "$resolution" ]; then
|
||||
printf '%s\n' "$resolution" > "$body"
|
||||
else
|
||||
printf 'Closed.\n' > "$body"
|
||||
fi
|
||||
cp "$body" "$dir/resolution.md"
|
||||
append_thread_event "$dir" close "Closed" "${USER:-unknown}" closed "$body"
|
||||
rm -f "$body"
|
||||
}
|
||||
|
||||
doctor_error() {
|
||||
printf 'doctor: %s\n' "$*" >&2
|
||||
DOCTOR_ERRORS=$((DOCTOR_ERRORS + 1))
|
||||
}
|
||||
|
||||
cmd_doctor() {
|
||||
DOCTOR_ERRORS=0
|
||||
for status in $STATUSES; do
|
||||
[ -d "$WORK_ITEMS_DIR/$status" ] || doctor_error "missing directory: $WORK_ITEMS_DIR/$status"
|
||||
done
|
||||
|
||||
ids=${TMPDIR:-/tmp}/tickets-sh.ids.$$
|
||||
slugs=${TMPDIR:-/tmp}/tickets-sh.slugs.$$
|
||||
: > "$ids"
|
||||
: > "$slugs"
|
||||
|
||||
for status in $STATUSES; do
|
||||
[ -d "$WORK_ITEMS_DIR/$status" ] || continue
|
||||
for dir in "$WORK_ITEMS_DIR/$status"/*; do
|
||||
[ -d "$dir" ] || continue
|
||||
item=$dir/item.md
|
||||
thread=$dir/thread.md
|
||||
artifacts=$dir/artifacts
|
||||
[ -f "$item" ] || { doctor_error "missing item.md: $dir"; continue; }
|
||||
[ -f "$thread" ] || doctor_error "missing thread.md: $dir"
|
||||
[ -d "$artifacts" ] || doctor_error "missing artifacts/: $dir"
|
||||
first=$(sed -n '1p' "$item")
|
||||
[ "$first" = "---" ] || doctor_error "item.md missing frontmatter opener: $item"
|
||||
for field in $REQUIRED_FIELDS; do
|
||||
value=$(field_value "$item" "$field" || true)
|
||||
[ -n "$value" ] || doctor_error "missing required field '$field': $item"
|
||||
done
|
||||
id=$(field_value "$item" id || true)
|
||||
slug=$(field_value "$item" slug || true)
|
||||
fm_status=$(field_value "$item" status || true)
|
||||
if [ -n "$id" ]; then
|
||||
printf '%s\t%s\n' "$id" "$item" >> "$ids"
|
||||
base=$(basename "$dir")
|
||||
[ "$base" = "$id" ] || doctor_error "directory id mismatch: $dir has id $id"
|
||||
fi
|
||||
if [ -n "$slug" ]; then
|
||||
printf '%s\t%s\n' "$slug" "$item" >> "$slugs"
|
||||
fi
|
||||
if [ "$fm_status" != "$status" ]; then
|
||||
doctor_error "status mismatch: $item has '$fm_status' under '$status'"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
dup_ids=$(cut -f1 "$ids" | sort | uniq -d)
|
||||
if [ -n "$dup_ids" ]; then
|
||||
old_ifs=$IFS
|
||||
IFS='
|
||||
'
|
||||
for dup in $dup_ids; do
|
||||
[ -n "$dup" ] && doctor_error "duplicate id: $dup"
|
||||
done
|
||||
IFS=$old_ifs
|
||||
fi
|
||||
dup_slugs=$(cut -f1 "$slugs" | sort | uniq -d)
|
||||
if [ -n "$dup_slugs" ]; then
|
||||
old_ifs=$IFS
|
||||
IFS='
|
||||
'
|
||||
for dup in $dup_slugs; do
|
||||
[ -n "$dup" ] && doctor_error "duplicate slug: $dup"
|
||||
done
|
||||
IFS=$old_ifs
|
||||
fi
|
||||
rm -f "$ids" "$slugs"
|
||||
|
||||
if [ -f TODO.md ] && grep -Eq 'tickets/[^][ )]+\.md|tickets/.*\.review\.md' TODO.md; then
|
||||
doctor_error "TODO.md still references legacy tickets/*.md"
|
||||
fi
|
||||
for f in tickets/*.md tickets/*.review.md; do
|
||||
[ -e "$f" ] || continue
|
||||
doctor_error "legacy ticket file remains: $f"
|
||||
done
|
||||
|
||||
if [ "$DOCTOR_ERRORS" -eq 0 ]; then
|
||||
printf 'doctor: ok\n'
|
||||
return 0
|
||||
fi
|
||||
printf 'doctor: %s error(s)\n' "$DOCTOR_ERRORS" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
main() {
|
||||
cmd=${1:-help}
|
||||
case "$cmd" in
|
||||
help|--help|-h) usage ;;
|
||||
list) shift; cmd_list "$@" ;;
|
||||
show) shift; cmd_show "$@" ;;
|
||||
create) shift; cmd_create "$@" ;;
|
||||
comment) shift; cmd_comment "$@" ;;
|
||||
review) shift; cmd_review "$@" ;;
|
||||
status) shift; cmd_status "$@" ;;
|
||||
close) shift; cmd_close "$@" ;;
|
||||
doctor) shift; [ "$#" -eq 0 ] || die "doctor takes no arguments"; cmd_doctor ;;
|
||||
*) usage >&2; die "unknown command: $cmd" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
|
@ -1,22 +1,3 @@
|
|||
---
|
||||
id: 20260527-000001-auto-maintain-workflow
|
||||
slug: auto-maintain-workflow
|
||||
title: 半自動開発運用 Workflow
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:01Z
|
||||
updated_at: 2026-05-27T00:00:01Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/auto-maintain-workflow.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/auto-maintain-workflow.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# 半自動開発運用 Workflow
|
||||
|
||||
## 背景
|
||||
|
|
@ -1,22 +1,3 @@
|
|||
---
|
||||
id: 20260527-000002-e2e-harness
|
||||
slug: e2e-harness
|
||||
title: E2E テストハーネス
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:02Z
|
||||
updated_at: 2026-05-27T00:00:02Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/e2e-harness.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/e2e-harness.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# E2E テストハーネス
|
||||
|
||||
## 背景
|
||||
|
|
@ -1,22 +1,3 @@
|
|||
---
|
||||
id: 20260527-000003-internal-worker-workflow
|
||||
slug: internal-worker-workflow
|
||||
title: 内部 Worker / 内部 Pod の Workflow 化
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:03Z
|
||||
updated_at: 2026-05-27T00:00:03Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/internal-worker-workflow.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/internal-worker-workflow.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# 内部 Worker / 内部 Pod の Workflow 化
|
||||
|
||||
## 背景
|
||||
284
tickets/maintainer-work-items.md
Normal file
284
tickets/maintainer-work-items.md
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
# AI maintainer 用 WorkItem / Thread 抽象
|
||||
|
||||
## 背景
|
||||
|
||||
現在の開発運用は `TODO.md` と `tickets/*.md` を中心に回している。これは Git 履歴で要件と完了条件を追うには十分だが、AI maintainer が単なる Coding Agent を超えて運用を担うには弱い。
|
||||
|
||||
特に、設計相談、実装 Pod の作業報告、review 指摘、修正依頼、完了判断、Pod run、lease、artifact が ticket file / review file / 会話 / git log に分散し、thread として扱いづらい。
|
||||
|
||||
shiguredo/http3-rs の `issues/` directory のように、repository 内に issue / work item を置く運用は参考になる。一方で、同 repository の owner も指摘している通り、中央の `SEQUENCE` ファイルによる連番採番は並列 branch / worktree で conflict しやすい。将来的な network 越し workspace / remote coordination も想定すると、最初から Git directory や中央採番前提で API を固めるべきではない。
|
||||
|
||||
本チケットでは、`tickets/` を直ちに置き換えるのではなく、AI maintainer が扱う上位の **WorkItem / Thread / Event / Lease / Artifact** 抽象を設計し、最小 file backend を導入できる状態にする。
|
||||
|
||||
## 方針
|
||||
|
||||
- WorkItem / Thread の正本は `.insomnia` ではなく、project-visible な repo-managed 領域に置く
|
||||
- `.insomnia` は local runtime state / memory / workflow / Pod run / lease cache の領域として分離する
|
||||
- API / domain model は Git に依存しない形にする
|
||||
- WorkItem ID は中央 `SEQUENCE` 連番ではなく、作成時刻ベースの衝突しにくい ID にする
|
||||
- 初期 backend は repo 内 directory(例: `work-items/` または `issues/`)でよい
|
||||
- network 越し workspace / remote hub は後回しにするが、将来差し替え可能な interface を先に切る
|
||||
- 既存 `tickets/` は当面維持し、WorkItem から link するか、file backend の一 view として扱えるようにする
|
||||
|
||||
## データ配置
|
||||
|
||||
初期設計では以下の分離を前提にする。
|
||||
|
||||
```text
|
||||
repo/
|
||||
work-items/ or issues/ # project-visible, git-managed coordination data
|
||||
tickets/ # 当面の既存 ticket
|
||||
docs/ # 設計・report
|
||||
.insomnia/ # local agent/runtime state
|
||||
memory/
|
||||
workflow/
|
||||
maintainer/
|
||||
leases/
|
||||
runs/
|
||||
inbox/
|
||||
```
|
||||
|
||||
### repo-managed に置くもの
|
||||
|
||||
- WorkItem description
|
||||
- acceptance criteria
|
||||
- discussion thread
|
||||
- design decision
|
||||
- review comment
|
||||
- status history
|
||||
- linked branch / worktree / commit
|
||||
- durable artifact metadata
|
||||
|
||||
### `.insomnia` に置くもの
|
||||
|
||||
- Pod run state
|
||||
- lease の local cache
|
||||
- SpawnedPod polling cursor
|
||||
- temporary inbox
|
||||
- local-only trial log
|
||||
- model / role runtime state
|
||||
|
||||
## WorkItem ID
|
||||
|
||||
WorkItem ID は identity のためだけに使い、priority や処理順序を背負わせない。`SEQUENCE` のような中央連番ファイルは、複数 branch / worktree / Pod が同時に WorkItem を作ると conflict しやすいため採用しない。
|
||||
|
||||
初期 file backend では、directory name を immutable ID として扱う。
|
||||
|
||||
```text
|
||||
YYYYMMDD-HHMMSS-<slug>
|
||||
YYYYMMDD-HHMMSS-<short-rand>-<slug> # 同一秒衝突を避けたい場合
|
||||
```
|
||||
|
||||
例:
|
||||
|
||||
```text
|
||||
20260510-184233-maintainer-work-items
|
||||
20260510-184233-a1b2-maintainer-work-items
|
||||
```
|
||||
|
||||
要件:
|
||||
|
||||
- lexical sort で概ね作成順になる
|
||||
- 中央採番ファイルを更新しない
|
||||
- collision 時は backend が短い random suffix や retry で解決する
|
||||
- human-visible `slug` / `title` と immutable `id` を分ける
|
||||
- priority / status / scheduling は `id` ではなく metadata で表す
|
||||
- 将来 remote backend に移る場合も ID 生成責務は backend 側に閉じ込める
|
||||
|
||||
## WorkItem model
|
||||
|
||||
最低限、以下の概念を持つ。
|
||||
|
||||
```text
|
||||
WorkItem
|
||||
- id
|
||||
- slug
|
||||
- title
|
||||
- status
|
||||
- kind: feature | bug | refactor | design | ops | investigation
|
||||
- priority / labels
|
||||
- owner / assignee / current lease summary
|
||||
- acceptance criteria
|
||||
- linked ticket / docs / branches / worktrees / commits
|
||||
- thread events
|
||||
- artifacts
|
||||
```
|
||||
|
||||
```text
|
||||
ThreadEvent
|
||||
- id
|
||||
- work_item_id
|
||||
- occurred_at
|
||||
- author: human | orchestrator | pod:<name>
|
||||
- role: comment | plan | decision | implementation_report | review | status_change | escalation | artifact
|
||||
- reply_to?
|
||||
- body
|
||||
- links
|
||||
```
|
||||
|
||||
```text
|
||||
Lease
|
||||
- id
|
||||
- work_item_id
|
||||
- holder
|
||||
- scope hint
|
||||
- worktree path
|
||||
- expires_at
|
||||
- status: active | released | expired
|
||||
```
|
||||
|
||||
Lease はリアルタイム coordination に近いため、Git-managed thread の正本とは分ける。file backend 初期実装では `.insomnia/maintainer/leases/` か local DB を使ってよい。
|
||||
|
||||
## backend interface
|
||||
|
||||
AI maintainer / `/auto-maintain` は file path を直接前提にせず、概念上は store interface を通す。
|
||||
|
||||
```text
|
||||
WorkItemStore
|
||||
- list(filter)
|
||||
- get(id)
|
||||
- create(item)
|
||||
- append_event(id, event)
|
||||
- update_status(id, status)
|
||||
- attach_artifact(id, artifact)
|
||||
```
|
||||
|
||||
```text
|
||||
LeaseStore
|
||||
- acquire(work_item_id, holder, scope, ttl)
|
||||
- refresh(lease_id)
|
||||
- release(lease_id)
|
||||
- list_active(filter)
|
||||
```
|
||||
|
||||
初期 backend 候補:
|
||||
|
||||
- `file://<repo>/work-items`
|
||||
- `file://<repo>/issues`
|
||||
- `sqlite://<workspace>/.insomnia/maintainer/work-items.db`
|
||||
|
||||
将来 backend 候補:
|
||||
|
||||
- `http://maintainer-hub/...`
|
||||
- `github://owner/repo/issues`
|
||||
|
||||
## Query / listing
|
||||
|
||||
初期 file backend の list / query は、index や DB を作らず全 `item.md` frontmatter scan で行う。
|
||||
|
||||
対象:
|
||||
|
||||
```text
|
||||
work-items/{open,pending,closed}/*/item.md
|
||||
```
|
||||
|
||||
`item.md` の frontmatter に query 用 metadata を集約する。
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: 20260510-184233-a1b2-maintainer-work-items
|
||||
slug: maintainer-work-items
|
||||
title: AI maintainer 用 WorkItem / Thread 抽象
|
||||
status: open
|
||||
kind: design
|
||||
priority: P2
|
||||
labels: [maintainer, workflow]
|
||||
created_at: 2026-05-10T18:42:33Z
|
||||
updated_at: 2026-05-10T19:10:00Z
|
||||
assignee: null
|
||||
---
|
||||
```
|
||||
|
||||
`WorkItemStore::list` / `query` は frontmatter だけを読み、summary を返す。
|
||||
|
||||
- status / kind / labels / priority / assignee で filter する
|
||||
- title / slug / description excerpt の軽い substring query を提供する
|
||||
- sort は priority / updated_at / created_at を metadata で行う
|
||||
- `thread.jsonl` は一覧では読まず、`get(id)` / thread read 時だけ読む
|
||||
- `updated_at` は item metadata として持ち、必要なら thread append 時に更新する
|
||||
|
||||
当面の件数では全件 scan で十分であり、AI に directory を探索させて候補を推測させない。index / SQLite / search daemon は件数増加、全文検索、remote backend 同期が必要になった時点で検討する。
|
||||
|
||||
### status の二重管理
|
||||
|
||||
file backend では directory と frontmatter の両方に status が出る。
|
||||
|
||||
```text
|
||||
work-items/open/<id>/item.md
|
||||
frontmatter: status: open
|
||||
```
|
||||
|
||||
これは人間の `ls` と backend abstraction の両方を成立させるため許容する。ただし linter / doctor で一致確認する。
|
||||
|
||||
- `work-items/open/*/item.md` は `status: open`
|
||||
- `work-items/pending/*/item.md` は `status: pending`
|
||||
- `work-items/closed/*/item.md` は `status: closed`
|
||||
|
||||
不一致は warning ではなく error とする。
|
||||
|
||||
## 初期 file backend 案
|
||||
|
||||
```text
|
||||
work-items/
|
||||
open/
|
||||
20260510-184233-maintainer-work-items/
|
||||
item.md
|
||||
thread.jsonl
|
||||
artifacts/
|
||||
review.md
|
||||
test-log.txt
|
||||
pending/
|
||||
20260510-190102-transport-parameter-api/
|
||||
item.md
|
||||
thread.jsonl
|
||||
artifacts/
|
||||
closed/
|
||||
20260510-201522-anthropic-burst-bundling/
|
||||
item.md
|
||||
thread.jsonl
|
||||
resolution.md
|
||||
artifacts/
|
||||
```
|
||||
|
||||
`item.md` は human-readable な issue 本文(背景、根拠、完了条件、非目標など)を持つ。`thread.jsonl` は append-only を基本にし、AI maintainer が conversation / decision / review / status change を追いやすい形にする。`resolution.md` は close 時の解決方法や検証結果を、人間が読みやすい形でまとめる任意ファイルとする。
|
||||
|
||||
## `/auto-maintain` との関係
|
||||
|
||||
`/auto-maintain` は当面 `TODO.md` / `tickets/` を読むが、将来的には WorkItemStore を入口にする。
|
||||
|
||||
移行方針:
|
||||
|
||||
1. 既存 `tickets/` は維持
|
||||
2. WorkItem 抽象と file backend schema を設計する
|
||||
3. 新しい設計相談・並列作業・長い thread が必要な作業だけ WorkItem 化する
|
||||
4. ticket は WorkItem の linked artifact として扱う
|
||||
5. `work-items/open/` が安定したら、`TODO.md` は generated view または廃止候補にする
|
||||
6. 十分に安定したら `tickets/` を WorkItem backend の view に寄せる
|
||||
|
||||
## 範囲外
|
||||
|
||||
- remote maintainer hub の実装
|
||||
- index / SQLite / search daemon による query 最適化
|
||||
- 既存 `tickets/` の即時移行
|
||||
- 常駐 scheduler
|
||||
- Pod lifecycle / completion tracking の完全実装
|
||||
- scope owner handoff の再設計
|
||||
|
||||
## 完了条件
|
||||
|
||||
- WorkItem / Thread / Event / Lease / Artifact の domain model が docs に定義されている
|
||||
- repo-managed coordination data と `.insomnia` local runtime state の分担が明文化されている
|
||||
- `WorkItemStore` / `LeaseStore` 相当の interface 方針が決まっている
|
||||
- list / query は初期実装では全 `item.md` frontmatter scan で行う方針になっている
|
||||
- `thread.jsonl` は一覧では読まず、詳細 read 時だけ読む方針になっている
|
||||
- directory status と frontmatter status の一致を linter / doctor で確認する方針になっている
|
||||
- WorkItem ID scheme が中央連番ではなく timestamp-based になっている
|
||||
- 初期 file backend の directory schema が決まっている
|
||||
- `/auto-maintain` / AI maintainer が将来 WorkItemStore を入口にできる移行方針が書かれている
|
||||
- network 越し workspace / remote hub は後回しにしつつ、backend 差し替え可能性を潰していない
|
||||
|
||||
## 参照
|
||||
|
||||
- `docs/plan/ai-maintainer.md`
|
||||
- `tickets/auto-maintain-workflow.md`
|
||||
- `docs/report/2026-05-10-ticket-lifecycle-branch-placement.md`
|
||||
|
|
@ -1,22 +1,3 @@
|
|||
---
|
||||
id: 20260527-000004-manual-turn-rollback
|
||||
slug: manual-turn-rollback
|
||||
title: Pod/TUI: 手動 rollback 導線
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:04Z
|
||||
updated_at: 2026-05-27T00:00:04Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/manual-turn-rollback.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/manual-turn-rollback.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# Pod/TUI: 手動 rollback 導線
|
||||
|
||||
## 背景
|
||||
64
tickets/memory-consolidation-skip-observability.md
Normal file
64
tickets/memory-consolidation-skip-observability.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# メモリ機構: consolidation skip 表示と invalid staging の観測性
|
||||
|
||||
## 背景
|
||||
|
||||
`memory-audit-log` 実装後、TUI actionbar に `memory consolidation skipped: no_staging_entries` が表示されるようになった。これは正常な idle/no-op でも頻繁に発生するため、actionbar 上では「何かが skip され続けている」ように見える。
|
||||
|
||||
また、workspace に `.insomnia/memory/_staging/*.json` が残っているのに、現行 schema として読めないため valid staging entry が 0 件になり、`no_staging_entries` と区別できない場合がある。今回観測された主因は旧 schema の staging file で、`source.session_id` を持ち、現行 schema が要求する `source.segment_id` を持たないものだった。
|
||||
|
||||
旧 schema staging の後方互換・migration は求めない。この workspace に残っている旧 staging は必要に応じて手動で削除 / 退避する。実装としては、壊れた staging / 現行 schema として読めない staging がある場合に、`no_staging_entries` と誤認しない観測性だけを持たせる。
|
||||
|
||||
## 方針
|
||||
|
||||
Audit log には worker の skip / no-op を残してよいが、actionbar には人間が見る価値のある memory worker 状態だけを出す。
|
||||
|
||||
特に以下は actionbar に出さない。
|
||||
|
||||
- `no_staging_entries`
|
||||
- successful consolidation の直後、backlog drain 終端確認で出る空確認 skip
|
||||
- post-run / periodic check で staging が本当に空だっただけの skip
|
||||
- 通常運用で頻出する no-op / idle skip
|
||||
|
||||
一方で、以下は actionbar に出してよい。
|
||||
|
||||
- consolidation の started / completed
|
||||
- record changes を伴う completed
|
||||
- failed / error
|
||||
- invalid staging が存在するなど、人間が確認すべき状態
|
||||
|
||||
staging directory に file があるが current schema として読めない場合は、audit log 上で invalid staging の存在と件数が分かるようにする。actionbar に出す場合も、`no_staging_entries` ではなく invalid staging と分かる文言にする。
|
||||
|
||||
## 要件
|
||||
|
||||
- consolidation が `completed` した直後の drain 確認で発生する idle skip が、直前の actionbar 表示を上書きしない。
|
||||
- 例: `completed_record_changes` の直後に `no_staging_entries` を actionbar へ出さない。
|
||||
- audit log へ残すかどうかは実装判断でよいが、UI event としては抑制する。
|
||||
- 通常の post-run check / periodic check で staging が本当に空の場合も、actionbar に `no_staging_entries` を出さない。
|
||||
- idle/no-op 状態は audit log 側で確認できればよい。
|
||||
- `threshold_not_reached` は通常運用の no-op として扱い、actionbar へ常時表示しない。
|
||||
- audit log 側では `no_staging_entries` と区別して記録する。
|
||||
- `list_staging_entries` あるいはその呼び出し側で、invalid / parse-failed staging file の存在を区別できるようにする。
|
||||
- 少なくとも invalid count が audit reason または structured field から分かる。
|
||||
- 例: `no_valid_staging_entries invalid=6`。
|
||||
- `threshold_not_reached` と `no_valid_staging_entries` / `invalid_staging_entries` は区別される。
|
||||
- old schema staging file の自動 migration / 自動削除 / 自動 archive はしない。
|
||||
- 後方互換は不要。
|
||||
- 既存 workspace の旧 staging は手動整理でよい。
|
||||
- 実装側では、現行 schema として読めない staging を invalid として観測できればよい。
|
||||
|
||||
## 完了条件
|
||||
|
||||
- successful consolidation の直後に actionbar が `no_staging_entries` で上書きされない。
|
||||
- staging が本当に空の periodic/post-run check は actionbar にノイズを出さない。
|
||||
- `threshold_not_reached` が actionbar を継続的に上書きしない。
|
||||
- staging directory に parse 不能な `.json` がある場合、audit log が `no_staging_entries` ではなく invalid staging の存在を示す。
|
||||
- invalid staging を actionbar に出す場合、`no_staging_entries` ではなく invalid staging と分かる表示になる。
|
||||
- consolidation skip / invalid staging / actionbar 抑制の挙動を確認する test がある。
|
||||
- `cargo fmt --check` と関連 crate の test が通る。
|
||||
|
||||
## 範囲外
|
||||
|
||||
- old schema staging file の自動 migration。
|
||||
- old schema staging file の自動削除 / archive。
|
||||
- actionbar transient notice 汎用 API の実装。
|
||||
- memory audit log の保存形式の大幅変更。
|
||||
104
tickets/memory-summary-resident-injection.md
Normal file
104
tickets/memory-summary-resident-injection.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# メモリ機構: summary.md の resident 注入
|
||||
|
||||
## 背景
|
||||
|
||||
`memory/summary.md` は durable memory の圧縮サマリとして設計されているが、現状の通常 Pod では system prompt に自動注入されていない。
|
||||
|
||||
現在 `summary.md` が使われる経路は主に以下である。
|
||||
|
||||
- `MemoryRead(kind=summary)` / `MemoryQuery` などの tool 経由。
|
||||
- memory consolidation worker の入力。既存 `summary.md` / `decisions/*` / `requests/*` が consolidation prompt に渡され、必要なら rewrite される。
|
||||
- linter / audit / usage metrics の対象。
|
||||
|
||||
一方で、通常 Pod の resident injection は `.insomnia/knowledge/*.md` のうち `model_invokation: true` な Knowledge description と resident Workflow description に限られている。`summary.md` は LLM が自発的に `MemoryRead` / `MemoryQuery` しない限り参照されない。
|
||||
|
||||
memory は「思い出してほしい情報」であり、短く保たれた summary は tool discovery に任せるより、通常 Pod の context に常駐させた方が活用されやすい。オン/オフ可能にした上で、`summary.md` を resident context として注入する。
|
||||
|
||||
## 方針
|
||||
|
||||
`summary.md` の本文を、通常 Pod の system prompt trailing section に resident memory summary として注入する。
|
||||
|
||||
- `[memory]` が有効な Pod だけが対象。
|
||||
- `memory/summary.md` が存在し、valid な summary record として読める場合だけ注入する。
|
||||
- frontmatter は注入しない。body のみを注入する。
|
||||
- 空 body は注入しない。
|
||||
- consolidation / compaction などの disposable internal Worker には注入しない。
|
||||
- 既存の `set_resident_knowledge_injection(false)` 相当の opt-out と同じ考え方に揃える。
|
||||
- resident Knowledge / resident Workflow と同じく、system prompt materialization 時に一度だけ読む。
|
||||
- turn ごとに history へ system item を append しない。
|
||||
- context だけに揮発的に差し込む実装は禁止。system prompt の materialized text として扱う。
|
||||
|
||||
## 設定
|
||||
|
||||
オン/オフは manifest で制御する。
|
||||
|
||||
候補名:
|
||||
|
||||
```toml
|
||||
[memory]
|
||||
inject_summary = true
|
||||
```
|
||||
|
||||
- default は `true`。
|
||||
- `[memory]` が無い場合は無効。
|
||||
- `inject_summary = false` で明示的に無効化できる。
|
||||
|
||||
既存の `inject_resident_knowledge` は Pod の内部 opt-out API として残し、manifest の `inject_summary` は memory summary の注入だけを制御する。
|
||||
|
||||
## 注入形式
|
||||
|
||||
System prompt の resident section に、Knowledge / Workflow とは別の明確な block として入れる。
|
||||
|
||||
例:
|
||||
|
||||
```text
|
||||
## Resident memory summary
|
||||
|
||||
The following is the workspace-local durable memory summary. Treat it as helpful context, not as authoritative over current user instructions, tickets, files, or git state.
|
||||
|
||||
<summary body>
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- summary は stale になり得る。現行の user instruction / tickets / files / git / session log を上書きする権威として扱わせない。
|
||||
- summary が大きすぎる場合は hard error にしない。初期実装では soft cap で切り詰めるか、既存 linter / consolidation prompt の「1-5k tokens 目安」に依存してよい。
|
||||
- ただし注入時に上限を設ける場合は、切り詰めたことが分かる注記を入れる。
|
||||
|
||||
## usage metrics
|
||||
|
||||
resident summary injection は「受動的露出」であり、`MemoryRead(kind=summary)` のような明示使用とは区別する。
|
||||
|
||||
- 既存の memory usage metrics が明示 tool use を測っている場合、resident injection を `use_count` として混ぜない。
|
||||
- 必要なら resident exposure として別イベント / 別 source に記録する。
|
||||
- 既存の resident Knowledge / resident Workflow exposure 記録があるなら、それに合わせる。
|
||||
|
||||
## 要件
|
||||
|
||||
- `[memory]` が有効で `memory/summary.md` が存在する場合、通常 Pod の system prompt に summary body が注入される。
|
||||
- summary frontmatter は注入されない。
|
||||
- `inject_summary = false` の場合は注入されない。
|
||||
- `[memory]` が無い Pod では注入されない。
|
||||
- summary が存在しない / parse 不能 / body 空の場合、Pod 起動や run は失敗しない。
|
||||
- internal disposable Worker には summary resident injection されない。
|
||||
- resident injection は `worker.history` へ新しい item を append しない。
|
||||
- summary resident exposure は明示 tool read usage と混同されない。
|
||||
- stale summary が current project authority を上書きしないよう、注入文言で優先順位を明示する。
|
||||
|
||||
## 完了条件
|
||||
|
||||
- system prompt render test で summary body が resident block として入ることを確認する。
|
||||
- frontmatter が入らない test がある。
|
||||
- `inject_summary = false` で入らない test がある。
|
||||
- `[memory]` 無しで入らない test がある。
|
||||
- malformed summary でも run / render が失敗しない test がある。
|
||||
- internal Worker opt-out の既存経路を壊さない test、または該当コード確認がある。
|
||||
- `cargo fmt --check` と関連 crate の test が通る。
|
||||
|
||||
## 範囲外
|
||||
|
||||
- summary の自動圧縮 / token budget 再設計。
|
||||
- summary を turn ごとに動的 refresh する仕組み。
|
||||
- `MemoryRead(kind=summary)` の挙動変更。
|
||||
- global memory / project local memory の store 分離。
|
||||
- memory の git 運用方針変更。
|
||||
|
|
@ -1,22 +1,3 @@
|
|||
---
|
||||
id: 20260527-000006-permission-default-policy
|
||||
slug: permission-default-policy
|
||||
title: Permission: allow-all 既定 policy への整理
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:06Z
|
||||
updated_at: 2026-05-27T00:00:06Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/permission-default-policy.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/permission-default-policy.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# Permission: allow-all 既定 policy への整理
|
||||
|
||||
## 背景
|
||||
|
|
@ -1,22 +1,3 @@
|
|||
---
|
||||
id: 20260527-000007-pod-inbound-pod-event-dedup
|
||||
slug: pod-inbound-pod-event-dedup
|
||||
title: Inbound PodEvent ハンドリングの重複を統合する
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:07Z
|
||||
updated_at: 2026-05-27T00:00:07Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/pod-inbound-pod-event-dedup.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/pod-inbound-pod-event-dedup.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# Inbound PodEvent ハンドリングの重複を統合する
|
||||
|
||||
## 背景
|
||||
|
|
@ -1,22 +1,3 @@
|
|||
---
|
||||
id: 20260527-000009-pod-session-fork
|
||||
slug: pod-session-fork
|
||||
title: Pod: 任意ターンからの Fork(複数ターン巻き戻し)
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:09Z
|
||||
updated_at: 2026-05-27T00:00:09Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/pod-session-fork.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/pod-session-fork.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# Pod: 任意ターンからの Fork(複数ターン巻き戻し)
|
||||
|
||||
## 背景
|
||||
|
|
@ -1,27 +1,8 @@
|
|||
---
|
||||
id: 20260527-000010-prompt-eval-metrics
|
||||
slug: prompt-eval-metrics
|
||||
title: Prompt / Workflow 評価メトリクスと改善 Offer
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:10Z
|
||||
updated_at: 2026-05-27T00:00:10Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/prompt-eval-metrics.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/prompt-eval-metrics.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# Prompt / Workflow 評価メトリクスと改善 Offer
|
||||
|
||||
## 背景
|
||||
|
||||
empirical prompt tuning pattern は、agent-facing な指示(Skill / slash command / prompt 等)を新規 subagent に実行させ、実行者の自己申告と指示側メトリクスを突き合わせて反復改善する手法である。insomnia では Workflow / Skill ingest / Knowledge / memory consolidation / usage metrics / Pod orchestration があるため、この手法を単なる「手順」ではなく、**agent-facing instruction の品質観測 pipeline** として扱える。
|
||||
mizchi の empirical-prompt-tuning は、agent-facing な指示(Skill / slash command / prompt 等)を新規 subagent に実行させ、実行者の自己申告と指示側メトリクスを突き合わせて反復改善する手法である。insomnia では Workflow / Skill ingest / Knowledge / memory consolidation / usage metrics / Pod orchestration があるため、この手法を単なる「手順」ではなく、**agent-facing instruction の品質観測 pipeline** として扱える。
|
||||
|
||||
特に insomnia では以下をシステム側で観測できる。
|
||||
|
||||
|
|
@ -159,7 +140,7 @@ Claude Code 版の `tool_uses` を、insomnia では tool 種別ごとの偏り
|
|||
|
||||
## 参照
|
||||
|
||||
- empirical prompt tuning skill example(外部参照。取り込み時は必要最小限に一般化する)
|
||||
- `/home/hare/ghq/github.com/mizchi/skills/empirical-prompt-tuning/SKILL.md`(外部参照。取り込み時は必要最小限に一般化する)
|
||||
- `docs/plan/workflow.md`
|
||||
- `docs/plan/memory.md`
|
||||
- `tickets/memory-usage-metrics.md`
|
||||
|
|
@ -1,22 +1,3 @@
|
|||
---
|
||||
id: 20260527-000011-session-todo-reminder
|
||||
slug: session-todo-reminder
|
||||
title: セッション内 Task ツールの注意機構
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:11Z
|
||||
updated_at: 2026-05-27T00:00:11Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/session-todo-reminder.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/session-todo-reminder.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# セッション内 Task ツールの注意機構
|
||||
|
||||
## 背景
|
||||
|
|
@ -27,8 +8,7 @@ legacy_ticket: tickets/session-todo-reminder.md
|
|||
- 「やったつもり」になって `completed` への更新を忘れる
|
||||
- そもそも TaskStore の存在を忘れて、構造化を諦めて自由記述に回帰する
|
||||
|
||||
OpenCode の todo は専用の注意機構を持たない(汎用 reminder 経由)。一方、一部の既存エージェント実装では todo reminder を「N リクエスト無アクティビティで初めて発火するナッジ型」として扱い、毎リクエスト押し戻しはしない。
|
||||
|
||||
OpenCode の todo は専用の注意機構を持たない(汎用 reminder 経由)。一方 Claude Code は `task_reminder` を「N リクエスト無アクティビティで初めて発火するナッジ型」として実装しており、毎リクエスト押し戻しはしない(`/home/hare/.local/share/claude/versions/2.x` の `du_` / `cu_` 関数、閾値 `Z_8 = { TURNS_SINCE_WRITE: 10, TURNS_BETWEEN_REMINDERS: 10 }`)。
|
||||
|
||||
Insomnia でも同方針を採り、active Task が残っているのに `TaskCreate` / `TaskUpdate` が一定リクエスト呼ばれていない場合に限り、`<system-reminder>` Item を 1件 history に append する。「やったつもり」抑止と、トークン浪費・LLM の自律性侵害のバランスを取るため、毎リクエスト押し戻しはしない。
|
||||
|
||||
|
|
@ -77,4 +57,4 @@ Insomnia でも同方針を採り、active Task が残っているのに `TaskCr
|
|||
|
||||
- 設計指針: `AGENTS.md`(LLM コンテキストの加工原則。揮発的注入は禁止、history に append してから commit する)
|
||||
- 前提: `tickets/session-todo.md`(Tool 群と TaskStore)、`tickets/notify-history-persist.md`(`pending_history_appends` レーン)
|
||||
- 参考: 一部エージェント実装の todo reminder は、一定リクエスト無アクティビティ後に発火し、再通知にも cooldown を置くナッジ型として扱われている
|
||||
- 参考実装: Claude Code の `task_reminder`(`du_` / `cu_` 関数、閾値 `Z_8 = { TURNS_SINCE_WRITE: 10, TURNS_BETWEEN_REMINDERS: 10 }`)
|
||||
|
|
@ -1,22 +1,3 @@
|
|||
---
|
||||
id: 20260527-000012-spawnpod-initial-run-confirmation
|
||||
slug: spawnpod-initial-run-confirmation
|
||||
title: SpawnPod: initial Run delivery confirmation
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:12Z
|
||||
updated_at: 2026-05-27T00:00:12Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/spawnpod-initial-run-confirmation.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/spawnpod-initial-run-confirmation.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# SpawnPod: initial Run delivery confirmation
|
||||
|
||||
## 背景
|
||||
|
|
@ -25,9 +6,9 @@ legacy_ticket: tickets/spawnpod-initial-run-confirmation.md
|
|||
|
||||
確認された状態:
|
||||
|
||||
- `<runtime-dir>/pods.json` に live allocation がある
|
||||
- `<runtime-dir>/<pod>/status.json` は `state: "idle"` と runtime `segment_id` を持つ
|
||||
- `<insomnia-sessions>/pods/<pod>/metadata.json` は pending segment のまま
|
||||
- `/run/user/1000/insomnia/pods.json` に live allocation がある
|
||||
- `/run/user/1000/insomnia/<pod>/status.json` は `state: "idle"` と runtime `segment_id` を持つ
|
||||
- `/home/hare/.insomnia/sessions/pods/<pod>/metadata.json` は pending segment のまま
|
||||
- 対応する session / segment `.jsonl` が存在しない
|
||||
- `ReadPodOutput` は no new assistant text
|
||||
|
||||
|
|
@ -1,22 +1,3 @@
|
|||
---
|
||||
id: 20260527-000014-tui-actionbar-transient-notice-api
|
||||
slug: tui-actionbar-transient-notice-api
|
||||
title: TUI: actionbar transient notice API
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:14Z
|
||||
updated_at: 2026-05-27T00:00:14Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/tui-actionbar-transient-notice-api.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/tui-actionbar-transient-notice-api.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# TUI: actionbar transient notice API
|
||||
|
||||
## 背景
|
||||
|
|
@ -1,22 +1,3 @@
|
|||
---
|
||||
id: 20260527-000015-tui-navigation-mode-design
|
||||
slug: tui-navigation-mode-design
|
||||
title: TUI: navigation mode / block focus の設計
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:15Z
|
||||
updated_at: 2026-05-27T00:00:15Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/tui-navigation-mode-design.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/tui-navigation-mode-design.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# TUI: navigation mode / block focus の設計
|
||||
|
||||
## 背景
|
||||
|
|
@ -1,22 +1,3 @@
|
|||
---
|
||||
id: 20260527-000016-tui-picker-live-pending-pods
|
||||
slug: tui-picker-live-pending-pods
|
||||
title: TUI picker: live pending Pod の表示優先と状態補完
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:16Z
|
||||
updated_at: 2026-05-27T00:00:16Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/tui-picker-live-pending-pods.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/tui-picker-live-pending-pods.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# TUI picker: live pending Pod の表示優先と状態補完
|
||||
|
||||
## 背景
|
||||
|
|
@ -1,22 +1,3 @@
|
|||
---
|
||||
id: 20260527-000017-tui-spawned-pod-panel
|
||||
slug: tui-spawned-pod-panel
|
||||
title: TUI: spawned child Pod の一覧と一時 attach
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:17Z
|
||||
updated_at: 2026-05-27T00:00:17Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/tui-spawned-pod-panel.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/tui-spawned-pod-panel.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# TUI: spawned child Pod の一覧と一時 attach
|
||||
|
||||
## 背景
|
||||
47
tickets/tui-user-manifest-env-overlay.md
Normal file
47
tickets/tui-user-manifest-env-overlay.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# TUI spawn の user manifest env override と scope overlay の前提ズレ
|
||||
|
||||
## 背景
|
||||
|
||||
`pod` CLI は `tickets/pod-cli-manifest-flags.md` で `--user-manifest` を廃止し、`INSOMNIA_USER_MANIFEST` によって user manifest のパスを上書きできるようになった。
|
||||
|
||||
一方、TUI の spawn dialog は Pod 起動前に user / project manifest を先読みし、既存 cascade に `scope.allow` があるかどうかを見て、spawn overlay に cwd scope を補完するかを判断している。現状この先読みは `manifest::user_manifest_path()` に依存しており、`INSOMNIA_USER_MANIFEST` による user manifest override と一致しない可能性がある。
|
||||
|
||||
Pod の最終起動自体は `pod --overlay <toml>` 経由で行われるため、Pod 側では `INSOMNIA_USER_MANIFEST` が有効になる。しかし TUI が作る overlay は、別の user manifest を前提に組まれる可能性がある。
|
||||
|
||||
## 問題
|
||||
|
||||
`INSOMNIA_USER_MANIFEST` が設定されている場合、TUI spawn dialog が作成する scope overlay が実際に Pod CLI が読む manifest cascade とズレる可能性がある。
|
||||
|
||||
例:
|
||||
|
||||
- override された user manifest には `scope.allow` があるが、通常の XDG user manifest には無い
|
||||
- TUI は「cascade に scope が無い」と判断して cwd scope を overlay に追加する
|
||||
- 実際の Pod は override user manifest の scope と TUI overlay の cwd scope を両方読む
|
||||
- 通常の XDG user manifest には `scope.allow` があるが、override された user manifest には無い
|
||||
- TUI は「cascade に scope がある」と判断して cwd scope を overlay に追加しない
|
||||
- 実際の Pod は override user manifest を読むため、期待より scope が狭い、または空になる可能性がある
|
||||
|
||||
この問題は spawn 段階で manifest が必須という意味ではなく、TUI が spawn overlay を補完するために行う cascade の事前見積もりが Pod CLI の manifest 解決規則と一致していない、という問題である。
|
||||
|
||||
## 要件
|
||||
|
||||
本チケットでは問題の記録を目的とする。対応方針はまだ決めない。
|
||||
|
||||
対応時には少なくとも以下を確認する:
|
||||
|
||||
- TUI spawn dialog が参照する user manifest 解決規則と、Pod CLI の `INSOMNIA_USER_MANIFEST` 解決規則の関係
|
||||
- TUI が cwd scope を overlay に補完する条件が、実際の Pod 起動時 cascade と一致しているか
|
||||
- `INSOMNIA_USER_MANIFEST` が空文字列の場合の扱い
|
||||
- TUI 表示上の scope origin 表示が、実際に Pod が読む manifest と矛盾しないか
|
||||
|
||||
## 完了条件
|
||||
|
||||
- `INSOMNIA_USER_MANIFEST` 設定時に、TUI spawn dialog の scope overlay 補完が Pod CLI の実際の manifest cascade と矛盾しない
|
||||
- TUI spawn dialog の表示・判断に使う user manifest 解決規則がコード上明確になっている
|
||||
- 必要なテストが追加されている
|
||||
|
||||
## 範囲外
|
||||
|
||||
- TUI CLI フラグ全体のドキュメント整備
|
||||
- Pod CLI の manifest flag 仕様変更
|
||||
- `--project` や project manifest の env override 新設
|
||||
|
|
@ -1,22 +1,3 @@
|
|||
---
|
||||
id: 20260527-000018-tui-user-model-setup
|
||||
slug: tui-user-model-setup
|
||||
title: TUI: ユーザーマニフェストのモデル設定 wizard
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:18Z
|
||||
updated_at: 2026-05-27T00:00:18Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/tui-user-model-setup.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/tui-user-model-setup.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# TUI: ユーザーマニフェストのモデル設定 wizard
|
||||
|
||||
## 背景
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
# Work Items backend
|
||||
|
||||
`work-items/` is the canonical file backend for repository work after the migration from `TODO.md` and `tickets/*.md`.
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
work-items/
|
||||
README.md
|
||||
open/
|
||||
<id>/
|
||||
item.md
|
||||
thread.md
|
||||
artifacts/
|
||||
pending/
|
||||
<id>/
|
||||
item.md
|
||||
thread.md
|
||||
artifacts/
|
||||
closed/
|
||||
<id>/
|
||||
item.md
|
||||
thread.md
|
||||
resolution.md
|
||||
artifacts/
|
||||
```
|
||||
|
||||
`<id>` is timestamp based: `YYYYMMDD-HHMMSS-<slug>`. There is no central sequence file.
|
||||
|
||||
## `item.md` schema
|
||||
|
||||
`item.md` is Markdown with YAML-like frontmatter. The MVP parser intentionally supports only simple `key: value` lines.
|
||||
|
||||
Required fields:
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: 20260527-000001-example
|
||||
slug: example
|
||||
title: Human-readable title
|
||||
status: open
|
||||
kind: feature
|
||||
priority: P2
|
||||
labels: [maintainer, workflow]
|
||||
created_at: 2026-05-27T00:00:01Z
|
||||
updated_at: 2026-05-27T00:00:01Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/example.md
|
||||
---
|
||||
```
|
||||
|
||||
`status` must match the containing directory (`open`, `pending`, or `closed`). `legacy_ticket` records the migrated source path when one existed; use `null` for items created from a TODO-only entry or new work.
|
||||
|
||||
## `thread.md` schema
|
||||
|
||||
`thread.md` is an append-only Markdown event log. `tickets.sh comment`, `tickets.sh review`, and `tickets.sh close` append an HTML-comment event header, a Markdown heading, the event body, and a `---` separator.
|
||||
|
||||
Example:
|
||||
|
||||
```md
|
||||
<!-- event: review author: orchestrator at: 2026-05-27T12:00:00Z status: request_changes -->
|
||||
|
||||
## Review: request changes
|
||||
|
||||
Review notes.
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
Review state belongs in `thread.md`; new `tickets/*.review.md` files should not be created.
|
||||
|
||||
## Commands
|
||||
|
||||
Use `../tickets.sh --help` from this repository root for the command reference. The script performs file operations only; it does not run `git add` or `git commit`.
|
||||
|
||||
Common commands:
|
||||
|
||||
```sh
|
||||
./tickets.sh list --status all
|
||||
./tickets.sh show <id-or-slug>
|
||||
./tickets.sh create --title "Title" --slug title
|
||||
./tickets.sh comment <id-or-slug> --role plan --file notes.md
|
||||
./tickets.sh review <id-or-slug> --approve --file review.md
|
||||
./tickets.sh close <id-or-slug> --resolution "Done"
|
||||
./tickets.sh doctor
|
||||
```
|
||||
|
||||
## Migration policy
|
||||
|
||||
The migration moved unfinished `TODO.md` entries and `tickets/*.md` bodies into `work-items/open/*/item.md`. Existing review files, when present, must be represented as `review` events in `thread.md`. After migration:
|
||||
|
||||
- `work-items/` is the source of truth.
|
||||
- `TODO.md` is only a legacy/generated-view notice and must not carry open ticket state.
|
||||
- `tickets/*.md` and `tickets/*.review.md` must not remain as canonical unfinished items.
|
||||
- Closed items are moved to `work-items/closed/` instead of being deleted.
|
||||
- `./tickets.sh doctor` checks schema, status placement, duplicate IDs/slugs, and leftover legacy ticket files.
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
---
|
||||
id: 20260527-000013-tickets-sh-workitem-thread-mvp
|
||||
slug: tickets-sh-workitem-thread-mvp
|
||||
title: Ticket 管理: tickets.sh による WorkItem / Thread MVP
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:13Z
|
||||
updated_at: 2026-05-27T19:28:41Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/tickets-sh-workitem-thread-mvp.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/tickets-sh-workitem-thread-mvp.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# Ticket 管理: tickets.sh による WorkItem / Thread MVP
|
||||
|
||||
## 背景
|
||||
|
||||
現在の ticket 運用は `TODO.md` と `tickets/*.md`、必要に応じて `tickets/*.review.md` を Git 履歴で管理している。要件と完了条件を追うには機能しているが、multi-agent worktree workflow と組み合わせると review / 修正依頼 / 実装報告が扱いづらい。
|
||||
|
||||
特に `.review.md` は、review artifact を main workspace の ticket directory に作る必要がある。一方で実装 Pod は child worktree だけに write scope を持つため、review thread と実装 thread が分断されやすい。子 Pod を止めて scope を回収し、review file を作り、再度 restore / spawn するような運用になりがちで面倒である。
|
||||
|
||||
Git は履歴の保存層として有用だが、人間や AI maintainer が毎回 file move / delete / review file 作成 / git log 探索を直接操作するのは低級すぎる。repository 内の file backend を正本にしつつ、`tickets.sh` で create / list / show / comment / review / close などの意味的操作を提供する。
|
||||
|
||||
この ticket は `docs/plan/maintainer-work-items.md` の抽象メモを踏まえた最小実装である。既存 `TODO.md` / `tickets/` を併用したまま新規領域を試すのではなく、今回の MVP では既存 `TODO.md` / `tickets/*.md` を手動で `work-items/` に移し、`tickets.sh doctor` が通る状態までをゴールにする。
|
||||
|
||||
## 方針
|
||||
|
||||
- 新しい正本は repo root の `work-items/` に置く。
|
||||
- 既存 `TODO.md` / `tickets/*.md` は手動 migration の入力として扱う。
|
||||
- migration 完了後、`TODO.md` は残す場合でも legacy / generated view 相当の最小内容にする。少なくとも未完了 item の正本を `tickets/*.md` に残さない。
|
||||
- `tickets.sh` は Git を内部保存層として前提にしてよいが、操作単位は file path ではなく WorkItem 操作にする。
|
||||
- 初期実装では自動 commit しない。
|
||||
- `tickets.sh` は file 操作まで。
|
||||
- `git add/commit` は利用者または追加指示に任せる。
|
||||
- `--help` だけで基本操作と migration 方針が分かるようにする。
|
||||
- shell script なので依存は POSIX shell + 基本 Unix tool に寄せる。`jq` 必須にはしない。
|
||||
- 既存 `tickets/*.review.md` がある場合は、対象 WorkItem の `thread.md` に review event として手動で移す。
|
||||
|
||||
## backend schema
|
||||
|
||||
```text
|
||||
work-items/
|
||||
README.md
|
||||
open/
|
||||
20260526-123456-short-slug/
|
||||
item.md
|
||||
thread.md
|
||||
artifacts/
|
||||
pending/
|
||||
...
|
||||
closed/
|
||||
...
|
||||
resolution.md
|
||||
artifacts/
|
||||
```
|
||||
|
||||
`item.md` は YAML frontmatter + Markdown body。
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: 20260526-123456-short-slug
|
||||
slug: short-slug
|
||||
title: Human-readable title
|
||||
status: open
|
||||
kind: feature
|
||||
priority: P2
|
||||
labels: [maintainer, workflow]
|
||||
created_at: 2026-05-26T12:34:56Z
|
||||
updated_at: 2026-05-26T12:34:56Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/foo.md
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
...
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- ...
|
||||
```
|
||||
|
||||
`legacy_ticket` は migration 直後の追跡用 metadata とする。移行元 file は Git history で参照できるため、migration commit 後に `tickets/foo.md` を残し続けない。
|
||||
|
||||
`thread.md` は append-only Markdown event log とする。JSONL より人間が読みやすいことを優先する。
|
||||
|
||||
```md
|
||||
<!-- event: comment author: hare at: 2026-05-26T12:40:00Z -->
|
||||
|
||||
## Comment
|
||||
|
||||
...
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: orchestrator at: 2026-05-26T13:00:00Z status: request_changes -->
|
||||
|
||||
## Review: request changes
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
`tickets.sh` が必ず event header と separator を付ける。機械 parse は初期実装では簡易でよい。
|
||||
|
||||
## コマンド MVP
|
||||
|
||||
```text
|
||||
./tickets.sh help
|
||||
./tickets.sh list [--status open|pending|closed|all]
|
||||
./tickets.sh show <id-or-slug>
|
||||
./tickets.sh create --title <title> [--slug <slug>] [--kind <kind>] [--priority P2] [--label a,b]
|
||||
./tickets.sh comment <id-or-slug> [--role comment|plan|decision|implementation_report] [--author <name>] [--file <path>]
|
||||
./tickets.sh review <id-or-slug> --approve|--request-changes [--author <name>] [--file <path>]
|
||||
./tickets.sh status <id-or-slug> open|pending|closed
|
||||
./tickets.sh close <id-or-slug> [--resolution <text>|--file <path>]
|
||||
./tickets.sh doctor
|
||||
```
|
||||
|
||||
`help` / `--help` は同じ内容を出す。
|
||||
|
||||
### list
|
||||
|
||||
- `work-items/{open,pending,closed}/*/item.md` を scan する。
|
||||
- status / id / slug / title / kind / priority / updated_at を一行で表示する。
|
||||
- 初期実装では frontmatter parser は簡易でよい。
|
||||
|
||||
### show
|
||||
|
||||
- `item.md` と `thread.md` の末尾を読みやすく表示する。
|
||||
- 完全な thread 全体を出すか、初期は tail 表示でもよい。`--all` は後続でよい。
|
||||
|
||||
### create
|
||||
|
||||
- ID は `YYYYMMDD-HHMMSS-<slug>`。
|
||||
- 同一 path が存在する場合は短い random suffix または pid suffix を付けて衝突回避する。
|
||||
- `work-items/open/<id>/item.md`, `thread.md`, `artifacts/` を作る。
|
||||
- central `SEQUENCE` は作らない。
|
||||
|
||||
### comment / review
|
||||
|
||||
- `thread.md` に append する。
|
||||
- `item.md` の `updated_at` を更新する。
|
||||
- review は role/comment の special case として、`approve` / `request_changes` が分かる event header を付ける。
|
||||
- `.review.md` は作らない。
|
||||
|
||||
### status / close
|
||||
|
||||
- status directory を move する。
|
||||
- `item.md` frontmatter の `status` と `updated_at` を更新する。
|
||||
- `close` は `status closed` + optional `resolution.md` + close event append。
|
||||
- 完了しても削除しない。
|
||||
|
||||
### doctor
|
||||
|
||||
- directory status と frontmatter `status` の一致を検査する。
|
||||
- `item.md` / `thread.md` / `artifacts/` の存在を検査する。
|
||||
- duplicate slug / duplicate id を検査する。
|
||||
- `TODO.md` / `tickets/*.md` に未移行の未完了 ticket が残っていないことを検査する。
|
||||
- `tickets/*.review.md` が残っていないことを検査する。
|
||||
- work-items 配下の markdown frontmatter に必須 field があることを検査する。
|
||||
- error は非ゼロ exit。
|
||||
|
||||
## 手動 migration 要件
|
||||
|
||||
この ticket の作業には既存運用からの手動 migration を含める。
|
||||
|
||||
- 現在 `TODO.md` に載っている未完了 ticket を `work-items/open/` に移す。
|
||||
- 各 `tickets/*.md` の本文を対応する `item.md` に移す。
|
||||
- 既存 `tickets/*.review.md` があれば対応する `thread.md` に review event として移す。
|
||||
- 移行元 ticket path は `legacy_ticket` metadata または本文の参照欄に残す。
|
||||
- migration commit 後、未完了 work item の正本として `tickets/*.md` を残さない。
|
||||
- `TODO.md` は legacy notice / generated view 相当の最小内容に更新する。
|
||||
- `tickets.sh doctor` が repository の移行状態まで含めて 0 になることをゴールにする。
|
||||
|
||||
## 要件
|
||||
|
||||
- `tickets.sh --help` で使い方と migration 後の配置が分かる。
|
||||
- `create/list/show/comment/review/status/close/doctor` が動く。
|
||||
- WorkItem ID は timestamp-based で、central sequence file を使わない。
|
||||
- close しても削除せず `work-items/closed/` に移動する。
|
||||
- review は `.review.md` ではなく thread event として append できる。
|
||||
- `doctor` が directory status と frontmatter status の不一致を検出する。
|
||||
- `doctor` が未移行 `TODO.md` / `tickets/*.md` / `tickets/*.review.md` を検出する。
|
||||
- 初期実装では自動 git commit しない。
|
||||
- README 相当の usage は `--help` または `work-items/README.md` に含める。
|
||||
|
||||
## 完了条件
|
||||
|
||||
- repo root に `tickets.sh` が追加される。
|
||||
- `work-items/README.md` で schema / migration 後の運用が説明される。
|
||||
- `tickets.sh create` で WorkItem を作成できる。
|
||||
- `tickets.sh comment` / `tickets.sh review` で thread event を append できる。
|
||||
- `tickets.sh close` で closed に移動できる。
|
||||
- 既存 `TODO.md` / `tickets/*.md` / `tickets/*.review.md` が手動で `work-items/` に移行される。
|
||||
- migration 後、`tickets.sh doctor` が repository 全体の状態に対して 0 になる。
|
||||
- 不整合 fixture または smoke test で `doctor` が非ゼロになることを確認する。
|
||||
- shellcheck が利用可能なら通る。無い場合は少なくとも focused smoke test を実行する。
|
||||
|
||||
## 範囲外
|
||||
|
||||
- Rust crate / DB / remote backend 実装。
|
||||
- LeaseStore / Pod run tracking の実装。
|
||||
- Git commit の自動化。
|
||||
- TUI 統合。
|
||||
- WorkItem から TODO.md を自動生成する仕組み。
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
---
|
||||
id: 20260527-000013-tickets-sh-workitem-thread-mvp
|
||||
slug: tickets-sh-workitem-thread-mvp
|
||||
title: Ticket 管理: tickets.sh による WorkItem / Thread MVP
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:13Z
|
||||
updated_at: 2026-05-27T19:28:41Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/tickets-sh-workitem-thread-mvp.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/tickets-sh-workitem-thread-mvp.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# Ticket 管理: tickets.sh による WorkItem / Thread MVP
|
||||
|
||||
## 背景
|
||||
|
||||
現在の ticket 運用は `TODO.md` と `tickets/*.md`、必要に応じて `tickets/*.review.md` を Git 履歴で管理している。要件と完了条件を追うには機能しているが、multi-agent worktree workflow と組み合わせると review / 修正依頼 / 実装報告が扱いづらい。
|
||||
|
||||
特に `.review.md` は、review artifact を main workspace の ticket directory に作る必要がある。一方で実装 Pod は child worktree だけに write scope を持つため、review thread と実装 thread が分断されやすい。子 Pod を止めて scope を回収し、review file を作り、再度 restore / spawn するような運用になりがちで面倒である。
|
||||
|
||||
Git は履歴の保存層として有用だが、人間や AI maintainer が毎回 file move / delete / review file 作成 / git log 探索を直接操作するのは低級すぎる。repository 内の file backend を正本にしつつ、`tickets.sh` で create / list / show / comment / review / close などの意味的操作を提供する。
|
||||
|
||||
この ticket は `docs/plan/maintainer-work-items.md` の抽象メモを踏まえた最小実装である。既存 `TODO.md` / `tickets/` を併用したまま新規領域を試すのではなく、今回の MVP では既存 `TODO.md` / `tickets/*.md` を手動で `work-items/` に移し、`tickets.sh doctor` が通る状態までをゴールにする。
|
||||
|
||||
## 方針
|
||||
|
||||
- 新しい正本は repo root の `work-items/` に置く。
|
||||
- 既存 `TODO.md` / `tickets/*.md` は手動 migration の入力として扱う。
|
||||
- migration 完了後、`TODO.md` は残す場合でも legacy / generated view 相当の最小内容にする。少なくとも未完了 item の正本を `tickets/*.md` に残さない。
|
||||
- `tickets.sh` は Git を内部保存層として前提にしてよいが、操作単位は file path ではなく WorkItem 操作にする。
|
||||
- 初期実装では自動 commit しない。
|
||||
- `tickets.sh` は file 操作まで。
|
||||
- `git add/commit` は利用者または追加指示に任せる。
|
||||
- `--help` だけで基本操作と migration 方針が分かるようにする。
|
||||
- shell script なので依存は POSIX shell + 基本 Unix tool に寄せる。`jq` 必須にはしない。
|
||||
- 既存 `tickets/*.review.md` がある場合は、対象 WorkItem の `thread.md` に review event として手動で移す。
|
||||
|
||||
## backend schema
|
||||
|
||||
```text
|
||||
work-items/
|
||||
README.md
|
||||
open/
|
||||
20260526-123456-short-slug/
|
||||
item.md
|
||||
thread.md
|
||||
artifacts/
|
||||
pending/
|
||||
...
|
||||
closed/
|
||||
...
|
||||
resolution.md
|
||||
artifacts/
|
||||
```
|
||||
|
||||
`item.md` は YAML frontmatter + Markdown body。
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: 20260526-123456-short-slug
|
||||
slug: short-slug
|
||||
title: Human-readable title
|
||||
status: open
|
||||
kind: feature
|
||||
priority: P2
|
||||
labels: [maintainer, workflow]
|
||||
created_at: 2026-05-26T12:34:56Z
|
||||
updated_at: 2026-05-26T12:34:56Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/foo.md
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
...
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- ...
|
||||
```
|
||||
|
||||
`legacy_ticket` は migration 直後の追跡用 metadata とする。移行元 file は Git history で参照できるため、migration commit 後に `tickets/foo.md` を残し続けない。
|
||||
|
||||
`thread.md` は append-only Markdown event log とする。JSONL より人間が読みやすいことを優先する。
|
||||
|
||||
```md
|
||||
<!-- event: comment author: hare at: 2026-05-26T12:40:00Z -->
|
||||
|
||||
## Comment
|
||||
|
||||
...
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: orchestrator at: 2026-05-26T13:00:00Z status: request_changes -->
|
||||
|
||||
## Review: request changes
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
`tickets.sh` が必ず event header と separator を付ける。機械 parse は初期実装では簡易でよい。
|
||||
|
||||
## コマンド MVP
|
||||
|
||||
```text
|
||||
./tickets.sh help
|
||||
./tickets.sh list [--status open|pending|closed|all]
|
||||
./tickets.sh show <id-or-slug>
|
||||
./tickets.sh create --title <title> [--slug <slug>] [--kind <kind>] [--priority P2] [--label a,b]
|
||||
./tickets.sh comment <id-or-slug> [--role comment|plan|decision|implementation_report] [--author <name>] [--file <path>]
|
||||
./tickets.sh review <id-or-slug> --approve|--request-changes [--author <name>] [--file <path>]
|
||||
./tickets.sh status <id-or-slug> open|pending|closed
|
||||
./tickets.sh close <id-or-slug> [--resolution <text>|--file <path>]
|
||||
./tickets.sh doctor
|
||||
```
|
||||
|
||||
`help` / `--help` は同じ内容を出す。
|
||||
|
||||
### list
|
||||
|
||||
- `work-items/{open,pending,closed}/*/item.md` を scan する。
|
||||
- status / id / slug / title / kind / priority / updated_at を一行で表示する。
|
||||
- 初期実装では frontmatter parser は簡易でよい。
|
||||
|
||||
### show
|
||||
|
||||
- `item.md` と `thread.md` の末尾を読みやすく表示する。
|
||||
- 完全な thread 全体を出すか、初期は tail 表示でもよい。`--all` は後続でよい。
|
||||
|
||||
### create
|
||||
|
||||
- ID は `YYYYMMDD-HHMMSS-<slug>`。
|
||||
- 同一 path が存在する場合は短い random suffix または pid suffix を付けて衝突回避する。
|
||||
- `work-items/open/<id>/item.md`, `thread.md`, `artifacts/` を作る。
|
||||
- central `SEQUENCE` は作らない。
|
||||
|
||||
### comment / review
|
||||
|
||||
- `thread.md` に append する。
|
||||
- `item.md` の `updated_at` を更新する。
|
||||
- review は role/comment の special case として、`approve` / `request_changes` が分かる event header を付ける。
|
||||
- `.review.md` は作らない。
|
||||
|
||||
### status / close
|
||||
|
||||
- status directory を move する。
|
||||
- `item.md` frontmatter の `status` と `updated_at` を更新する。
|
||||
- `close` は `status closed` + optional `resolution.md` + close event append。
|
||||
- 完了しても削除しない。
|
||||
|
||||
### doctor
|
||||
|
||||
- directory status と frontmatter `status` の一致を検査する。
|
||||
- `item.md` / `thread.md` / `artifacts/` の存在を検査する。
|
||||
- duplicate slug / duplicate id を検査する。
|
||||
- `TODO.md` / `tickets/*.md` に未移行の未完了 ticket が残っていないことを検査する。
|
||||
- `tickets/*.review.md` が残っていないことを検査する。
|
||||
- work-items 配下の markdown frontmatter に必須 field があることを検査する。
|
||||
- error は非ゼロ exit。
|
||||
|
||||
## 手動 migration 要件
|
||||
|
||||
この ticket の作業には既存運用からの手動 migration を含める。
|
||||
|
||||
- 現在 `TODO.md` に載っている未完了 ticket を `work-items/open/` に移す。
|
||||
- 各 `tickets/*.md` の本文を対応する `item.md` に移す。
|
||||
- 既存 `tickets/*.review.md` があれば対応する `thread.md` に review event として移す。
|
||||
- 移行元 ticket path は `legacy_ticket` metadata または本文の参照欄に残す。
|
||||
- migration commit 後、未完了 work item の正本として `tickets/*.md` を残さない。
|
||||
- `TODO.md` は legacy notice / generated view 相当の最小内容に更新する。
|
||||
- `tickets.sh doctor` が repository の移行状態まで含めて 0 になることをゴールにする。
|
||||
|
||||
## 要件
|
||||
|
||||
- `tickets.sh --help` で使い方と migration 後の配置が分かる。
|
||||
- `create/list/show/comment/review/status/close/doctor` が動く。
|
||||
- WorkItem ID は timestamp-based で、central sequence file を使わない。
|
||||
- close しても削除せず `work-items/closed/` に移動する。
|
||||
- review は `.review.md` ではなく thread event として append できる。
|
||||
- `doctor` が directory status と frontmatter status の不一致を検出する。
|
||||
- `doctor` が未移行 `TODO.md` / `tickets/*.md` / `tickets/*.review.md` を検出する。
|
||||
- 初期実装では自動 git commit しない。
|
||||
- README 相当の usage は `--help` または `work-items/README.md` に含める。
|
||||
|
||||
## 完了条件
|
||||
|
||||
- repo root に `tickets.sh` が追加される。
|
||||
- `work-items/README.md` で schema / migration 後の運用が説明される。
|
||||
- `tickets.sh create` で WorkItem を作成できる。
|
||||
- `tickets.sh comment` / `tickets.sh review` で thread event を append できる。
|
||||
- `tickets.sh close` で closed に移動できる。
|
||||
- 既存 `TODO.md` / `tickets/*.md` / `tickets/*.review.md` が手動で `work-items/` に移行される。
|
||||
- migration 後、`tickets.sh doctor` が repository 全体の状態に対して 0 になる。
|
||||
- 不整合 fixture または smoke test で `doctor` が非ゼロになることを確認する。
|
||||
- shellcheck が利用可能なら通る。無い場合は少なくとも focused smoke test を実行する。
|
||||
|
||||
## 範囲外
|
||||
|
||||
- Rust crate / DB / remote backend 実装。
|
||||
- LeaseStore / Pod run tracking の実装。
|
||||
- Git commit の自動化。
|
||||
- TUI 統合。
|
||||
- WorkItem から TODO.md を自動生成する仕組み。
|
||||
|
|
@ -1,226 +0,0 @@
|
|||
<!-- event: migration author: tickets.sh-migration at: 2026-05-27T00:00:13Z -->
|
||||
|
||||
## Migrated
|
||||
|
||||
Migrated from tickets/tickets-sh-workitem-thread-mvp.md. No legacy review file was present at migration time.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-05-27T19:28:41Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
---
|
||||
id: 20260527-000013-tickets-sh-workitem-thread-mvp
|
||||
slug: tickets-sh-workitem-thread-mvp
|
||||
title: Ticket 管理: tickets.sh による WorkItem / Thread MVP
|
||||
status: closed
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:13Z
|
||||
updated_at: 2026-05-27T19:28:41Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/tickets-sh-workitem-thread-mvp.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/tickets-sh-workitem-thread-mvp.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# Ticket 管理: tickets.sh による WorkItem / Thread MVP
|
||||
|
||||
## 背景
|
||||
|
||||
現在の ticket 運用は `TODO.md` と `tickets/*.md`、必要に応じて `tickets/*.review.md` を Git 履歴で管理している。要件と完了条件を追うには機能しているが、multi-agent worktree workflow と組み合わせると review / 修正依頼 / 実装報告が扱いづらい。
|
||||
|
||||
特に `.review.md` は、review artifact を main workspace の ticket directory に作る必要がある。一方で実装 Pod は child worktree だけに write scope を持つため、review thread と実装 thread が分断されやすい。子 Pod を止めて scope を回収し、review file を作り、再度 restore / spawn するような運用になりがちで面倒である。
|
||||
|
||||
Git は履歴の保存層として有用だが、人間や AI maintainer が毎回 file move / delete / review file 作成 / git log 探索を直接操作するのは低級すぎる。repository 内の file backend を正本にしつつ、`tickets.sh` で create / list / show / comment / review / close などの意味的操作を提供する。
|
||||
|
||||
この ticket は `docs/plan/maintainer-work-items.md` の抽象メモを踏まえた最小実装である。既存 `TODO.md` / `tickets/` を併用したまま新規領域を試すのではなく、今回の MVP では既存 `TODO.md` / `tickets/*.md` を手動で `work-items/` に移し、`tickets.sh doctor` が通る状態までをゴールにする。
|
||||
|
||||
## 方針
|
||||
|
||||
- 新しい正本は repo root の `work-items/` に置く。
|
||||
- 既存 `TODO.md` / `tickets/*.md` は手動 migration の入力として扱う。
|
||||
- migration 完了後、`TODO.md` は残す場合でも legacy / generated view 相当の最小内容にする。少なくとも未完了 item の正本を `tickets/*.md` に残さない。
|
||||
- `tickets.sh` は Git を内部保存層として前提にしてよいが、操作単位は file path ではなく WorkItem 操作にする。
|
||||
- 初期実装では自動 commit しない。
|
||||
- `tickets.sh` は file 操作まで。
|
||||
- `git add/commit` は利用者または追加指示に任せる。
|
||||
- `--help` だけで基本操作と migration 方針が分かるようにする。
|
||||
- shell script なので依存は POSIX shell + 基本 Unix tool に寄せる。`jq` 必須にはしない。
|
||||
- 既存 `tickets/*.review.md` がある場合は、対象 WorkItem の `thread.md` に review event として手動で移す。
|
||||
|
||||
## backend schema
|
||||
|
||||
```text
|
||||
work-items/
|
||||
README.md
|
||||
open/
|
||||
20260526-123456-short-slug/
|
||||
item.md
|
||||
thread.md
|
||||
artifacts/
|
||||
pending/
|
||||
...
|
||||
closed/
|
||||
...
|
||||
resolution.md
|
||||
artifacts/
|
||||
```
|
||||
|
||||
`item.md` は YAML frontmatter + Markdown body。
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: 20260526-123456-short-slug
|
||||
slug: short-slug
|
||||
title: Human-readable title
|
||||
status: open
|
||||
kind: feature
|
||||
priority: P2
|
||||
labels: [maintainer, workflow]
|
||||
created_at: 2026-05-26T12:34:56Z
|
||||
updated_at: 2026-05-26T12:34:56Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/foo.md
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
...
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- ...
|
||||
```
|
||||
|
||||
`legacy_ticket` は migration 直後の追跡用 metadata とする。移行元 file は Git history で参照できるため、migration commit 後に `tickets/foo.md` を残し続けない。
|
||||
|
||||
`thread.md` は append-only Markdown event log とする。JSONL より人間が読みやすいことを優先する。
|
||||
|
||||
```md
|
||||
<!-- event: comment author: hare at: 2026-05-26T12:40:00Z -->
|
||||
|
||||
## Comment
|
||||
|
||||
...
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: orchestrator at: 2026-05-26T13:00:00Z status: request_changes -->
|
||||
|
||||
## Review: request changes
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
`tickets.sh` が必ず event header と separator を付ける。機械 parse は初期実装では簡易でよい。
|
||||
|
||||
## コマンド MVP
|
||||
|
||||
```text
|
||||
./tickets.sh help
|
||||
./tickets.sh list [--status open|pending|closed|all]
|
||||
./tickets.sh show <id-or-slug>
|
||||
./tickets.sh create --title <title> [--slug <slug>] [--kind <kind>] [--priority P2] [--label a,b]
|
||||
./tickets.sh comment <id-or-slug> [--role comment|plan|decision|implementation_report] [--author <name>] [--file <path>]
|
||||
./tickets.sh review <id-or-slug> --approve|--request-changes [--author <name>] [--file <path>]
|
||||
./tickets.sh status <id-or-slug> open|pending|closed
|
||||
./tickets.sh close <id-or-slug> [--resolution <text>|--file <path>]
|
||||
./tickets.sh doctor
|
||||
```
|
||||
|
||||
`help` / `--help` は同じ内容を出す。
|
||||
|
||||
### list
|
||||
|
||||
- `work-items/{open,pending,closed}/*/item.md` を scan する。
|
||||
- status / id / slug / title / kind / priority / updated_at を一行で表示する。
|
||||
- 初期実装では frontmatter parser は簡易でよい。
|
||||
|
||||
### show
|
||||
|
||||
- `item.md` と `thread.md` の末尾を読みやすく表示する。
|
||||
- 完全な thread 全体を出すか、初期は tail 表示でもよい。`--all` は後続でよい。
|
||||
|
||||
### create
|
||||
|
||||
- ID は `YYYYMMDD-HHMMSS-<slug>`。
|
||||
- 同一 path が存在する場合は短い random suffix または pid suffix を付けて衝突回避する。
|
||||
- `work-items/open/<id>/item.md`, `thread.md`, `artifacts/` を作る。
|
||||
- central `SEQUENCE` は作らない。
|
||||
|
||||
### comment / review
|
||||
|
||||
- `thread.md` に append する。
|
||||
- `item.md` の `updated_at` を更新する。
|
||||
- review は role/comment の special case として、`approve` / `request_changes` が分かる event header を付ける。
|
||||
- `.review.md` は作らない。
|
||||
|
||||
### status / close
|
||||
|
||||
- status directory を move する。
|
||||
- `item.md` frontmatter の `status` と `updated_at` を更新する。
|
||||
- `close` は `status closed` + optional `resolution.md` + close event append。
|
||||
- 完了しても削除しない。
|
||||
|
||||
### doctor
|
||||
|
||||
- directory status と frontmatter `status` の一致を検査する。
|
||||
- `item.md` / `thread.md` / `artifacts/` の存在を検査する。
|
||||
- duplicate slug / duplicate id を検査する。
|
||||
- `TODO.md` / `tickets/*.md` に未移行の未完了 ticket が残っていないことを検査する。
|
||||
- `tickets/*.review.md` が残っていないことを検査する。
|
||||
- work-items 配下の markdown frontmatter に必須 field があることを検査する。
|
||||
- error は非ゼロ exit。
|
||||
|
||||
## 手動 migration 要件
|
||||
|
||||
この ticket の作業には既存運用からの手動 migration を含める。
|
||||
|
||||
- 現在 `TODO.md` に載っている未完了 ticket を `work-items/open/` に移す。
|
||||
- 各 `tickets/*.md` の本文を対応する `item.md` に移す。
|
||||
- 既存 `tickets/*.review.md` があれば対応する `thread.md` に review event として移す。
|
||||
- 移行元 ticket path は `legacy_ticket` metadata または本文の参照欄に残す。
|
||||
- migration commit 後、未完了 work item の正本として `tickets/*.md` を残さない。
|
||||
- `TODO.md` は legacy notice / generated view 相当の最小内容に更新する。
|
||||
- `tickets.sh doctor` が repository の移行状態まで含めて 0 になることをゴールにする。
|
||||
|
||||
## 要件
|
||||
|
||||
- `tickets.sh --help` で使い方と migration 後の配置が分かる。
|
||||
- `create/list/show/comment/review/status/close/doctor` が動く。
|
||||
- WorkItem ID は timestamp-based で、central sequence file を使わない。
|
||||
- close しても削除せず `work-items/closed/` に移動する。
|
||||
- review は `.review.md` ではなく thread event として append できる。
|
||||
- `doctor` が directory status と frontmatter status の不一致を検出する。
|
||||
- `doctor` が未移行 `TODO.md` / `tickets/*.md` / `tickets/*.review.md` を検出する。
|
||||
- 初期実装では自動 git commit しない。
|
||||
- README 相当の usage は `--help` または `work-items/README.md` に含める。
|
||||
|
||||
## 完了条件
|
||||
|
||||
- repo root に `tickets.sh` が追加される。
|
||||
- `work-items/README.md` で schema / migration 後の運用が説明される。
|
||||
- `tickets.sh create` で WorkItem を作成できる。
|
||||
- `tickets.sh comment` / `tickets.sh review` で thread event を append できる。
|
||||
- `tickets.sh close` で closed に移動できる。
|
||||
- 既存 `TODO.md` / `tickets/*.md` / `tickets/*.review.md` が手動で `work-items/` に移行される。
|
||||
- migration 後、`tickets.sh doctor` が repository 全体の状態に対して 0 になる。
|
||||
- 不整合 fixture または smoke test で `doctor` が非ゼロになることを確認する。
|
||||
- shellcheck が利用可能なら通る。無い場合は少なくとも focused smoke test を実行する。
|
||||
|
||||
## 範囲外
|
||||
|
||||
- Rust crate / DB / remote backend 実装。
|
||||
- LeaseStore / Pod run tracking の実装。
|
||||
- Git commit の自動化。
|
||||
- TUI 統合。
|
||||
- WorkItem から TODO.md を自動生成する仕組み。
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
---
|
||||
id: 20260527-201313-openai-responses-unhandled-sse-observability
|
||||
slug: openai-responses-unhandled-sse-observability
|
||||
title: OpenAI Responses 未対応 SSE event を破棄せず観測する
|
||||
status: closed
|
||||
kind: feature
|
||||
priority: P1
|
||||
labels: [llm, openai, observability, trace]
|
||||
created_at: 2026-05-27T20:13:13Z
|
||||
updated_at: 2026-05-27T20:44:19Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
Created by tickets.sh.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- TBD
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
---
|
||||
id: 20260527-201313-openai-responses-unhandled-sse-observability
|
||||
slug: openai-responses-unhandled-sse-observability
|
||||
title: OpenAI Responses 未対応 SSE event を破棄せず観測する
|
||||
status: closed
|
||||
kind: feature
|
||||
priority: P1
|
||||
labels: [llm, openai, observability, trace]
|
||||
created_at: 2026-05-27T20:13:13Z
|
||||
updated_at: 2026-05-27T20:44:19Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
Created by tickets.sh.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- TBD
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
<!-- event: create author: tickets.sh at: 2026-05-27T20:13:13Z -->
|
||||
|
||||
## Created
|
||||
|
||||
Created by tickets.sh create.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: orchestrator at: 2026-05-27T20:13:30Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
## Background
|
||||
|
||||
OpenAI Responses parser currently drops unsupported SSE event types by falling through to `_ => Ok(Vec::new())`. That means provider events that do not yet have a match arm are neither visible in stream trace nor preserved as diagnostics. This made it impossible to inspect the "unexpected event" class of logs after the fact.
|
||||
|
||||
Recent work preserved diagnostics for known error-like event types (`response.failed`, `response.incomplete`, top-level `error`), but it did not cover event types that are not matched at all. We need observability for those raw/unhandled SSE frames without turning them into conversation history or model-visible content.
|
||||
|
||||
## Requirements
|
||||
|
||||
- OpenAI Responses SSE event types that are not otherwise handled must be observable.
|
||||
- Do not silently return `Ok(Vec::new())` without any traceable signal.
|
||||
- Include the raw `event_type` and a bounded preview of `data`.
|
||||
- Include full data length so truncation is visible.
|
||||
- The signal must be visible in existing stream trace when `[session].record_event_trace = true`.
|
||||
- The signal must not become assistant/user history and must not be sent back to the model as normal content.
|
||||
- Timeline / collectors must ignore the signal for generation semantics.
|
||||
- Known intentionally ignorable events may be classified separately if needed, but they must still be observable enough for debugging.
|
||||
- Add tests for at least one unknown OpenAI Responses event type.
|
||||
- Existing `unknown_event_is_ignored` should be replaced or updated.
|
||||
- Verify event type and data preview are retained.
|
||||
- Verify large data is bounded / marked by length.
|
||||
|
||||
## Suggested implementation shape
|
||||
|
||||
A small normalized event variant is acceptable, for example:
|
||||
|
||||
```rust
|
||||
Event::UnhandledSse {
|
||||
provider: "openai_responses",
|
||||
event_type: String,
|
||||
data_preview: String,
|
||||
data_len: usize,
|
||||
}
|
||||
```
|
||||
|
||||
or equivalent. If adding a generic variant to `llm_client::event::Event`, make sure Timeline ignores it and trace serialization captures it.
|
||||
|
||||
Avoid plumbing raw SSE into session history. This is observability only.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Unknown OpenAI Responses SSE event types appear in trace output instead of disappearing.
|
||||
- Timeline semantics / assistant output are unchanged for unknown events.
|
||||
- Large raw data is capped in the event payload but original byte length is recorded.
|
||||
- Focused tests pass for OpenAI Responses parser and Timeline behavior if touched.
|
||||
- `cargo fmt --check` and related crate tests pass.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Implementing semantics for every OpenAI Responses event type.
|
||||
- Retrying or changing behavior based on unknown events.
|
||||
- Raw SSE frame permanent audit log separate from trace.
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-05-27T20:44:19Z status: closed -->
|
||||
|
||||
## Closed
|
||||
|
||||
---
|
||||
id: 20260527-201313-openai-responses-unhandled-sse-observability
|
||||
slug: openai-responses-unhandled-sse-observability
|
||||
title: OpenAI Responses 未対応 SSE event を破棄せず観測する
|
||||
status: closed
|
||||
kind: feature
|
||||
priority: P1
|
||||
labels: [llm, openai, observability, trace]
|
||||
created_at: 2026-05-27T20:13:13Z
|
||||
updated_at: 2026-05-27T20:44:19Z
|
||||
assignee: null
|
||||
legacy_ticket: null
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
Created by tickets.sh.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- TBD
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<!-- event: migration author: tickets.sh-migration at: 2026-05-27T00:00:01Z -->
|
||||
|
||||
## Migrated
|
||||
|
||||
Migrated from tickets/auto-maintain-workflow.md. No legacy review file was present at migration time.
|
||||
|
||||
---
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<!-- event: migration author: tickets.sh-migration at: 2026-05-27T00:00:02Z -->
|
||||
|
||||
## Migrated
|
||||
|
||||
Migrated from tickets/e2e-harness.md. No legacy review file was present at migration time.
|
||||
|
||||
---
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<!-- event: migration author: tickets.sh-migration at: 2026-05-27T00:00:03Z -->
|
||||
|
||||
## Migrated
|
||||
|
||||
Migrated from tickets/internal-worker-workflow.md. No legacy review file was present at migration time.
|
||||
|
||||
---
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<!-- event: migration author: tickets.sh-migration at: 2026-05-27T00:00:04Z -->
|
||||
|
||||
## Migrated
|
||||
|
||||
Migrated from tickets/manual-turn-rollback.md. No legacy review file was present at migration time.
|
||||
|
||||
---
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
---
|
||||
id: 20260527-000005-memory-tool-guidance-prompt
|
||||
slug: memory-tool-guidance-prompt
|
||||
title: プロンプト: memory / knowledge tool 利用タイミングのガイダンス
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:05Z
|
||||
updated_at: 2026-05-27T00:00:05Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/memory-tool-guidance-prompt.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/memory-tool-guidance-prompt.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# プロンプト: memory / knowledge tool 利用タイミングのガイダンス
|
||||
|
||||
## 背景
|
||||
|
||||
通常 Pod には `MemoryQuery` / `MemoryRead` / `KnowledgeQuery` / `MemoryWrite` 等の memory / knowledge tools が提供されているが、現状の通常 system prompt はそれらを「いつ使うべきか」をほとんど説明していない。
|
||||
|
||||
現在の `resources/prompts/common/tool-usage.md` は、既知パスなら Read、検索なら Grep/Glob、並列可能ならまとめる、という汎用 tool 方針に留まる。memory / knowledge tools の description には操作方法はあるが、モデルが自発的に memory lookup すべき状況は明示されていない。
|
||||
|
||||
このため、過去の決定・ユーザー嗜好・以前の経緯を問われても、モデルが `MemoryQuery` / `MemoryRead` を自発的に使わない可能性が高い。`summary.md` resident injection により短い durable context は常時見えるようになるが、詳細な過去判断や request を探すには query guidance が必要である。
|
||||
|
||||
## 方針
|
||||
|
||||
通常 Pod の system prompt に、memory / knowledge tools の利用タイミングを短く追加する。
|
||||
|
||||
目的は「必要な時に過去情報を探す」ことであり、毎 turn memory query を強制することではない。memory / knowledge は helpful context だが stale になり得るため、現在の user instruction / files / tickets / git state / session log を上書きする権威として扱わせない。
|
||||
|
||||
## 推奨する追加文言
|
||||
|
||||
`resources/prompts/common/tool-usage.md` に新しい小節を足すか、`resources/prompts/common/memory.md` を作って `default.md` から include する。
|
||||
|
||||
例:
|
||||
|
||||
```md
|
||||
## Memory and knowledge
|
||||
|
||||
Use memory and knowledge tools when the user asks about past decisions, prior requests, durable preferences, project history, or why something was done. Do not guess from vague recollection when a targeted memory lookup would answer the question.
|
||||
|
||||
- Use `MemoryQuery` for durable memory records: summary, decisions, and requests.
|
||||
- Use `KnowledgeQuery` for project knowledge records.
|
||||
- Use `MemoryRead(kind=summary)` when you need the full workspace memory summary.
|
||||
- Use `MemoryRead` on returned slugs when query excerpts are insufficient.
|
||||
|
||||
Resident memory and knowledge are helpful context but may be stale. Current user instructions, repository files, tickets, git history, and session logs are more authoritative for exact current state.
|
||||
|
||||
Do not query memory on every turn. Prefer it when past context, user preferences, or prior rationale materially affects the answer or implementation.
|
||||
```
|
||||
|
||||
文言は実装時に自然に調整してよいが、以下の意味は維持する。
|
||||
|
||||
- 過去判断 / 過去依頼 / ユーザー嗜好 / project history / why 系では memory lookup を促す。
|
||||
- `MemoryQuery`, `KnowledgeQuery`, `MemoryRead(kind=summary)`, slug read の役割を明示する。
|
||||
- resident context は stale になり得ると明示する。
|
||||
- current user instruction / files / tickets / git / session logs の方が exact current state では強いと明示する。
|
||||
- 毎 turn query しないと明示する。
|
||||
|
||||
## 要件
|
||||
|
||||
- 通常 Pod の default prompt に memory / knowledge tool 利用タイミングの guidance が入る。
|
||||
- internal prompts (`memory_extract_system`, `memory_consolidation_system`, `compact_system`) の挙動を変えない。
|
||||
- guidance は短く、通常 turn の token overhead を過度に増やさない。
|
||||
- guidance は memory / knowledge を current authority より上に置かない。
|
||||
- guidance は毎 turn memory query を促さない。
|
||||
- `MemoryWrite` / `MemoryEdit` / `MemoryDelete` の自発的利用を安易に促さない。
|
||||
- 通常作業では read/query を促し、write/edit/delete は明示的な依頼または memory maintenance worker に寄せる。
|
||||
|
||||
## 完了条件
|
||||
|
||||
- `resources/prompts/default.md` から memory guidance が render される。
|
||||
- prompt render / catalog 関連 test があれば更新されている。
|
||||
- internal worker prompt には不要な memory guidance が混ざらない。
|
||||
- `cargo fmt --check` と関連 test が通る。
|
||||
|
||||
## 範囲外
|
||||
|
||||
- `summary.md` resident injection の実装。これは `memory-summary-resident-injection.md` で扱う。
|
||||
- memory tool descriptions の大幅変更。
|
||||
- memory usage metrics の設計変更。
|
||||
- global memory / project local memory の store 分離。
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<!-- event: migration author: tickets.sh-migration at: 2026-05-27T00:00:05Z -->
|
||||
|
||||
## Migrated
|
||||
|
||||
Migrated from tickets/memory-tool-guidance-prompt.md. No legacy review file was present at migration time.
|
||||
|
||||
---
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<!-- event: migration author: tickets.sh-migration at: 2026-05-27T00:00:06Z -->
|
||||
|
||||
## Migrated
|
||||
|
||||
Migrated from tickets/permission-default-policy.md. No legacy review file was present at migration time.
|
||||
|
||||
---
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<!-- event: migration author: tickets.sh-migration at: 2026-05-27T00:00:07Z -->
|
||||
|
||||
## Migrated
|
||||
|
||||
Migrated from tickets/pod-inbound-pod-event-dedup.md. No legacy review file was present at migration time.
|
||||
|
||||
---
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
---
|
||||
id: 20260527-000008-pod-scope-persistence-authority
|
||||
slug: pod-scope-persistence-authority
|
||||
title: Pod: scope 永続化 authority の整理
|
||||
status: open
|
||||
kind: task
|
||||
priority: P2
|
||||
labels: [migrated]
|
||||
created_at: 2026-05-27T00:00:08Z
|
||||
updated_at: 2026-05-27T00:00:08Z
|
||||
assignee: null
|
||||
legacy_ticket: tickets/pod-scope-persistence-authority.md
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
|
||||
- legacy_ticket: tickets/pod-scope-persistence-authority.md
|
||||
- migrated_from: TODO.md / tickets directory migration on 2026-05-27
|
||||
|
||||
# Pod: scope 永続化 authority の整理
|
||||
|
||||
## 背景
|
||||
|
||||
Pod の scope は複数の場所に関連情報が存在している。
|
||||
|
||||
- session log の `pod.scope` extension: Pod 自身の復元用 runtime scope snapshot
|
||||
- Pod metadata: Pod 名から active session/segment への pointer と spawned child 情報
|
||||
- spawned child 情報: child に委譲した scope
|
||||
- runtime registry: live Pod の allocation / conflict detection 用 scope
|
||||
- runtime mirror: `spawned_pods.json` 等の現在プロセス向け表示・制御用情報
|
||||
|
||||
これらは用途が異なるが、どの情報が durable authority で、どれが live mirror / derived state なのかが読み取りづらい。特に restore、compact/fork による segment 切替、child scope の委譲・reclaim、runtime registry の再構築で、scope の保存先と復元順序が曖昧だと権限の過大復元または過小復元につながる。
|
||||
|
||||
## 要件
|
||||
|
||||
- Pod scope に関する durable authority を明確に定義する。
|
||||
- Pod 自身の base scope / effective runtime scope / deny による delegated-out 部分を区別する。
|
||||
- spawned child に委譲した scope と、親 Pod 自身の effective scope を区別する。
|
||||
- live registry / runtime mirror は durable authority ではないことを明確にする。
|
||||
- Pod 名からの restore に必要な情報の保存先を一貫させる。
|
||||
- Pod 名から active session/segment を解決できる。
|
||||
- 解決した Pod が、前回終了時点の effective scope を過大に復元しない。
|
||||
- child が生存・復元対象の場合、親の delegated-out scope が意図せず reclaim されない。
|
||||
- segment 遷移で scope が失われない。
|
||||
- compact / fork / resume / attach の後も、次にその segment を restore したとき同じ effective scope が得られる。
|
||||
- 新 segment 作成時に scope authority が必要なら、初期状態として確実に引き継がれる。
|
||||
- spawned child の scope 永続化を親 Pod の restore/reclaim 要件と整合させる。
|
||||
- 親は child に委譲済みの scope を把握できる。
|
||||
- child 停止・shutdown・restore 時の prune により、親の effective write scope が正しく reclaim される。
|
||||
- explicit deny と delegated-out deny を混同しない。
|
||||
- runtime registry 再構築時の入力と副作用を定義する。
|
||||
- restore 時にどの durable state から allocation を再作成するかが明確である。
|
||||
- stale / unreachable child を pruning した場合、durable state と runtime mirror が矛盾しない。
|
||||
- 保存形式は inspect/debug しやすい。
|
||||
- Pod ごとに「active pointer」「自身の scope」「spawned child と delegated scope」が追跡できる。
|
||||
- restore 失敗時に、欠けている authority が何か分かる error になる。
|
||||
- session log の conversation/history authority と scope authority の関係を明確にする。
|
||||
- scope 更新が conversation history の意味内容を汚染しない。
|
||||
- append-only session log に置く場合は、compact/fork と replay semantics 上の扱いが明示される。
|
||||
- Pod metadata に置く場合は、session/segment lineage との整合と更新順序が明示される。
|
||||
|
||||
## 完了条件
|
||||
|
||||
- Pod scope に関する durable authority / runtime mirror / derived state の責務がコードとドキュメント上で一致している。
|
||||
- Pod restore が、前回の effective scope を過大復元しない regression test を持つ。
|
||||
- compact または fork 後の新 segment restore で scope が失われない regression test を持つ。
|
||||
- spawned child に scope 委譲済みの親 Pod を restore しても、child 側の write scope が親に二重に戻らない regression test を持つ。
|
||||
- child 停止・shutdown・restore pruning 後に、親の effective scope と durable state が一致する regression test を持つ。
|
||||
- runtime registry / runtime mirror が durable authority と矛盾した場合の扱いが test で確認されている。
|
||||
|
||||
## 範囲外
|
||||
|
||||
- manifest scope 設定そのものの設計変更。
|
||||
- tool permission policy の allow / ask / deny 挙動変更。
|
||||
- UI 表示だけで scope 不整合を隠す対応。
|
||||
- 既存の壊れた手元 session log を自動修復する migration。
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<!-- event: migration author: tickets.sh-migration at: 2026-05-27T00:00:08Z -->
|
||||
|
||||
## Migrated
|
||||
|
||||
Migrated from tickets/pod-scope-persistence-authority.md. No legacy review file was present at migration time.
|
||||
|
||||
---
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<!-- event: migration author: tickets.sh-migration at: 2026-05-27T00:00:09Z -->
|
||||
|
||||
## Migrated
|
||||
|
||||
Migrated from tickets/pod-session-fork.md. No legacy review file was present at migration time.
|
||||
|
||||
---
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user