Compare commits

..

508 Commits

Author SHA1 Message Date
abe21a5e8a
chore: tune project memory thresholds 2026-05-26 09:05:14 +09:00
9707a0173a
docs: add memory summary resident injection ticket 2026-05-26 08:50:58 +09:00
e95c35b76d
merge: memory consolidation skip observability 2026-05-26 08:37:32 +09:00
28ad8f01ec
fix: confirm SpawnPod initial run delivery 2026-05-26 08:37:24 +09:00
0a07c50be4
chore: ignore generated insomnia memory 2026-05-26 08:14:46 +09:00
5872a53ec1
fix: suppress memory idle skip notices 2026-05-26 08:03:17 +09:00
5b3b16c4b2
docs: refine memory consolidation skip ticket 2026-05-26 07:53:37 +09:00
46765404bf
chore: complete llm retry continuation ticket 2026-05-26 07:22:45 +09:00
3d3db8b6ac
feat: surface llm retry and continuation state 2026-05-26 07:13:59 +09:00
3f750668ba
docs: refine llm retry continuation ticket 2026-05-26 05:20:43 +09:00
fe9b12aa65
docs: note spawnpod delivery race precedent 2026-05-25 07:03:00 +09:00
a414655366
docs: add spawnpod run delivery ticket 2026-05-25 06:37:38 +09:00
0b6f09c112
docs: add live pending pod picker ticket 2026-05-25 06:29:13 +09:00
e5b918283a
docs: add memory consolidation skip ticket 2026-05-25 05:43:06 +09:00
8dc23183c1
docs: specify stream continuation policy 2026-05-25 04:48:07 +09:00
2bd73fdca8
chore: complete memory audit log ticket 2026-05-25 03:38:18 +09:00
6dc696a461
merge: memory-audit-log 2026-05-25 03:38:03 +09:00
7c14b51bac
memory: add audit log events 2026-05-25 03:24:04 +09:00
8653fdd3e5
docs: add actionbar notice api ticket 2026-05-25 02:40:59 +09:00
ef9c23251e
docs: expand memory audit log ticket 2026-05-25 02:06:42 +09:00
9172ad3af1
fix: refine command mode footer 2026-05-25 01:08:41 +09:00
dae7d10fd4
chore: complete tui-system-command-compact ticket 2026-05-24 09:40:41 +09:00
49abacf694
merge: tui-system-command-compact 2026-05-24 09:40:25 +09:00
7054a179d4
test: clean up compact event assertion 2026-05-24 09:39:57 +09:00
2109733cb7
feat: add manual compact command 2026-05-24 08:59:44 +09:00
ebff9a0293
chore: complete tui-command-mode ticket 2026-05-24 08:39:25 +09:00
8daa0f1a01
merge: tui-command-mode 2026-05-24 08:38:39 +09:00
1f8afd1243
feat: add TUI command mode 2026-05-24 08:32:21 +09:00
f6024c0c2c
docs: replace gui mvp with tui spawned pod panel 2026-05-24 08:10:21 +09:00
0fd0a89730
docs: split tui command and navigation tickets 2026-05-24 07:59:51 +09:00
2b547d6dd9
chore: complete worker-history-append-contract ticket 2026-05-24 07:37:29 +09:00
081070f03e
merge: worker-history-append-contract 2026-05-24 07:37:05 +09:00
614d461877
docs: split maintainer workflows by role 2026-05-24 07:34:30 +09:00
f1bd498df7
fix: route worker history appends through callbacks 2026-05-24 06:44:19 +09:00
6e4512afeb
chore: drop stale tui spawn error todo 2026-05-24 06:29:15 +09:00
918ed3900a
chore: complete tui-input-queue ticket 2026-05-23 13:58:09 +09:00
ff747da1a0
merge: tui-input-queue 2026-05-23 13:57:32 +09:00
c8810280af
feat: queue tui input during runs 2026-05-23 13:57:22 +09:00
82775bf9d3
docs: add manual turn rollback ticket 2026-05-23 13:35:03 +09:00
0775b4112b
chore: complete tui-empty-turn-restore ticket 2026-05-23 13:30:01 +09:00
e79e7362f8
merge: tui-empty-turn-restore 2026-05-23 13:29:07 +09:00
e078be443a
feat: restore rolled back tui input 2026-05-23 13:28:56 +09:00
3db9726062
chore: complete pod-empty-turn-rollback ticket 2026-05-23 12:52:42 +09:00
b9498810a4
merge: pod-empty-turn-rollback 2026-05-23 12:52:12 +09:00
fbe8e64410
chore: handle rolled back run result clients 2026-05-23 12:51:40 +09:00
de653f546a
feat: rollback empty interrupted turns 2026-05-23 12:50:46 +09:00
63407f153c
fix: make visible pod list schema object 2026-05-23 12:29:37 +09:00
ee9bedc5de
chore: complete pod-discovery-restore-tools ticket 2026-05-23 12:05:30 +09:00
da7c1a54a1
merge: pod-discovery-restore-tools 2026-05-23 12:04:59 +09:00
9f7c2f3856
feat: add visible pod discovery tools 2026-05-23 12:04:45 +09:00
3f7de349c3
chore: complete memory-extract-remove-input-cap ticket 2026-05-23 09:14:37 +09:00
1feb560ff9
merge: memory-extract-remove-input-cap 2026-05-23 09:14:15 +09:00
902083bd38
fix: remove memory extract input cap 2026-05-23 09:14:07 +09:00
221b1edd92
chore: complete tui-pod-restore-picker ticket 2026-05-23 09:13:57 +09:00
828004a5e2
merge: tui-pod-restore-picker 2026-05-23 09:13:19 +09:00
b26bc420f1
feat: restore tui sessions by pod 2026-05-23 09:13:06 +09:00
fb7abb1b7c
chore: complete spawned-delegation-scope-reclaim ticket 2026-05-23 08:39:04 +09:00
66996f902b
merge: spawned-delegation-scope-reclaim 2026-05-23 08:38:50 +09:00
d62cd09c4d
fix: reclaim delegated scope from stopped children 2026-05-23 08:38:42 +09:00
a4f03e7688
docs: refine pod visibility and tui restore flow 2026-05-23 08:33:00 +09:00
5ade50dec5
update: tui -rの際のリストの時系列ソート 2026-05-23 08:02:05 +09:00
dbb6cca894
chore: complete tui-streaming-input-loss ticket 2026-05-23 07:16:08 +09:00
7c9abb37ad
merge: tui-streaming-input-loss 2026-05-23 07:15:55 +09:00
0535260c8a
fix: preserve tui input during streaming 2026-05-23 07:15:39 +09:00
c435635e5b
chore: complete tui-context-usage-indicator ticket 2026-05-23 07:15:30 +09:00
c78cd28b27
merge: tui-context-usage-indicator 2026-05-23 07:15:17 +09:00
abe890cda5
feat: show context usage in tui status 2026-05-23 07:15:03 +09:00
fa04b03643
docs: identify tui streaming input loss race 2026-05-23 05:47:59 +09:00
1bebd8b6df
Create tui-parts.md 2026-05-23 05:41:48 +09:00
4dec7f916f
fix: tighten task tool usage guidance 2026-05-23 05:11:48 +09:00
8662ca404f
chore: complete prune-token-budget ticket 2026-05-23 05:00:30 +09:00
5f2efeb75e
merge: prune-token-budget 2026-05-23 05:00:15 +09:00
820dea1873
feat: protect prune tail by token budget 2026-05-23 05:00:06 +09:00
8d5ee0f0b8
chore: complete pod-event-callback-delivery ticket 2026-05-23 04:57:26 +09:00
0d39170bbe
merge: pod-event-callback-delivery 2026-05-23 04:57:10 +09:00
16963d15f2
fix: drain snapshots before pod callbacks 2026-05-23 04:57:03 +09:00
2cea02648f
docs: add memory extract input cap ticket 2026-05-23 04:42:38 +09:00
f55503edc3
docs: add pod event callback delivery ticket 2026-05-23 03:29:01 +09:00
d18e3a0256
docs: add spawned delegation scope reclaim ticket 2026-05-23 03:02:48 +09:00
5ba4be1c9b
refactor: remove legacy plural log entries 2026-05-23 02:03:42 +09:00
90d4c8f5ad
docs: track read pod output log entry bug 2026-05-23 00:53:47 +09:00
61c9719da5
docs: add pod discovery restore tools ticket 2026-05-23 00:09:34 +09:00
73fbdcc025
chore: complete spawned-registry-persist ticket 2026-05-22 23:30:16 +09:00
5fdc46db47
merge: spawned-registry-persist 2026-05-22 23:30:06 +09:00
534c6f4cac
feat: persist spawned pod registry 2026-05-22 23:30:02 +09:00
5540ca3d0e
chore: complete pod-name-resume ticket 2026-05-22 22:57:31 +09:00
edfdca3457
merge: pod-name-resume 2026-05-22 22:57:23 +09:00
bd32f704b4
feat: resume pods by name 2026-05-22 22:57:16 +09:00
e55fc9a834
chore: complete pod-state-write-points ticket 2026-05-22 22:29:23 +09:00
7f6e3b949f
merge: pod-state-write-points 2026-05-22 22:29:12 +09:00
0954a4804a
feat: wire pod metadata lifecycle writes 2026-05-22 22:29:08 +09:00
e9cc4b90dc
chore: complete pod-state-backend ticket 2026-05-22 22:03:36 +09:00
8aca67c97c
style: run cargo fmt 2026-05-22 22:03:27 +09:00
d7eabb18c9
merge: pod-state-backend 2026-05-22 22:03:17 +09:00
b13c2735bb
feat: add pod metadata store backend 2026-05-22 22:03:11 +09:00
67f135fbc6
Merge: live-fork-marker 2026-05-20 06:45:49 +09:00
a728b7045d
chore: 空になった Storage 親見出しを TODO から削除 2026-05-20 06:45:43 +09:00
3eabcb6a6d
ticket: live-fork-marker 完了 2026-05-20 06:45:19 +09:00
ffcba3aa54
chore: auto-fork ロジック二重実装を KNOWN_ISSUES に登録 2026-05-20 06:45:14 +09:00
eb752fb295
ticket: live-fork-marker レビュー (Approve) 2026-05-20 06:44:54 +09:00
ac3ee5fcbe
feat: live auto-fork の marker 形式を確定(seq 比較 + forked_from 記録)
方針: 末尾 entry-count 比較で検知し、元 Segment は immutable のまま
(terminal marker を書き戻さない)。fork lineage は新 Segment の
SegmentStart.forked_from に前向きに記録するため、log だけから辿れる。
過去 fork と対称で、nested fork も marker 位置の調停が不要。

- session-store ensure_head_or_fork に at_turn_index 引数を追加し
  新 Segment へ forked_from を記録
- pod ensure_segment_head の auto-fork も同様に forked_from を記録
  (at_turn_index = writer の現 turn_count)
- fork_at の doc に「元 Segment を mutate しない」invariant を明記
- test: nested past-fork が祖先を不変に保つ / Pod 並行 writer drift で
  auto-fork し forked_from を記録 / 元 Segment に marker が書かれない
2026-05-20 06:42:09 +09:00
46b0e20685
Merge: session-grouping-introduce 2026-05-20 06:29:48 +09:00
6a4ee37be8
ticket: session-grouping-introduce 完了 2026-05-20 06:29:43 +09:00
3f3ead3b71
update: session-grouping review follow-up
- PickerOutcome::Picked から未使用の session_id を除去(pod-cli が lookup_session_of で再解決)
- picker preview が singular AssistantItem も拾うように
- fs_store layout doc に migration(後方互換なし、旧 flat sessions は破棄)を明記
- TaskStore は Session-lifetime、ScopedFs/Tracker は Pod-process lifetime と用語整理
- Pod::session_id / from_manifest_spawned のコメント補強
2026-05-20 06:29:37 +09:00
a6cc05f74c
feat: Session(Segment 群の grouping)を導入
- SessionId 型を新設、各 SegmentStart に session_id を持たせる
- compaction / 内部 fork は同 SessionId を継承、fork() は新 Session を発行
- Store API を (SessionId, SegmentId) ベースに、FsStore layout は
  <root>/<session_id>/<segment_id>.jsonl に
- Store::list_sessions / list_segments(session_id) / lookup_session_of を追加
- restore_by_segment shim を session-store に提供(pod-cli --session で使用)
- SegmentState に SegmentLocation (session_id, segment_id) を保持し ArcSwap で更新
- RestoredState に session_id: Option<SessionId> を追加
- Picker は Session 単位に列挙、leaf segment を解決して resume
2026-05-20 06:17:56 +09:00
e4cda5d3f2
Merge: segment-rename 2026-05-20 05:18:11 +09:00
dd9abfee2e
ticket: segment-rename 完了 2026-05-20 05:18:04 +09:00
d7ff25b6a7
update: 残存 Session 識別子の Segment 化(review follow-up)
レビュー指摘の通り、次の session-grouping-introduce で新 SessionId が
入る前に名称衝突を避けるため取り残しを掃除。

- PodError::Session{Empty,ScopeMissing} → Segment{Empty,ScopeMissing}
- ScopeLockError::SessionConflict → SegmentConflict
- Pod.session_state / SegmentState.set_session_id 系
- source_session_id / prev_session_id / ensure_session_head / short_session
- pod_cli の "Session ID:" 表示
- fs_store の sessions ローカル変数
2026-05-20 05:17:49 +09:00
7577577c9f
update: Session-lifetime/scoped を Pod-lifetime に修正
タスクストア/ファイルトラッカーは compaction を跨いで Pod プロセス寿命まで生きる。
旧 SessionId = Segment の時代の表現を Pod-lifetime に正す。pod_cli の表示も Segment: に。
2026-05-20 05:06:38 +09:00
0d7c37f673
update: SessionId / SessionStart / SessionOrigin 等を Segment 系名称へ
- Type/Function/Variantを Segment* 系へ統一
  - SessionId/SessionStart/SessionOrigin/SessionStartState/SessionState/SessionLogSink/SessionLockInfo
  - new_session_id / session_id / create_session* / list_sessions / lookup_session / update_session / find_by_session
  - protocol Event::SessionRotated → SegmentRotated、CompactDone.new_session_id → new_segment_id
- Module: session_log → segment_log / session → segment (file mv 含む)
  pod 側の session_log_sink → segment_log_sink も同様
- crate 名 (session-store)、CLI flag (--session)、ResumeWithSession (CLI tied) は据え置き
- session-tests/session_metrics_test 等の Store impl も追従
2026-05-20 05:06:04 +09:00
d5c7330659
Merge: entry-hash-abolish 2026-05-20 04:53:52 +09:00
9c1f51b4f0
ticket: entry-hash-abolish 完了 2026-05-20 04:53:47 +09:00
1d8a490504
update: 旧用語コメントの掃除と KNOWN_ISSUES 追記
- 残存していた head_hash / SessionHead 言及コメントを 3 箇所更新
- FsStore::read_entry_count の O(n) 計測コストを KNOWN_ISSUES に登録
2026-05-20 04:53:33 +09:00
6e791d8668
ticket: entry-hash-abolish レビュー (Approve) 2026-05-20 04:49:17 +09:00
d5dff6d17b
update: entry hash chain と session_head mutex を撤廃
- HashedEntry / EntryHash / compute_hash / build_chain 撤去、JSONL は 1 行 1 LogEntry
- SessionOrigin.at_hash → at_turn_index (TurnEnd 由来) に置換
- Pod 側 SessionHead mutex を ArcSwap<SessionId> + AtomicUsize の SessionState に置換
- ensure_head_or_fork は store の entry count と writer の append tally で判定
- session-store から sha2 / hex 依存、pod から parking_lot 依存を削除
2026-05-20 04:31:37 +09:00
35c15923db
ticket: 永続化整理を 8 個に分割
persistence-semantics と pod-persistent-state を実装可能な粒度に分割。
Storage 層 (Phase 1) を entry-hash-abolish / segment-rename /
session-grouping-introduce / live-fork-marker に、Pod 単位永続化
(Phase 2) を pod-state-backend / pod-state-write-points /
pod-name-resume / spawned-registry-persist に切り出した。
2026-05-20 04:07:44 +09:00
be5e413b55
Merge: invoke-turn-llmcall-semantics
# Conflicts:
#	crates/pod/src/controller.rs
2026-05-15 22:08:41 +09:00
58c2a51ae1
ticket: invoke-turn-llmcall-semantics 完了 2026-05-15 21:54:40 +09:00
e00e284d8c
ticket: worker-history-append-contract 作成 2026-05-15 21:53:24 +09:00
e5f5670f68
chore: KNOWN_ISSUES に controller_test::double_run_returns_error の flakiness を追記 2026-05-15 21:52:40 +09:00
a2376b0742
ticket: pod-interrupt-prep-internalize 完了 2026-05-15 21:52:24 +09:00
fbd7d8acb7
ticket: pod-interrupt-prep-internalize レビュー (Approve with follow-up) 2026-05-15 21:51:57 +09:00
282a857248
update: Paused→Run の interrupt 前処理を Pod::run に内包 2026-05-15 21:51:57 +09:00
9304b52f17
ticket: invoke-turn-llmcall-semantics review (Approve) 2026-05-15 21:42:43 +09:00
d0dbac109d
feat: Invoke marker と LlmCall callback を導入し AgentTurn セマンティクスを明確化
- protocol: InvokeKind enum、Event::InvokeStart / LlmCallStart / LlmCallEnd 追加
- llm-worker: Worker.llm_call_count と on_llm_call_start/end callback、turn_count を AgentTurn 数として doc 更新
- session-store: LogEntry::Invoke { ts, trigger } 追加 (replay は marker のみで state 不変)
- pod: run/run_for_notification 開始時に Invoke marker commit、PendingRun::RunForNotification(InvokeKind) で kind を伝搬
- pod ipc: sink + server で Invoke エントリーを Event::InvokeStart として broadcast
- tui: 新 Event 3種を no-op で受理 (UI 設計はチケット範囲外)
2026-05-15 07:04:26 +09:00
d710cac879
ticket: invoke/turn/llmcall 決定事項と実装範囲を明文化 2026-05-15 06:48:57 +09:00
bca9161a42
ticket: Exchange語撤廃、Invoke/Turn/LlmCall でセマンティクスを再整理 2026-05-15 05:41:13 +09:00
61b4c0f5cd
ticket: pod-input-validate-internlize完了 2026-05-15 05:38:27 +09:00
d076258d30
update: Controllerで入力のValidationを行っていた部分をPod側に移す 2026-05-15 05:33:33 +09:00
b5069a9f82
ticket: PodとControllerの責務の抱え違いを修正するチケット 2026-05-15 04:52:39 +09:00
da417efddd
ticket: pod-parent-turn-callback完了 2026-05-15 04:43:12 +09:00
21053f7d01
ticket: pod-parent-turn-callbackレビュー 2026-05-15 04:42:29 +09:00
456af3167b
ticket: 消し忘れ 2026-05-15 04:39:30 +09:00
8019d0d77c
update: 親にターン完了を通達する経路の整理 2026-05-15 04:38:53 +09:00
6e5b1482e6
update: エントリの単数化のフォローアップ 2026-05-14 19:42:23 +09:00
7520dcad87
update: 書き込みの不要なasyncを削除 2026-05-14 19:16:48 +09:00
112ccb2365
ticket: 書き込みのsync化を計画 2026-05-14 16:45:58 +09:00
fe9cecb51a
update: SystemItem1本化 2026-05-14 14:36:29 +09:00
65a5e68035
ticket: イベントプロトコルと永続化におけるシステムイベントの統合 2026-05-14 04:12:40 +09:00
63e27b2dee
chore: cargo fmt 2026-05-14 03:36:08 +09:00
350bb1afd8
fix: 実態にそぐわないEvent::Entryを実装した構造を訂正 2026-05-14 03:35:52 +09:00
0f76142993
refactor: Podのメインループのリファクタリング 2026-05-14 03:27:49 +09:00
7c66b7e073
ticket: 追加:Podのメインループとソケット通信周りのリファクタリング 2026-05-13 22:16:25 +09:00
69d67ab050
ticket: add tui manual compact command 2026-05-13 06:50:27 +09:00
877e094a53
docs: update pod cli manifest flags 2026-05-13 06:44:48 +09:00
5a16cc6daf
ticket: note tui user manifest overlay mismatch 2026-05-13 06:41:23 +09:00
b0c91049b1
close: complete pod manifest and file ref tickets 2026-05-13 06:30:45 +09:00
418451ebf8
merge: file-ref-directory 2026-05-13 06:30:45 +09:00
3d0dce2a2e
merge: pod-cli-manifest-flags 2026-05-13 06:30:45 +09:00
4bde31e952
review: file-ref-directory 2026-05-13 06:30:45 +09:00
533610f053
review: pod-cli-manifest-flags 2026-05-13 06:30:45 +09:00
8e50a9583a
refactor: PodControllerの構造のリファクタリング 2026-05-13 06:07:38 +09:00
cb24586362
docs(tickets): PodControllerの構造調整チケット作成 2026-05-13 05:43:23 +09:00
e32b208dee
chore: planの更新 2026-05-13 05:42:55 +09:00
7363b105f6
feat: handle directory file refs 2026-05-13 02:57:58 +09:00
0ebe173009
feat: organize pod manifest cli flags 2026-05-13 02:57:50 +09:00
023de0f58d
feat: Languageインストラクションの追加 2026-05-13 02:27:30 +09:00
eae0efb2c0
update: fmt + memoryに用いる言語の構成 2026-05-13 01:57:04 +09:00
0141880b9d
fix: compact時にToolCallとOutputの間でCutしてしまう問題 2026-05-13 00:59:02 +09:00
2b5da965ca
chore: workflowの調整・knowledgeの追加テスト 2026-05-13 00:06:33 +09:00
ba72a66a40
merge: lint common crate 2026-05-12 21:56:49 +09:00
671e07a31e
chore: complete lint common crate ticket 2026-05-12 21:56:39 +09:00
7ce4600a42
refactor: extract shared lint record primitives 2026-05-12 21:56:25 +09:00
2f70411254
docs(tickets): submit時FileRefでディレクトリを参照した時の挙動 2026-05-12 17:39:40 +09:00
7c5b810fa1
docs(tickets): mainfest-output-upload-limits完了 2026-05-12 17:27:47 +09:00
86fc889606
feat: add manifest output upload limits 2026-05-12 16:20:15 +09:00
59bf20f2cd
Merge branch 'tui-knowledge-completion' into develop 2026-05-12 15:43:29 +09:00
f7f59dd30c
docs(memory): fix knowledge dir path in collect_resident_knowledge doc 2026-05-12 15:07:39 +09:00
806440ac7a
docs(tickets): review tui knowledge completion (approve) 2026-05-12 14:56:30 +09:00
7b8eb3af8d
feat(pod): wire knowledge slugs into # completion 2026-05-12 14:45:46 +09:00
705c873097
docs(tickets): tui knowledge completion unimplemented fix 2026-05-12 14:40:37 +09:00
ae6c27a5c7
docs(tickets): define work item query strategy 2026-05-12 02:32:32 +09:00
03a577527a
docs(tickets): use timestamp work item ids 2026-05-12 02:07:29 +09:00
b4dff2835e
docs: add ai maintainer work item plan 2026-05-12 01:53:52 +09:00
e87a515474
docs(tickets): add lint-common crate ticket 2026-05-12 00:06:06 +09:00
9df6bd5fcb
merge: workflow crate extraction 2026-05-11 22:50:19 +09:00
6610ef8150
docs(tickets): complete workflow crate extraction 2026-05-11 22:50:06 +09:00
7159a66a60
review: workflow crate extraction 2026-05-11 22:49:50 +09:00
7db4146f3d
refactor: extract workflow crate 2026-05-11 22:49:07 +09:00
d8f29bcbcb
merge: anthropic assistant burst bundling 2026-05-11 22:24:36 +09:00
f444b387be
docs(tickets): complete anthropic assistant burst bundling 2026-05-11 22:23:53 +09:00
d18f536945
review: anthropic assistant burst bundling 2026-05-11 22:23:38 +09:00
19badfe8b7
fix: bundle anthropic assistant bursts 2026-05-11 22:22:36 +09:00
31f94bf791
merge: memory usage metrics 2026-05-11 21:46:24 +09:00
ac09bfcc21
docs(tickets): complete memory usage metrics 2026-05-11 21:46:19 +09:00
76f83a0894
review: memory usage metrics 2026-05-11 21:46:19 +09:00
d581a35426
feat: add memory usage event metrics 2026-05-11 21:29:48 +09:00
f69aa469f8
docs(tickets): complete memory phase naming cleanup 2026-05-11 17:16:36 +09:00
3d4d83db68
docs(tickets): simplify memory usage metrics 2026-05-11 16:54:23 +09:00
9abddb5a95
fix: remove remaining memory phase wording 2026-05-11 01:57:39 +09:00
075730d0a6
docs(tickets): compact-worker-occupancy-cap完了 2026-05-11 01:56:20 +09:00
aae36f2b56
update: memoryシステムの"Phase"表記を撤廃 2026-05-11 01:55:28 +09:00
e418f3996f
docs(tickets): memory-extract-occupancy-cap 完了 2026-05-11 01:32:45 +09:00
240b36d738
review: memory-extract-occupancy-cap (approve) 2026-05-11 01:25:20 +09:00
0356e29707
feat: extract worker サーキットブレーカーを占有量ベースに統一 2026-05-11 01:20:37 +09:00
eec33aba98
docs(tickets): add memory-extract-occupancy-cap ticket 2026-05-11 01:14:59 +09:00
248e3d7aa2
Merge branch 'compact-worker-occupancy-cap' into develop 2026-05-11 01:12:32 +09:00
3beaff7679
review: compact-worker-occupancy-cap (set_max_turns 分岐削除) 2026-05-11 00:56:41 +09:00
ef0cdf75e2
feat: compact worker サーキットブレーカーを占有量ベースに統一 2026-05-11 00:43:16 +09:00
5976aac78d
docs(tickets): add memory audit log ticket 2026-05-11 00:06:42 +09:00
d818b37f3d
docs(tickets): completed tickets cleanup 2026-05-10 17:31:34 +09:00
f8bae4a298
merge: memory prompt record policy 2026-05-10 14:40:58 +09:00
3abf4d4d1a
docs: generalize memory prompt record policy 2026-05-10 14:40:52 +09:00
e2a6c43fea
docs: memory effectiveness plan 2026-05-10 01:25:10 +09:00
df01d8e567
docs: memory prompt ticket policy ticket 2026-05-10 01:13:57 +09:00
e647d1a7c9
feat: client-crateの実装 2026-05-10 00:57:50 +09:00
29f45bee6e
chore: E2Eの計画とgit運用の話 2026-05-09 05:04:57 +09:00
576814ed20
docs(tickets): file-ref-symlink-diagnostics完了 2026-05-09 04:22:27 +09:00
5590fc4ff1
docs(tickets): file-ref-symlink-diagnosticsレビュー 2026-05-09 04:21:56 +09:00
25d22fc4af
feat: Toolsのシンボリックリンク対応 2026-05-09 04:21:56 +09:00
9194b10d50
docs(tickets): tui-assistant-markdown完了 2026-05-09 03:31:49 +09:00
99bc161e43
docs(tickets): permission既定policy整理チケット追加 2026-05-09 03:27:22 +09:00
37dfd7e327
docs(tickets): permission-extension-point完了 2026-05-09 03:20:17 +09:00
7bbc9afc7a
feat: パターンベースのツール権限制御を追加 2026-05-09 03:20:02 +09:00
3337731222
chore: tui compact progress ticket完了 2026-05-09 03:14:23 +09:00
16bd8e3a88
feat: compactのプログレス表示 2026-05-09 03:11:53 +09:00
ed08ee1ce1
chore: git方針の変更とセマンティクス変更の計画の帳尻合わせ 2026-05-08 20:17:11 +09:00
12e7d27a08
docs(tickets): 自己改善workflowの設計 2026-05-08 01:50:55 +09:00
f09e6a0156
docs(tickets): workflow-directory-layout完了 2026-05-08 01:08:25 +09:00
8c12e799da
update: Workflowディレクトリ修正のフォローアップ 2026-05-08 00:59:08 +09:00
04f1837fa9
feat: Workflowの読み取り位置変更の実装 2026-05-08 00:15:50 +09:00
5ec24707f4
docs(tickets): reportの運用・Workflowのディレクトリ位置修正 2026-05-07 23:34:00 +09:00
c0c5eb9ad2
feat: TUIのmarkdown対応 2026-05-05 18:30:25 +09:00
e9e80c5918
docs(tickets): PermissionのチケットとTUIのmd表示 2026-05-05 17:16:03 +09:00
f4ab361889
docs(tickets): agent-skills完了 2026-05-05 16:00:40 +09:00
60c779b80c
update: Agent skills実装のレビュー・対応 2026-05-05 13:54:02 +09:00
50fa2ce3f7
feat: writingに対する基本的な指示promptを追加 2026-05-05 13:42:34 +09:00
760b304969
feat: agent skillsの互換実装 2026-05-05 13:16:10 +09:00
5fe9a5805e
fix: Reasoningの永続化のスキーマのミスを修正 2026-05-05 12:30:29 +09:00
64b5d61a23
docs(tickets): turnのセマンティクスを変える計画 2026-05-05 12:29:52 +09:00
461d7f9142
docs(tickets): reasoning-history-perisit完了 2026-05-04 23:06:21 +09:00
6f62ea8ce8
update: Reasoningコンテキスト管理のレビュー・対応 2026-05-04 23:05:08 +09:00
9fd61e8068
feat: Reasoningのコンテキスト管理の対応 2026-05-04 21:31:44 +09:00
dd3903efde
docs(tickets): Reasoningのコンテキスト管理とPruneの調整チケット追加 2026-05-04 21:16:31 +09:00
089db05535
docs(tickets): tui-task-display完了 2026-05-04 20:43:21 +09:00
0a83909f30
feat: Task表示のレビュー・修正 2026-05-04 17:28:39 +09:00
9072ac4e03
feat: TUI上に進行中のTaskを表示する実装 2026-05-04 17:06:02 +09:00
6178812979
docs(tickets): Compaction進行中のライブ表示 2026-05-04 17:03:51 +09:00
d385a72d85
docs(tickets): post-run memory detach 完了 2026-05-04 16:11:38 +09:00
c48b99cfe3
feat: Pos処理の非同期化・Busy状態の削除 2026-05-04 15:52:27 +09:00
632d63df33
docs(tickets): 追加:タスクリストの表示とコンテキスト長インジケータ 2026-05-04 15:32:40 +09:00
107dcf6636
docs(tickets): Busyの切り離し 2026-05-04 13:20:25 +09:00
7f9d2f93f9
Merge branch 'llm-worker-transient-retry' into develop 2026-05-04 13:16:26 +09:00
9771533b31
docs(tickets): pod状態のTUI同期完了 2026-05-04 13:08:44 +09:00
36a4e9f9b8
feat: Podのステータス同期の修正 2026-05-04 12:55:29 +09:00
0be30052c1
feat: Podのステータスを厳密にし、同期漏れを防ぐ 2026-05-04 12:55:11 +09:00
72e03f9e8f
docs(tickets): llm-worker-transient-retry完了 2026-05-04 12:51:41 +09:00
09a1cde92c
docs(tickets): llm-worker-transient-retry レビュー追記
7183847 のレビュー結果を Approve として記録する。チケット要件
(リトライ対象 / バックオフ / Retry-After 上書き / mid-stream 温存 /
完了条件) はすべて満たしており、コードベースの層構造を歪める変更も
ない。Retry-After テストの方針差 (実時間 1s vs 仮想時間 5s) と
connect refused テストの試行回数未検証は non-blocking として
review.md に記録。
2026-05-04 12:49:13 +09:00
7183847ee5
feat(llm-worker): HTTP transient エラーへのリトライを追加
`transport.rs` の HTTP 送信〜ステータスチェック区間に指数バックオフ
+ フルジッターのリトライループを追加する。SSE 読み出し開始後 (
`bytes_stream()` 以降) のエラーは従来どおりそのまま流す。

- `is_retryable(&ClientError)`: 408/425/429/500/502/503/504/529 と
  reqwest の connect/timeout のみ true
- `RetryPolicy` (default: base 500ms / cap 10s / max_attempts 4 /
  total_timeout 30s)
- `Retry-After` ヘッダ (秒数) があればバックオフを上書き
- リトライ発火ごとに warn! でステータス・attempt・wait を出す

ref: tickets/llm-worker-transient-retry.md
2026-05-04 12:45:33 +09:00
1451998e0e
Merge branch 'tui-system-message-render' into develop 2026-05-04 12:10:17 +09:00
5bc6fb4b5c
docs(tickets): tui-system-message-render完了 2026-05-04 12:05:50 +09:00
ac1a672973
feat: システムメッセージをTUIで表示させる 2026-05-04 12:04:09 +09:00
4ec1c8b64c
update: Taskツールの説明を更新 2026-05-04 11:32:04 +09:00
771503e69c
docs(tickets): tuiトークン表示完了 2026-05-04 00:07:59 +09:00
89a66f1d58
docs(tickets): tuiトークン表示レビュー 2026-05-04 00:05:59 +09:00
8be579dc3c
feat: tuiのトークン集計表示の修正 2026-05-04 00:01:37 +09:00
ffd59b05a1
docs(tickets): TUI表示トークンの集計の修正 2026-05-03 23:28:31 +09:00
d2f2b7920d
docs(tickets): チケット追加:システムメッセージのTUI表示とセッションのロールバック・フォーク 2026-05-03 22:43:21 +09:00
357fedc1a1
docs(tickets): tui-pod-event-render 完了 (消し忘れ片付け) 2026-05-03 22:14:24 +09:00
a07ccb0158
update: Taskツール群の説明を更新 2026-05-03 22:09:45 +09:00
9cbcd87f20
docs(tickets): notify-history-persist 完了 (消し忘れ片付け) 2026-05-03 22:07:18 +09:00
1602eea2c8
docs(tickets): session-todo-reminder spec を pending_history_appends に改訂 (AGENTS.md 揮発禁止に整合) 2026-05-03 21:53:20 +09:00
878e64597e
Merge branch 'session-todo-tools' into develop
# Conflicts:
#	tickets/session-todo.md
2026-05-03 21:50:30 +09:00
46208a3b45
docs(tickets): session-todo (本体) 完了 2026-05-03 21:48:44 +09:00
c214ea79d4
update: tuiからspawnする際にエラー詳細が落ちていた問題を修正 2026-05-03 21:47:54 +09:00
c693126703
docs(tickets): notify-history-persist完了 2026-05-03 21:37:13 +09:00
46390a9006
docs(tickets): session-todo レビュー反映 (Approve) + reminder spec 段階レビュー 2026-05-03 21:34:54 +09:00
420f74edc6
fix: TaskStore snapshot を JSON ブロック化 + 構造ラウンドトリップテスト追加 2026-05-03 21:33:50 +09:00
ceafff92b6
fix: TaskStore snapshot を compact 後 history の末尾に置いて retained 中の TaskCreate 重複を防ぐ 2026-05-03 21:26:49 +09:00
e8045776f2
feat: notify-history-persist実装 2026-05-03 19:27:22 +09:00
6f2aca84bf
feat: セッション内 Task ツール (TaskCreate/List/Get/Update + 履歴 replay + compact 跨ぎ) 2026-05-03 19:03:52 +09:00
28fe1dae1c
docs(tickets): セッション内 Task ツールを本体と注意機構に分割 2026-05-03 19:03:48 +09:00
ee9c60bec2
Merge branch 'resume-scope-claim' into develop
# Conflicts:
#	TODO.md
2026-05-03 18:59:01 +09:00
7e0d61eb08
docs(tickets): resume-scope-claim 完了 2026-05-03 18:56:39 +09:00
4328cb334a
fix: resume-scope-claim レビュー指摘対応 (deny セマンティクス doc・破損 snapshot の警告ログ) 2026-05-03 18:56:21 +09:00
02dcf182a7
docs(tickets): resume-scope-claim レビュー (Approve) 2026-05-03 18:46:15 +09:00
8ff6318cef
docs(tickets): Notifyが永続化されいない問題についてのチケット 2026-05-03 18:45:10 +09:00
c759307e40
feat: resume時のscope claimを過去の有効scopeに揃える 2026-05-03 17:12:36 +09:00
0c10150b02
feat: session-metrics完了 2026-05-03 15:56:06 +09:00
b9635c5002
feat: session-metrics実装 2026-05-03 15:10:43 +09:00
9010a920a4
feat: TUIに他Podからの通知を表示する 2026-05-03 12:45:05 +09:00
44bc35bd31
docs(tickets): 消し忘れチケットども 2026-05-03 01:16:22 +09:00
f8a3a7838b
chore: TODOから[ ]を削除 2026-05-03 01:08:43 +09:00
8b5f75ecc4
Update AGENTS.md 2026-05-03 01:06:23 +09:00
be22c65af3
docs(tickets): tuiでPodEventを表示する・セッション中でメトリクスを取るチケットを追加 2026-05-03 01:01:09 +09:00
d9f55185f0
update: tuiの文字入力のCtrlブロックを追加 2026-05-03 00:44:38 +09:00
4b9b4f1450
update: memoryシステム周りのプロンプトの整理 2026-05-03 00:27:10 +09:00
670abdc336
docs(tickets): memory-consolidation-drop-input-cap完了 2026-05-02 23:57:36 +09:00
2d5c6aad5f
update: Consolidationの不要なToken上限の削除 2026-05-02 23:48:33 +09:00
f16ccc0a09
docs(tickets): セッション内TODOツールと注意機構のチケット 2026-05-02 23:48:01 +09:00
6ebd10a006
update: codexのキャッシュ利用が出来てなかった問題 2026-05-02 03:23:44 +09:00
d8d802d120
fix: tuiからのPod作成の挙動を修正・開発時にcargo runでpodを起動する経路を実装 2026-05-02 02:13:30 +09:00
288e2239d4
Merge branch 'workflow-impl' into develop
# Conflicts:
#	crates/pod/src/controller.rs
#	crates/pod/src/pod.rs
2026-05-02 01:47:49 +09:00
8194bb10f4
docs(tickets): workflow完了 2026-05-02 01:40:06 +09:00
a9ad0c2e0d
update: workflowの実装修正 2026-05-02 01:38:50 +09:00
8ed739261f
docs(tickets): 消し忘れチケット 2026-05-02 01:36:19 +09:00
5a29c90786
feat: dynamic-scopeの実装修正 2026-05-02 01:33:32 +09:00
0d66b397af
feat: dynamic-scopeの実装 2026-05-02 01:26:17 +09:00
fa84d48c62
fix: SpawnPodの起動経路の問題・を修正 2026-05-02 01:09:57 +09:00
433ee0f37c
Implement workflow MVP 2026-05-02 00:46:47 +09:00
00755cf1b8
update: manifestで一部値のzeroの扱いを変更 2026-05-02 00:08:46 +09:00
31d9b9b2b7
fix(llm-worker): openai_responsesのroleの最新の投影を反映 2026-05-01 23:55:26 +09:00
d18de45293
chore: dev-depsの整理 2026-05-01 23:50:14 +09:00
2790a35acf
docs(tickets): workflowのプロパティ名の修正 2026-05-01 23:40:47 +09:00
776a6a29bd
chore: 依存パッケージの集約 2026-05-01 23:35:46 +09:00
0046f1efc9
Merge branch 'tui-mouse-scroll' into develop 2026-05-01 23:22:58 +09:00
1e060914ce
feat: memory P2の修正 2026-05-01 23:22:49 +09:00
2159711cd0
feat(tui): マウスホイールスクロール完了 2026-05-01 23:16:02 +09:00
1afe7c53aa
スキルの整理 2026-05-01 23:14:37 +09:00
25b016f3da
feat(tui): マウスホイールでスクロールする実装 2026-05-01 23:14:16 +09:00
beb6b686a1
メモリPhase2の実装 2026-05-01 23:00:55 +09:00
0070aabd26
docs: memoryシステムの仕様変更と、動的Tool・VCSの話 2026-05-01 18:47:52 +09:00
3e2c9ee32b
bashツール一旦完了 2026-05-01 18:47:09 +09:00
97f9b14ceb
bashツール実装 2026-05-01 18:14:13 +09:00
6e9ef385c8
ClaudeによるTool出力メタ認知 2026-05-01 02:47:44 +09:00
c331936455
ファイル参照を与えた際に自動的に読ませる実装 2026-04-30 21:58:10 +09:00
75c61bd3cb
TUI補完の細かい挙動修正 2026-04-30 14:38:03 +09:00
6788db1ef2
tuiの補完の実装 2026-04-30 12:46:48 +09:00
4b09ff0234
claudeの動的ツールの調査レポート 2026-04-30 01:35:42 +09:00
9177ee8ef3
fix: セッション復元時にhistoryが表示されない問題 2026-04-30 00:02:26 +09:00
d2ee84775b
cargo fmt 2026-04-29 23:20:25 +09:00
cb1d3e72e4
templatureがcodexエンドポイントで使えない件の修正 2026-04-29 23:20:16 +09:00
1b1f8f40c6
session-log関連完了 2026-04-29 23:00:55 +09:00
c70b0bdc5d
session-logリファクタのレビュー・修正 2026-04-29 22:55:36 +09:00
010edf2c94
session-log-segments実装 2026-04-29 22:42:10 +09:00
1ab6dbcee3
session-log-decouple-item実装 2026-04-29 22:24:18 +09:00
bd8154204a
session-storeの永続化形式からllm-workerの内部型を削除 2026-04-29 22:09:30 +09:00
7c2ef374e6
tui-input-word-motion完了 2026-04-29 21:45:49 +09:00
119a73c112
tui-input-word-motionレビュー・半角カナに関する修正 2026-04-29 21:41:24 +09:00
862c38d7f7
tuiの単語単位Backspace 2026-04-29 21:31:19 +09:00
0ad3923932 tuiの単語境界カーソル移動実装 2026-04-29 21:23:29 +09:00
588c25a570 workflowのチケットとtuiの単語境界カーソル移動のチケット 2026-04-29 21:22:49 +09:00
043c2e862c pod-registry-rename完了 2026-04-29 21:05:09 +09:00
27e5074450 pod-registry-rename修正 2026-04-29 21:04:47 +09:00
5fa060a748 pod-registryのモジュール分割 2026-04-29 20:14:34 +09:00
71434b9d8b scope-lock -> pod-registry 2026-04-29 20:01:32 +09:00
8a1baa5020 scope.lockの意味変更に伴うクレート名変更チケット作成 2026-04-29 19:54:08 +09:00
6fa7f169b4 memory-phase1-extract完了消し忘れ 2026-04-29 19:53:37 +09:00
44d660c894 tui-session-restore完了 2026-04-29 19:52:24 +09:00
b9575f1534 tuiからセッションを復帰する経路の実装 2026-04-29 19:03:03 +09:00
e98a596235 不要なforkの削除 2026-04-28 20:19:50 +09:00
3c90729156 resumeの実装 2026-04-28 18:52:58 +09:00
51309ec5bf max_tokenとreasoning_tokenに関するdocs修正 2026-04-28 18:01:17 +09:00
af57d5b566 max_tokensのスキーマ不整合に関する修正 2026-04-28 17:58:24 +09:00
0f6b724184 tui-thinking-display完了 2026-04-28 16:23:09 +09:00
d5d0e4124b tui-thinking-display修正 2026-04-28 16:22:45 +09:00
b192a3ce4e TUIにThinkingを表示する実装 2026-04-28 16:10:48 +09:00
1466f11a0b ThinkingのTUI表示のチケット作成 2026-04-28 16:07:41 +09:00
a79abb3c27 session-store-llm-worker-type-ownership完了 2026-04-28 15:44:16 +09:00
e72aac8cf2 セッション関連の責務の分離 2026-04-28 15:43:34 +09:00
06c5ecfeb3 memory-phase1の、トークンカウントの実装位置が悪い件 2026-04-28 14:24:38 +09:00
092386d98f memory-phase1-extract修正 2026-04-28 13:12:21 +09:00
03c4f49f73 memoryを抽出する仕組みの実装 2026-04-28 12:58:33 +09:00
ed5c07c301 session-restoreの設計更新 2026-04-28 12:42:49 +09:00
18dd98e05b session復帰経路を作るチケット・テスト用のファイルの削除 2026-04-28 12:31:38 +09:00
46e1b92ade memoryが.insomnia配下ではなくworkspace root直下を想定していた問題の修正 2026-04-28 11:53:08 +09:00
341bd71dc5 memoryのクエリと動作のテスト 2026-04-28 11:37:41 +09:00
1e068f4beb worker-generation-settings完了 2026-04-28 09:38:23 +09:00
bc9f93ab09 生成設定のmanifest化の実装 2026-04-28 09:37:22 +09:00
7d0b639fa4 cargo fmt 2026-04-27 22:51:07 +09:00
553b69b55a model-reasoning-control完了 2026-04-27 22:49:56 +09:00
532218dd40 model-reasoning-contolレビュー 2026-04-27 22:41:51 +09:00
f8948be43d model-reasoning-control実装 2026-04-27 22:25:27 +09:00
34d5c3aab6 home-dir-layout完了 2026-04-27 22:11:15 +09:00
5c91535a74 home-dir-layout修正 2026-04-27 22:10:36 +09:00
915061fd49 home-dirの整理 2026-04-27 21:45:30 +09:00
29812a9262 reasoningを利用可能にするチケット 2026-04-27 20:21:22 +09:00
b62b5c47b2 memory-resident-injection完了 2026-04-27 18:30:21 +09:00
7e921be43e メモリー内容のシステムプロンプトへの埋め込みの実装 2026-04-27 18:25:47 +09:00
2e562dc3e0 環境変数に関するチケットの修正 2026-04-27 18:11:40 +09:00
13974c66fb pod-spawn-ui完了・設定UI関連のチケット作成 2026-04-27 17:38:32 +09:00
97446180cd memory-search-tool完了 2026-04-27 17:26:07 +09:00
c73c870844 memoryサーチツールを実装 2026-04-27 17:24:08 +09:00
8ab1e5a858 manifest読み込み経路の整理チケット作成 2026-04-27 17:17:00 +09:00
d000e777b7 manifest側で設定ファイルの収集を行うようにした 2026-04-27 16:52:23 +09:00
e296320c7a tuiからSpawnする仮UI 2026-04-27 16:22:06 +09:00
db187813ff memory-file-format完了 2026-04-27 13:59:04 +09:00
837e77449d メモリーに関するクレート作成・ファイル構造の実装 2026-04-27 13:33:31 +09:00
be88b4bae5 セグメントのセッション永続化チケット 2026-04-27 13:25:16 +09:00
e122f4aadb submit-segment-protocol完了 2026-04-27 11:42:42 +09:00
ee6d2d2100 submitをvec segmentを受け付ける形に変更 2026-04-27 11:03:58 +09:00
de3e4f78ab notification-naming完了 2026-04-26 23:30:46 +09:00
7903259348 Method::NotifyとEvent::Notificationが紛らわしい問題 2026-04-26 23:25:50 +09:00
30508f7851 memory実装チケット 2026-04-26 17:00:38 +09:00
505d3a8341 カタログの実装完了、ドキュメント整理 2026-04-24 13:33:56 +09:00
bd0e10653d podのモジュール分割完了 2026-04-24 11:58:11 +09:00
79987dd754 podのモジュール分割 2026-04-24 11:48:27 +09:00
539dc604e3 modelsとprovidersをカタログ化 2026-04-24 10:45:03 +09:00
a4bd599d0f モデルとプロバイダーをカタログ化するチケット 2026-04-23 16:18:30 +09:00
553e281ba1 llm-provider-catalog実装 2026-04-23 15:37:51 +09:00
85de0aa2b1 Agents.mdを一定閾値でturncateする仕様を削除 2026-04-23 01:34:25 +09:00
4ae2a46ccb pod-prompt-catalog完了 2026-04-22 17:43:42 +09:00
c6c02f846f Promptを一元管理するファイルから参照する実装 2026-04-22 17:43:05 +09:00
6c406cdfd5 Memoryシステムの整理・Promptカタログチケット 2026-04-22 13:21:15 +09:00
bb14109b4e TUIのEditツール周りの表示とカラー 2026-04-22 01:17:58 +09:00
a39bce779c 複数クライアント間でのRunメソッドの同期漏れ 2026-04-21 23:59:49 +09:00
18533b3580 改行テキストの行計算・Padding設定 2026-04-21 23:26:34 +09:00
789348c252 TUIのオーバーホール実装 2026-04-21 23:12:35 +09:00
e0e6cc8616 protocol-tool-result-shape完了 2026-04-21 20:52:19 +09:00
b8d5398520 TUIに向けたprotocolの詳細調整 2026-04-21 20:50:59 +09:00
94e40c8ee4 TUIオーバーホールチケット 2026-04-21 19:37:14 +09:00
7698619f1b メモリシステムの設計 2026-04-21 19:23:07 +09:00
822f8d9ec2 モデル性能のハードコードを消し飛し、Codexのフォーマットの修正 2026-04-21 18:35:56 +09:00
25906220eb Docsのアップロード 2026-04-21 17:39:43 +09:00
0668bd5213 protocol拡張の実装(完了) 2026-04-21 09:30:02 +09:00
f4d21cd994 protocol拡張の実装 2026-04-21 09:27:58 +09:00
0ea1ca5ec7 protocolの拡張に関するチケット修正 2026-04-21 08:42:54 +09:00
d9358047cf llm-auth-codex-oauth完了 2026-04-20 23:14:45 +09:00
177ff80615 codexのOAuthを使う実装 2026-04-20 23:13:52 +09:00
bd8c5601c7 openai-responses完了 2026-04-20 03:00:48 +09:00
97326eef04 openai-responses対応 2026-04-20 02:59:16 +09:00
bf072cc4f0 llm-model-config完了 2026-04-20 00:57:27 +09:00
b1e4572823 llm-model-configの実装 2026-04-19 23:32:14 +09:00
1a4863d211 llmのモデル情報の設計チケット 2026-04-19 22:29:37 +09:00
ebee0b95ef マニフェストを継承してPodをスポーンさせる 2026-04-19 18:01:47 +09:00
170e0c2099 SpawnPodツールが落ちる問題の発見 2026-04-19 15:14:15 +09:00
79b0e3d51b Pause実装完了 2026-04-19 15:12:06 +09:00
0284b5a76f TUIからPauseする実装 2026-04-19 14:27:53 +09:00
78cf4599a2 compact-improvements をマージ
- 閾値の個別指定化 (compact_threshold / compact_request_threshold) と Option 化
- 占有量ソースを UsageRecord timeline に一本化 (last_input_tokens 撤去)
- retained_turns → retained_tokens
- compact worker をツール駆動に再設計 (mark_read_required / add_reference / write_summary / read_file)
- Auto-read budget と compact_worker_max_input_tokens の上限制御
- 新 history は system message のみで構成 [summary, auto-read..., references, retained...]
2026-04-19 12:14:16 +09:00
18b7556e0a compact-improvements チケット完了 2026-04-19 12:13:03 +09:00
5375698813 anthropic-cache完了 2026-04-19 12:07:03 +09:00
1a3e9030bd compact: retained_tokens テスト値を現実的な値に変更
2 を 8_000 に。retained_turns 時代の名残で 2 は "2 トークン保持" と読めてしまい意味不明だったため。
2026-04-19 12:02:11 +09:00
fc634bcd87 Anthropicのキャッシュポイントを打つ実装 2026-04-19 11:57:55 +09:00
da021103e4 compact: compact worker をツール駆動マルチターンに再設計
段階 4〜9 を一括で実装:
- mark_read_required / add_reference / write_summary + read_file の 4 ツールで
  compact worker を駆動。結果は CompactWorkerContext に集約
- 新セッションの先頭を [summary, ...auto-read, references, ...retained] で構築
- デフォルトリファレンスは tracker.recent_files(5) から
- auto-read は compact_auto_read_budget で総量制限。超過は即エラー
- compact worker 自身は compact_worker_max_input_tokens で累計入力を制限
- 5 セクション要約フォーマットに system prompt を更新
- write_summary 未呼び出し / auto-read 空のときは 1 回追加プロンプトで促す
2026-04-19 09:26:55 +09:00
db2dd8a3c0 compact: retained_turns を retained_tokens に置換
保護単位をターン数からトークン量に変更。compact 時のカット位置は
Pod::split_for_retained() で UsageRecord を逆算ソースとして決定し、
ターン境界ではなくアイテム単位で切る。デフォルトは 8000 トークン。
2026-04-19 08:56:16 +09:00
83f68e35ad compact: 要約入力から content/arguments/reasoning を除く
ToolCall.arguments, ToolResult.content, Reasoning は auto-read 側の責務。
要約は意思決定と意図のキャプチャに集中させ、コードや tool IO は持ち込まない。
2026-04-19 08:51:04 +09:00
f0a865552c compact: 閾値を個別指定化し占有量ソースを UsageRecord に一本化
- manifest に compact_request_threshold を追加 (proactive と safety net を個別指定)
- CompactState の両閾値を Option<u64> 化、last_input_tokens を撤去
- 閾値判定は Pod::total_tokens() / usage_history 経由の実測値ベースに切替
- turn_threshold → request_threshold にリネーム、Between-requests のログへ
2026-04-19 08:49:25 +09:00
a88febc15e 引数なしでToolCallすると構造エラーになる問題の修正 2026-04-19 08:39:16 +09:00
143715fb22 pod-upstream-event完了 2026-04-19 08:31:42 +09:00
3cdd8323de pod-upstream-event修正 2026-04-19 08:31:16 +09:00
7637f0e440 pod-upstream-event実装 2026-04-19 08:20:07 +09:00
cc7bb0b711 マニフェスト改修完了 2026-04-19 08:05:20 +09:00
3e788da7a7 プロジェクトManifestの相対基準の修正 2026-04-19 08:03:59 +09:00
6434b068fe マニフェスト解決の相対パス化 2026-04-19 07:53:54 +09:00
3cb9b251fe Pod操作ツール実装完了 2026-04-19 06:41:20 +09:00
1668e981b4 Pod操作ツール修正 2026-04-19 06:40:45 +09:00
bb71439787 Pod操作ツールの実装 2026-04-19 06:32:44 +09:00
8087349474 SpawnPodツール完了 2026-04-18 20:31:10 +09:00
cdbad36a48 scope-lock完了 2026-04-18 19:26:23 +09:00
2a7ee256f5 Scope-Lockの実装 2026-04-18 19:25:03 +09:00
8e43503bdb Compactのチケット修正 2026-04-18 19:15:39 +09:00
4f5f5bfe76 チケット分割 2026-04-18 18:48:26 +09:00
5786fedc1c Notificationの実装 2026-04-18 17:48:35 +09:00
e9a464a23c Interceptorの責務分離完了 2026-04-18 17:27:22 +09:00
9d038fc3b7 interceptorの修正 2026-04-18 17:19:59 +09:00
cc8c4c8189 shutdown実装完了 2026-04-16 13:55:17 +09:00
aa8a1ee64b プロトコル経由のshutdow経路 2026-04-16 13:49:53 +09:00
ac5265be41 instruction-file-refs完了 2026-04-16 13:08:08 +09:00
9e11cfac7e instructionファイルの定義・読み込みの実装 2026-04-16 11:16:16 +09:00
2052ac498c pod-factory完了 2026-04-16 00:57:26 +09:00
e8a5fe557a podのマニフェストの分離実装 2026-04-16 00:54:27 +09:00
5b25287471 tui-notification-channel完了 2026-04-15 12:59:15 +09:00
62c5cb87dd warn/errorのTUIへの通知ルート 2026-04-15 12:58:31 +09:00
6e10a722c3 greetingカードの作成 2026-04-15 10:35:15 +09:00
7c59b8677b AGENTS.md完了 2026-04-15 05:21:54 +09:00
4b1a73d38f AGENTS.mdの読み取り 2026-04-15 05:21:43 +09:00
3a02358668 tool出力制限の修正 2026-04-15 04:23:07 +09:00
fade875c6f tool出力の制限 2026-04-15 04:08:56 +09:00
61fabbc3b8 システムプロンプト完了 2026-04-15 02:46:12 +09:00
34ac754644 システムプロンプトの実装 2026-04-15 02:44:42 +09:00
203f188dae tuiの文字間隔修正・prompt設計の計画 2026-04-14 13:11:18 +09:00
57fb22ed94 scope再設計の完了 2026-04-14 12:10:00 +09:00
db02afb74f scopeの再設計 2026-04-14 12:09:18 +09:00
f8eabd3ac8 prune-savings-estimation完了 2026-04-14 03:42:04 +09:00
9eef8117c8 cargo fmt 2026-04-14 03:13:36 +09:00
be96efb5ed prune-projection完了 2026-04-14 02:57:25 +09:00
ff88fbc7e4 pruneのトークン計算置き換え・Podに接続 2026-04-14 02:35:35 +09:00
0a1d01d9b5 pruneで用いるトークン計算の改善 2026-04-14 00:15:09 +09:00
a89c4487c9 token-counter実装 2026-04-13 20:32:02 +09:00
13d83e0fd3 token-counter実装 2026-04-13 20:21:26 +09:00
d5e2c3819d usage永続化のdoc修正 2026-04-13 07:13:49 +09:00
101679dbb8 usageデータの永続化実装 2026-04-13 07:09:05 +09:00
313b5158b8 TODO・Ticketのアップデート 2026-04-13 05:58:33 +09:00
6f2362ec77 ToolsのTracker実装 2026-04-13 04:26:27 +09:00
a947922192 チケット更新 2026-04-13 04:10:19 +09:00
c850cdf2b6 組み込みツールの実装 2026-04-13 03:43:02 +09:00
029bb1fba7 Compactの実装 2026-04-13 02:08:25 +09:00
9747bd6d34 TUIをinline viewportに変更 2026-04-12 07:32:06 +09:00
48e62f65df compactの実装 2026-04-12 07:09:48 +09:00
f2aaa3683f TUIのratatuiを0.30.0にした 2026-04-12 06:57:07 +09:00
46526ed262 session-storeとして分離 2026-04-12 06:31:34 +09:00
be1119d859 Pruneの実装 2026-04-12 06:02:46 +09:00
afabd3d7fd TUI上のターンカウンタ・ターン統計の実装 2026-04-12 05:41:22 +09:00
0c9551eef0 Tool Outputの仕様簡素化 2026-04-12 05:19:00 +09:00
444c90d7e4 コンテキスト圧縮の設計更新 2026-04-12 04:47:42 +09:00
37e6301397 Pod切断時にTUIがハングする問題 2026-04-12 04:22:26 +09:00
601d93f8d0 history取得:TUI側の実装 2026-04-12 03:44:45 +09:00
57a8ad6b97 historyを返すプロトコル 2026-04-12 03:37:49 +09:00
1617a982e1 Tickets整理 2026-04-12 03:19:12 +09:00
26f9294f42 ツールの動的削除の実装 2026-04-11 20:01:55 +09:00
982e0d2dbb Workerのリファクタリング 2026-04-11 19:47:34 +09:00
61a977779e Podにキーを渡す実装 2026-04-11 19:28:59 +09:00
e1cf8fad0f Workerの自動キャッシュロック 2026-04-11 18:47:33 +09:00
3151fc27ef workerのAPI設計 2026-04-11 17:30:32 +09:00
748e858ec5 HookのPod側への移動・Interceptorの実装 2026-04-11 17:19:20 +09:00
b15a5ceffc プロトコルStreamのユーティリティ共通化 2026-04-11 15:58:52 +09:00
c936492a29 Sessionのハッシュ 2026-04-11 15:14:02 +09:00
71b3d550a4 Remove Pod-ID 2026-04-11 14:18:49 +09:00
88aa2cf953 llm-workerのAPI改善 2026-04-11 14:11:40 +09:00
1b8e2173bb Pod-ID (UUID)の削除 2026-04-11 03:44:37 +09:00
13ba83d45d Create remove-pod-id.md 2026-04-11 03:40:55 +09:00
b3d3fd524a Podのバイナリ実装 2026-04-11 03:26:38 +09:00
8aa058ef75 tickets 2026-04-11 03:23:48 +09:00
5af96aa191 Max Turnの実装 2026-04-11 03:16:36 +09:00
ee307e2926 Add README to all crates 2026-04-11 03:07:56 +09:00
8d18357460 cratesの整理 2026-04-11 02:48:50 +09:00
1d92680e76 プロトコルの定義 2026-04-09 05:23:57 +09:00
cc1ceee81d Crate設計・mv 2026-04-09 04:55:14 +09:00
133 changed files with 1118 additions and 4487 deletions

View 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
-

View File

@ -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

View File

@ -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 に必要な生成物を書けることを確認する。

View File

@ -32,7 +32,7 @@ workflowで明示されない限り、読み取り以外の操作は控えるこ
基本はworktree上の一時的なブランチでコミットを重ね、メインブランチに取り込む運用をしている。
コミットメッセージは適当に`<prefix>: *簡潔な1行*`で書いている。
外部の参考プロジェクトは必要に応じてローカルの外部 checkout からReadすること。
外部の参考プロジェクトはghqでgetしており、必要に応じて`~/ghq`からReadすること。
---

View File

@ -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/`ディレクトリに感じた障壁や改善案等を書き残す形にした。 明確に力不足な点/ツールの問題があった場合や、ユーザーからの指示があった際に作ること。

1
CLAUDE.md Symbolic link
View File

@ -0,0 +1 @@
AGENTS.md

29
Cargo.lock generated
View File

@ -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
View File

@ -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

View File

@ -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]

View File

@ -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 behaviorconversation header / request compression 等)
/// を切り替える。
fn is_codex_backend(&self) -> bool {
false
}
}

View File

@ -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();

View File

@ -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
// =============================================================================

View File

@ -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());
}
}

View File

@ -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");
}
}

View File

@ -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() {
// シンプルなテスト用構造体

View File

@ -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, &notice);
}
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, &notice);
}
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.
}

View File

@ -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

View File

@ -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();

View File

@ -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]

View File

@ -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"))]);

View File

@ -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]

View File

@ -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,

View File

@ -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();

View File

@ -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]

View File

@ -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,

View File

@ -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]

View File

@ -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`.

View File

@ -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> {

View File

@ -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"));
}
}

View File

@ -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]

View File

@ -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 }

View File

@ -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。

View File

@ -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"}}"#;

View File

@ -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,
}

View File

@ -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};

View File

@ -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();

View File

@ -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);

View File

@ -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.

View File

@ -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 本**: ルーター系は後追いで数を増やしやすい宣言型設計、実装コスト最小

View File

@ -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`

View File

@ -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`)は人間レビューに委ねる
## 関連

View 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 の真実性が一本化されて実装が単純になる

View File

@ -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と整合

View File

@ -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

View File

@ -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 も渡す必要があった。
## 障壁

View File

@ -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 時に混乱要因になり得る。

View File

@ -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.

View File

@ -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`

View File

@ -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,

View File

@ -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

View File

@ -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 "$@"

View File

@ -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
## 背景

View File

@ -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 テストハーネス
## 背景

View File

@ -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 化
## 背景

View 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`

View File

@ -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 導線
## 背景

View 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 の保存形式の大幅変更。

View 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 運用方針変更。

View File

@ -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 への整理
## 背景

View File

@ -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 ハンドリングの重複を統合する
## 背景

View File

@ -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複数ターン巻き戻し
## 背景

View File

@ -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`

View File

@ -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 }`

View File

@ -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

View File

@ -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
## 背景

View File

@ -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 の設計
## 背景

View File

@ -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 の表示優先と状態補完
## 背景

View File

@ -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
## 背景

View 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 新設

View File

@ -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
## 背景

View File

@ -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.

View File

@ -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 を自動生成する仕組み。

View File

@ -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 を自動生成する仕組み。

View File

@ -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 を自動生成する仕組み。
---

View File

@ -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

View File

@ -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

View File

@ -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
---

View File

@ -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.
---

View File

@ -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.
---

View File

@ -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.
---

View File

@ -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.
---

View File

@ -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 分離。

View File

@ -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.
---

View File

@ -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.
---

View File

@ -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.
---

View File

@ -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。

View File

@ -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.
---

View File

@ -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