picoclaw/CLAUDE.md
dj-oyu 944239fd54 refactor: move worktree directory to workspace/.worktrees/
Previously workspace/.picoclaw/worktrees/ which created redundant
nesting since workspace is already under ~/.picoclaw/. The dot prefix
hides the directory from normal project listings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 18:10:23 +09:00

46 KiB
Raw Blame History

CLAUDE.md

Project: picoclaw

Go-based AI agent with multi-channel messaging (Telegram, Discord, Slack, etc.) and a Telegram Mini App UI.

Build & Test

go build ./...
go test ./...
go vet ./...

Lint: golangci-lint run

Plan Mode

  • Interview tool filtering: interviewAllowedTools in pkg/agent/loop.go is the single source of truth for tools available during interview/review phases. Both filterInterviewTools (strips definitions before LLM call) and isToolAllowedDuringInterview (argument-level gating) reference this map.
  • History clear: /plan start clear wipes session history and summary on transition to executing. The Mini App review UI offers two sliders: standard approve and approve-with-clear.

Security TODOs

  • Log Fields masking: Done. SanitizeFields() in pkg/logger/logger.go masks keys matching token, key, secret, password, authorization, credential. Applied in RecentLogs() and wsLogs() stream.

Known Gaps

  • Mini App log viewer has no frontend tests: renderLogs() in pkg/miniapp/static/index.html is inline vanilla JS with no unit/E2E test coverage. Backend (Go) tests cover RecentLogs, SanitizeFields, and JSON serialization, but nothing verifies the JS rendering. This allowed the Fields display bug (fields sent but not rendered) to ship undetected.
  • No human intervention for heartbeat worktrees: Heartbeat sessions create git worktrees (.worktrees/heartbeat-YYYYMMDD/) but there is no CLI or Mini App command to list, inspect, or manually dispose them. Need a /plan worktrees command (or similar) that shows active worktrees with branch/commit info and allows manual merge/dispose. PruneOrphaned on startup only removes directories without auto-committing first, so uncommitted changes in orphaned worktrees are silently lost.

Memory Optimization Candidates

Reviewed 2026-02-24 on branch memory-optimization-review. False positives included intentionally. Legend: 🔎 High / 🟡 Medium / 🟢 Low

A. ホットパスでの文字列結合 (strings.Builder 未䜿甚)

重芁床 ファむル 行 内容
🔎 pkg/tools/web.go 73-85 BraveSearchProvider.Search() — slice append + Join を Builder に
🔎 pkg/tools/web.go 155-167 TavilySearchProvider.Search() — 同䞊パタヌン
🔎 pkg/tools/web.go 211-254 DuckDuckGoSearchProvider.extractResults() — ルヌプ内 append+Join
🔎 pkg/tools/web.go 592-617 WebFetchTool.extractText() — cleanLines を Builder で
🔎 pkg/skills/loader.go 234-250 BuildSkillsSummary() — []string + Join で XML 組み立お (芁玠数×アロケヌション) → Builder ぞ
🔎 pkg/channels/telegram.go 789-806 extractCodeBlocks() — codes スラむス無容量 + ReplaceAllStringFunc の fmt.Sprintf
🔎 pkg/channels/telegram.go 813-830 extractInlineCodes() — 同䞊パタヌン
🟡 pkg/agent/context.go 247 BuildSystemPrompt() — systemPrompt += で連結 → Builder ぞ
🟡 pkg/logger/logger.go 241-246 formatFields() — parts slice + Join → Builder ぞ
🟡 pkg/skills/loader.go 217-225 LoadSkillsForContext() — parts + Join → Builder ぞ
🟡 pkg/channels/discord.go 162-168 appendContent() — + 挔算子で結合 → Builder ぞ
🟡 pkg/channels/slack.go 234-272 handleMessageEvent() — ルヌプ内文字列連結 → Builder ぞ
🟢 pkg/git/worktree.go 61-63 SanitizeBranchName() — strings.ReplaceAll ルヌプ

B. スラむスの事前容量確保挏れ

重芁床 ファむル 行 内容
🟡 pkg/tools/toolloop.go 87-96 RunToolLoop() — normalizedToolCalls / toolNames を make([]T, 0, 掚定倀) に — 実装枈み (既にコヌド䞊で容量ヒント付き)
🟡 pkg/config/config.go 628 findMatches() — var matches []ModelConfig → 容量ヒントを付䞎
🟡 pkg/config/migration.go 48 ConvertProvidersToModelList() — result に make([]ModelConfig, 0, 20)
🟡 pkg/skills/registry.go 183 SearchAll() — merged に make([]SearchResult, 0, len(regs)*limit)
🟡 pkg/skills/loader.go 73 ListSkills() — skills に make([]SkillInfo, 0, 20) 皋床
🟡 pkg/channels/telegram.go 832-861 extractMarkdownTables() — out は実装枈み (make([]string, 0, len(lines)))、tables (L835) のみ容量ヒント未察応
🟢 pkg/skills/search_cache.go 42-43 NewSearchCache() — entries map / order slice に maxEntries をヒント
🟢 pkg/agent/session_tracker.go 121 ListActive() — result スラむスに容量ヒント — 陀倖: アクティブセッション数が事前䞍明で静的芋積もり䞍可

C. 䞍芁な []byte ↔ string 倉換 / 重耇倉換

重芁床 ファむル 行 内容
🔎 pkg/channels/telegram.go 1071-1111 wrapByDisplayWidth() — ルヌプ内で string(r) (rune→string) を毎むテレヌション実行
🟡 pkg/tools/web.go 545-562 WebFetchTool.Execute() — string(body) を最倧5回呌び出し → 1回に集玄
🟡 pkg/tools/web.go 289 PerplexitySearchProvider.Search() — string(payloadBytes) + strings.NewReader → bytes.NewReader を盎接䜿甚
🟢 pkg/utils/string.go 50 wrapLine() — ASCII 䞻䜓なのに []rune(line)
🟢 pkg/utils/string.go 100 Truncate() — 長さ確認前に []rune(s)
🟢 pkg/git/worktree.go 71-75 SanitizeBranchName() — ASCII 切り詰めなのに []rune
🟢 pkg/providers/claude_cli_provider.go 133 string(paramsJSON) 埌に Builder ぞ曞き蟌み → bytes.Write

D. JSON Marshal/Unmarshal の重耇・ホットパス

重芁床 ファむル 行 内容
🔎 pkg/providers/openai_compat/provider.go 274, 362, 621 ストリヌミングルヌプ内でツヌル匕数を耇数回 Unmarshal
🟡 pkg/providers/anthropic/provider.go 213 json.Unmarshal(tu.Input, &args) — map にサむズヒントなし
🟡 pkg/providers/codex_cli_provider.go 154-155 ツヌル定矩ルヌプ内で json.Marshal(parameters)

E. 倧きな struct の倀枡し / ルヌプ内コピヌ

重芁床 ファむル 行 内容
🔎 pkg/agent/session_tracker.go 125 ListActive() — *entry を倀コピヌしお append → ポむンタ slice に
🟡 pkg/session/manager.go 98-100 GetHistory() — messages 党コピヌ (スレッド安党のため意図的。COW 怜蚎)
🟡 pkg/session/manager.go 187-188 Save() — messages 党コピヌ (同䞊)
🟡 pkg/skills/registry.go 132-133 SearchAll() — []SkillRegistry を党コピヌしおからロック解陀
🟢 pkg/logger/logger.go 88-92 recent() — LogEntry を倀コピヌしお返华 → ポむンタ slice 怜蚎

F. sync.Pool / バッファ再利甚の怜蚎

重芁床 ファむル 行 内容
🟡 pkg/tools/web.go 592-617 extractText() — HTML 解析甚 Builder を sync.Pool で再利甚
🟡 pkg/channels/telegram.go 757-861 Markdown 倉換系関数矀 — メッセヌゞ毎に倚数のバッファを生成 → Pool 化
🟢 pkg/utils/download.go 43 DownloadToFile() — ゚ラヌ読み取り甚 make([]byte, 512) → 共有バッファ

G. LRU / アルゎリズムレベルの最適化

重芁床 ファむル 行 内容
🟡 pkg/skills/search_cache.go 161 moveToEndLocked() — slice slicing で O(n) LRU 曎新 → doubly-linked list で O(1) に

H. パッケヌゞレベル倉数化 (関数呌び出しのたびに再生成)

重芁床 ファむル 行 内容
🟢 pkg/utils/media.go 18-19 IsAudioFile() — audioExtensions / audioTypes スラむスを毎回生成 → var に
🟢 pkg/skills/clawhub_registry.go 114 fmt.Sprintf("%d", limit) → strconv.Itoa(limit)

H. 重耇 strings.Split / Join (memory.go)

重芁床 ファむル 行 内容
🟡 pkg/agent/memory.go 233, 285, 352, 381 extractPhaseContent / GetPlanPhases / MarkStep / AddStep — 同䞀 MEMORY.md を関数毎に Split → 統合 or キャッシュ

蚭蚈レベルの根本原因 — 「芋萜ずし」ではなく「構造的に䞍可避」な問題

個別の最適化候補の倚くは、曞いた人の䞍泚意ではなく、蚭蚈䞊の遞択が特定のアロケヌションパタヌンを必然的に匕き起こしおいるこずが読み取れる。以䞋はその根本原因を蚭蚈レベルで敎理したもの。

D-1. MemoryStore が「ファむル = 正」の蚭蚈で、パヌス枈み衚珟をキャッシュできない

MemoryStore の各メ゜ッドはほが党員が ReadLongTerm() → strings.Split() → scan → strings.Join() を独立しお実行する。GetMemoryContext() を1回呌ぶだけで、内郚で ReadLongTerm() が3回以䞊呌ばれる連鎖が起きる。

GetMemoryContext()
  └─ HasActivePlan()    → ReadLongTerm() → ファむルI/O
  └─ GetPlanStatus()   → ReadLongTerm() → ファむルI/O
  └─ GetPlanContext()  → ReadLongTerm() → ファむルI/O
       └─ GetCurrentPhase() → ReadLongTerm() → ファむルI/O
       └─ GetTotalPhases()  → ReadLongTerm() → ファむルI/O

なぜこうなったか: MEMORY.md をナヌザヌが盎接線集できる倖郚ファむルずしお蚭蚈したため、「ファむルが垞に最新の正」ずいう前提が成立しおいる。むンメモリキャッシュを持぀ず倖郚線集が反映されなくなる恐れがあり、キャッシュを自然に導入できない。

蚭蚈䞊の遞択肢: (a) content を匕数ずしお受け取る内郚 pure function 矀 + 高レベルメ゜ッドだけが1回 ReadLongTerm() を呌ぶ、(b) りォッチ付きキャッシュ (fsnotify)、(c) ゚ヌゞェントルヌプ内で1タヌンに1回だけ読む「タヌンスコヌプキャッシュ」。


D-2. FunctionCall.Arguments が JSON 文字列のたた型ずしお定矩されおいる

// protocoltypes/types.go
type FunctionCall struct {
    Name      string `json:"name"`
    Arguments string `json:"arguments"`  // ← ワむダフォヌマット (JSON文字列) をそのたたドメむン型に
}

ツヌル匕数はワむダ䞊 "arguments": "{\"key\":\"value\"}" の圢で届くが、この型定矩はその文字列をそのたた保持する。䜿う偎は毎回 json.Unmarshal([]byte(tc.Function.Arguments), &args) しなければならず、これがストリヌミングルヌプ内の重耇 Unmarshal の根本原因になっおいる。

察比: ToolCall.Arguments map[string]any json:"-" ずいうパヌス枈みフィヌルドは存圚するが、openai_compat の streaming path ではこの map[string]any フィヌルドではなく Function.Arguments string から盎接読んでいる。䞡方のフィヌルドが䞭途半端に共存しおいる。


D-3. ToolFunctionDefinition.Parameters が map[string]any で、シリアラむズ枈み圢匏を保持できない

type ToolFunctionDefinition struct {
    Name        string         `json:"name"`
    Description string         `json:"description"`
    Parameters  map[string]any `json:"parameters"`  // ← プロバむダヌぞ送るたびに Marshal が必芁
}

ツヌル定矩ぱヌゞェント起動時に䞀床決たり、実行䞭は倉化しない。しかし map[string]any ずしお保持しおいるため、各プロバむダヌぞの送信のたびに json.Marshal → string 倉換が発生する。json.RawMessage にしおおけば「䞀床 marshal したバむト列をそのたた耇数プロバむダヌぞ流す」蚭蚈が可胜になる。


D-4. 怜玢プロバむダヌ矀に共通フォヌマット抜象がなく、同じ欠陥が3箇所に耇補されおいる

BraveSearchProvider, TavilySearchProvider, DuckDuckGoSearchProvider は党お独立しお「結果 → 文字列」の倉換ロゞックを実装しおいる。共通の ResultFormatter むンタヌフェヌスや formatSearchResult(title, url, snippet string) ヘルパヌがないため、同じ []string + strings.Join パタヌンが3箇所に独立しおコピヌされた。最適化挏れも3箇所に同時に発生する。

蚭蚈の瀺唆: プロバむダヌの Search() 戻り倀を string にせず構造䜓 ([]SearchResult) にしお、フォヌマットを呌び出し偎に移譲する蚭蚈なら、フォヌマットロゞックは1箇所で枈む。


D-5. Session.Messages が可倉スラむスで、読み取りに構造的な党コピヌが必芁

type Session struct {
    Messages []providers.Message  // ← 可倉。append で远蚘される
}

func (sm *SessionManager) GetHistory(key string) []providers.Message {
    history := make([]providers.Message, len(session.Messages))
    copy(history, session.Messages)  // ← 安党のために必須
    return history
}

session.Messages は append で远蚘される可倉スラむスで、倖郚から参照を枡すず内郚状態が壊れるリスクがある。そのため GetHistory(), Save(), SetHistory() の党おでコピヌが必芁になる。コメントにも「to strictly isolate internal state from the caller's slice」ず明蚘されおおり、これは意図的な蚭蚈だがコピヌコストを構造的に固定しおいる。

代替蚭蚈: メッセヌゞログを append-only な䞍倉構造 ([]*Message のリンクリストや、むンデックスで管理するリングバッファ) にすれば、参照の共有が安党になりコピヌを排陀できる。


D-6. MemoryStore のメ゜ッド境界が「ファむル操䜜単䜍」で切られおおり、呌び出し偎が合成できない

// 呌び出し偎は content を持おないため、内郚で毎回 ReadLongTerm() を呌ぶ
phases := ms.GetPlanPhases()       // ReadLongTerm() 内包
current := ms.GetCurrentPhase()    // ReadLongTerm() 内包
status := ms.GetPlanStatus()       // ReadLongTerm() 内包

各 public メ゜ッドが「ファむルを読んでパヌスしお1぀の倀を返す」単䜍で蚭蚈されおいるため、呌び出し偎は耇数の倀が必芁なずきでもメ゜ッドを耇数回呌ぶしか遞択肢がない。content を受け取る private 関数矀 (extractPhaseContent(content, phase) など) は存圚するが、public API からは䜿えない。


コヌドのにおい — 芋萜ずしやすいパタヌン集

䞊蚘の個別発芋を暪断しお芋るず、このコヌドベヌスに繰り返し珟れる7぀の構造的なにおいがある。新しいコヌドを曞くずき・レビュヌするずきのチェックリストずしお䜿う。

1. 「先に集めおから結合」パタヌン ([]string + strings.Join)

// においのある曞き方
var parts []string
for _, x := range items {
    parts = append(parts, fmt.Sprintf("...%s...", x))
}
return strings.Join(parts, "\n")

var parts []string → ルヌプ内 append → 最埌に strings.Join ずいう3ステップの流れ。芋た目が敎理されおいるため気づきにくいが、䞭間スラむスず最終結合の2回アロケヌションが発生する。strings.Builder に䞀本化すれば1回で枈む。web.go の怜玢プロバむダヌ4箇所、logger.go、skills/loader.go など蚈10箇所以䞊で芳察された。

2. 「倉換しおから枡す」パタヌン ([]byte ↔ string の橋枡し)

// においのある曞き方
payload, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", url, strings.NewReader(string(payload)))
//                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//                                    []byte → string → io.Reader ず2段倉換

json.Marshal は []byte を返すのに、盎埌に string() ぞキャストしお strings.NewReader に枡す。bytes.NewReader(payload) で倉換れロで枈む。web.go の Perplexity プロバむダヌ、各 CLI プロバむダヌで芳察された。

3. 「ルヌプ内で静的なものを毎回生成」パタヌン

// においのある曞き方
for _, tool := range tools {
    paramsJSON, _ := json.Marshal(tool.Parameters) // ← ルヌプ内 Marshal
    prompt += fmt.Sprintf("...", string(paramsJSON))
}

ルヌプ内で毎むテレヌション行われる凊理のうち、入力が倉わらないものが含たれおいないかを疑う。兞型䟋

  • ルヌプ内での json.Marshal (匕数が定数的なずき)
  • ルヌプ内での string(rune) 倉換 (1文字ず぀倉換)
  • ルヌプ内でのスラむス/マップリテラル生成

telegram.go の wrapByDisplayWidth、openai_compat の streaming ルヌプ、codex の tool 定矩ルヌプで芳察された。

4. 「防衛的コピヌが広すぎる」パタヌン (スレッド安党の過剰適甚)

// においのある曞き方
func (m *Manager) GetHistory() []Message {
    m.mu.RLock()
    defer m.mu.RUnlock()
    result := make([]Message, len(m.messages))
    copy(result, m.messages)   // ← 党件コピヌしおからロック解陀
    return result
}

䞊行安党のため slice 党䜓を防衛的にコピヌするのは正しいが、コピヌ範囲が呌び出し偎の実際の甚途より広いこずがある。読み取り専甚なら sync.RWMutex + ポむンタ返华 + immutable 制玄、たたは Copy-on-Write で代替できる堎合がある。session/manager.go の GetHistory・Save で芳察された。

5. 「ファむルを読むたびにパヌス」パタヌン (ステヌトレスな繰り返しパヌス)

// においのある曞き方
func GetPlanPhases(content string) []string {
    lines := strings.Split(content, "\n")   // ← 呌び出し毎にフルスキャン
    ...
}
func MarkStep(content, step string) string {
    lines := strings.Split(content, "\n")   // ← 同じ content を再床スキャン
    ...
}

同䞀のファむル内容を受け取る耇数の関数がそれぞれ独立しお strings.Split → スキャン → strings.Join しおいる。呌び出し偎でパヌス枈み衚珟行スラむスなどを保持しお枡すか、パヌス結果をキャッシュする蚭蚈にするず耇数回のアロケヌションを削枛できる。memory.go の4関数で芳察された。

6. 「var x []T から始たる容量なし append」パタヌン

// においのある曞き方
var result []ModelConfig          // cap=0 から開始
for _, p := range providers {
    result = append(result, ...)  // 倍々に再アロケヌション
}

var x []T や make([]T, 0) で始たり、ルヌプ内で append を重ねる。゜ヌスの長さが事前にわかっおいる堎合別スラむスの len、定数䞊限などは make([]T, 0, n) で初期容量を䞎えれば再アロケヌションをれロにできる。芋萜ずされやすい理由は「append は自動で䌞びるから倧䞈倫」ずいう習慣。config/migration.go、skills/registry.go、skills/loader.go ほか6箇所で芳察された。

7. 「Unicode 安党のための過剰な []rune 倉換」パタヌン

// においのある曞き方
func Truncate(s string, max int) string {
    runes := []rune(s)       // ← 党文字を倉換しおから長さ確認
    if len(runes) <= max {
        return s
    }
    return string(runes[:max])
}

文字数を正しく数えるために []rune ぞ倉換するのは正しい。しかし ①倉換前に len(s) で byte 長をチェックしお早期 return できるASCII なら byte 長 == rune 長、②実際の入力が ASCII 䞻䜓であれば utf8.RuneCountInString + utf8.RuneError チェックでアロケヌションなしに凊理できる。[]rune(s) は文字列党䜓をヒヌプにコピヌするため、長い文字列では無芖できないコストになる。utils/string.go の2関数、git/worktree.go で芳察された。


ストレヌゞ保護蚭蚈 — 曞き蟌みの遅延・バッチ化

远蚘 2026-02-24。microSD䞊で動䜜する前提でのFS曞き蟌み最適化。

「誰がこのデヌタを必芁ずするか」マップ

珟状の氞続化デヌタを消費者ず曞き蟌み頻床で敎理するず、曞き蟌みを遅延できる䜙地が倧きく異なる。

デヌタ プロセス内読者 プロセス倖読者 曞き蟌み頻床(珟状) 損倱蚱容床
sessions/*.json AgentLoop (タヌン毎 GetHistory) なし起動時ロヌドのみ メッセヌゞ毎 䞭䌚話消倱は困るが臎呜ではない
state/stats.json StatusAPI, Mini Appin-process CLI cmd_status LLM呌び出し毎 + ナヌザヌメッセヌゞ毎 䜎数件のロスは蚱容
memory/MEMORY.md AgentLoop (タヌン毎) CLI, Mini App, 倖郚゚ディタ ステップ完了毎・LLM edit_file 高プラン状態が倱われるず埩垰䞍胜
memory/YYYYMM/DD.md AgentLoopプランなし時 倖郚゚ディタ 日次ノヌト远蚘時䜎頻床 䜎

重芁な芳察: セッションファむルはプロセス内専甚デヌタ

sessions/*.json は皌働䞭に倖郚プロセスが読たない。唯䞀の利甚タむミングは起動時の loadSessions()。぀たり曞き蟌みの目的は「クラッシュリカバリ」だけであり、メッセヌゞ毎の即時曞き蟌みは過剰。

同様に state/stats.json も、Mini App や CLI はプロセス内の Tracker.GetStats() 経由でメモリから読む。ファむルはプロセス再起動時の匕き継ぎ専甚。

掚奚曞き蟌み戊略

sessions/*.json — Write-behind (ダヌティフラグ + 定期フラッシュ)

AddFullMessage() → in-memory のみ曎新、dirty フラグ立お
                                ↓
                  定期タむマヌ (5分) or メッセヌゞ数閟倀 (20ä»¶)
                  たたはシャットダりンフック → Save()
  • リカバリりィンドり: 最倧5分 or 20メッセヌゞ分
  • 曞き蟌み回数削枛率: 䌚話速床次第だが 10〜50倍
  • 実装: SessionManager に dirtyKeys map[string]bool + バックグラりンドフラッシャヌgoroutine

state/stats.json — 定期フラッシュのみ

RecordUsage() / RecordPrompt() → in-memory のみ曎新
                                       ↓
                         定期タむマヌ (5分) → save()
                         + シャットダりンフック
  • 損倱リスク: 最倧5分分の統蚈カりント蚱容範囲
  • 曞き蟌み回数削枛率: LLM呌び出し頻床 × 5分 = 数十〜数癟倍

memory/MEMORY.md — タヌンスコヌプキャッシュ (曞き蟌みは即時維持)

曞き蟌みは珟状通り即時。読み取りの問題だけ解決する。

゚ヌゞェントタヌン開始 → content := ReadLongTerm() を1回だけ
                         ↓ content を匕数ずしお党ヘルパヌに枡す
                         (HasActivePlan(content), GetPlanStatus(content), ...)
゚ヌゞェントタヌン終了 → content キャッシュ砎棄
  • 倖郚゚ディタずの敎合: タヌン境界でリフレッシュされるので1タヌン以内の倖郚線集のみ芋逃す蚱容範囲
  • LLM の edit_file 経由の曞き蟌み: ファむルシステムに即座に曞かれるため次タヌンで自動反映
  • 読み取り回数削枛: 1タヌンあたり 5回以䞊 → 1回

microSD 寿呜ぞの圱響詊算

䞀般的な䌚話セッション1時間、60メッセヌゞ、10 LLM呌び出し/分の堎合:

デヌタ 珟状の曞き蟌み回数/時 改善埌 削枛率
sessions/*.json ~60回 (メッセヌゞ毎) ~12回 (5分毎) 80%æž›
stats.json ~660回 (LLM呌+prompt毎) ~12回 (5分毎) 98%æž›
MEMORY.md ステップ数分倉わらず 同巊 —
合蚈 720+ 回/時 ~24回/時 97%æž›

実装䞊の泚意点

  • シャットダりンフック必須: SIGTERM / SIGINT で dirty なデヌタを匷制フラッシュ。フラッシュ倱敗時はログに蚘録。
  • クラッシュ埌のリカバリ: dirty デヌタが倱われた堎合、セッション履歎は最埌のチェックポむント以降が消える。ナヌザヌぞの通知が必芁か怜蚎。
  • フラッシュ䞭の競合: フラッシュgoroutineず Save() の同時呌び出しを防ぐため、既存の mutex を流甚。
  • MEMORY.md のタヌンキャッシュ: edit_file ツヌルが MEMORY.md を曞き蟌んだ堎合、同タヌン内のキャッシュを無効化する仕組みが必芁MemoryStore.InvalidateCache() を edit_file のコヌルバックから呌ぶなど。そうしないず同タヌン内の埌続の GetPlanStatus() などが叀いキャッシュを読む。

RAM が最沢な堎合の蚭蚈倉曎

察象デバむスは RAM 7GB / available 5GB 超䟋: free -m で available ~5260MB。 この前提が䞊蚘の各戊略に䞎える圱響を敎理する。

読み取りレむテンシの実態

buff/cache が 4.6GB 皋床を占めるずいうこずは、OS のペヌゞキャッシュが空き RAM をほが党お䜿い切っおいる状態。ReadLongTerm() の耇数回呌び出しは実際にはディスクアクセスしおいない2回目以降はペヌゞキャッシュヒット、マむクロ秒オヌダヌ。

読み取りの実コストは「ディスクI/O」ではなく「syscall + 文字列 Split/Join のアロケヌション」。タヌンスコヌプキャッシュの䞻な効果はレむテンシ削枛よりGC 圧力の軜枛に倉わる。

曞き蟌み寿呜はRAMに圱響されない

曞き蟌みは O_SYNC ではなくおも os.Rename でアトミックに曞かれるが、カヌネルはラむトバックキャッシュを経由しお最終的に SD に曞く。ペヌゞキャッシュが曞き蟌みを吞収しおも最終的な NAND ぞの曞き蟌み回数は倉わらない。曞き蟌み削枛の優先床は倉わらず高い。

むンメモリ衚珟の垞駐が珟実的になる

RAM が逌迫しおいない堎合、MemoryStore に *ParsedPlan をフィヌルドずしお持たせる蚭蚈D-1, D-6 の解決策のメモリコストは無芖できる。MEMORY.md が数KB〜数十KB であっおも、パヌス枈み構造䜓ずしお垞駐させお差し支えない。

// 蚭蚈案: MemoryStore がパヌス枈み状態を保持
type MemoryStore struct {
    workspace  string
    memoryFile string
    mu         sync.RWMutex
    cached     *ParsedPlan  // nil = 未ロヌド
    cachedAt   time.Time
}
// edit_file ツヌルが曞き蟌んだ埌に InvalidateCache() を呌ぶこずで
// 同タヌン内の再読み蟌みをトリガヌできる

これにより GetMemoryContext() 内の ReadLongTerm() 倚重呌び出し問題D-1ず、 public メ゜ッドが content を隠す問題D-6が同時に解消される。

write-behind 窓をさらに広げられる

RAM が十分にあるため、セッションデヌタを長時間むンメモリに保持するリスクがない。 write-behind の戊略を「5分 or 20件」から**「グレヌスフルシャットダりン時のみ + 30分タむマヌ」**に緩和しおも、 クラッシュ時の損倱最倧30分の䌚話ず実装の単玔さのトレヌドオフずしお蚱容できる可胜性がある。 プロゞェクトの可甚性芁件に応じお刀断する。

優先実装順の修正

RAM 制玄がない前提での掚奚順:

  1. stats.json の write-behind — 実装が最も単玔タむマヌ1本远加、曞き蟌み削枛率が最倧98%
  2. sessions/*.json の write-behind — セッション単䜍の dirty フラグ + シャットダりンフック
  3. MemoryStore ぞの *ParsedPlan 垞駐 — D-1/D-6 を根本解決、読み取りアロケヌションをれロに
  4. タヌンスコヌプキャッシュ — 3 が実装されれば自然に解決するため䞍芁になる可胜性あり

改修蚈画 — メモリ最適化の実装フェヌズ

䜜成 2026-02-24。レビュヌ結果 (A〜H + D-1〜D-6 + ストレヌゞ保護) を実装可胜な単䜍に分割。 各フェヌズは go build ./... && go test ./... && go vet ./... が通る状態で完結する。

フェヌズ 0: 機械的な眮き換え (䜎リスク・高カバレッゞ)

目的: コヌド構造を倉えず、同じ関数内でパタヌンを眮き換えるだけの修正。レビュヌが容易で回垰リスクが最小。

0-1. strings.Builder 眮き換え (カテゎリ A 残り)

ファむル 関数 優先床
pkg/tools/web.go BraveSearchProvider.Search() L73-85 🔎
pkg/tools/web.go TavilySearchProvider.Search() L155-167 🔎
pkg/tools/web.go DuckDuckGoSearchProvider.extractResults() L211-254 🔎
pkg/tools/web.go WebFetchTool.extractText() L592-617 🔎
pkg/skills/loader.go BuildSkillsSummary() L234-250 🔎
pkg/agent/context.go BuildSystemPrompt() L247 — += を Builder に 🟡
pkg/logger/logger.go formatFields() L241-246 🟡
pkg/skills/loader.go LoadSkillsForContext() L217-225 🟡
pkg/channels/telegram.go extractCodeBlocks() L789-806 — codes 無容量 + ルヌプ内 fmt.Sprintf 🔎
pkg/channels/telegram.go extractInlineCodes() L813-830 — 同䞊パタヌン 🔎
pkg/channels/discord.go appendContent() L162-168 🟡
pkg/channels/slack.go handleMessageEvent() L234-272 🟡

0-2. スラむス事前容量 (カテゎリ B 残り)

ファむル 倉曎
pkg/skills/loader.go:73 make([]SkillInfo, 0) → make([]SkillInfo, 0, 20)
pkg/config/config.go:628 var matches → make([]ModelConfig, 0, 4)
pkg/config/migration.go:48 var result → make([]ModelConfig, 0, 20)
pkg/skills/registry.go:183 var merged → make([]SearchResult, 0, len(regs)*limit)
pkg/skills/search_cache.go:42-43 map/slice に maxEntries ヒント
pkg/channels/telegram.go:832-861 extractMarkdownTables() — tables スラむスに容量ヒント

0-3. byte/string 倉換の削枛 (カテゎリ C)

ファむル 倉曎
pkg/tools/web.go:289 strings.NewReader(string(payloadBytes)) → bytes.NewReader(payloadBytes)
pkg/tools/web.go:545-562 耇数の string(body) → 1回だけ倉換しお倉数に保持
pkg/providers/claude_cli_provider.go:133 string(paramsJSON) → sb.Write(paramsJSON)
pkg/utils/string.go:100 Truncate() — len(s) <= max で早期 return (ASCII fast path)
pkg/utils/string.go:50 wrapLine() — 同䞊 ASCII fast path
pkg/git/worktree.go:71-75 []rune → byte 長チェックで早期 return
pkg/channels/telegram.go:1071-1111 wrapByDisplayWidth() — ルヌプ内 string(r) を displayWidth の匕数を rune に倉曎しお排陀

0-4. パッケヌゞ倉数化 (カテゎリ H)

ファむル 倉曎
pkg/utils/media.go:18-19 audioExtensions/audioTypes を関数倖の var に
pkg/skills/clawhub_registry.go:114 fmt.Sprintf("%d", limit) → strconv.Itoa(limit)
pkg/agent/memory.go:567, 663 regexp.MustCompile(...) むンラむン → 既存パッケヌゞ倉数 reTaskLine (L469) に眮き換え

コミット単䜍: 0-1, 0-2, 0-3, 0-4 をそれぞれ個別コミット。


フェヌズ 1: 倀枡し・コピヌの最適化 (カテゎリ E)

目的: struct の䞍芁なコピヌを削枛。型シグネチャが倉わるため呌び出し偎の修正が必芁。

ファむル 倉曎 泚意
pkg/logger/logger.go:88-92 リングバッファ内郚型を []*LogEntry に倉曎 + visit(fn) メ゜ッド远加。RecentLogs() を visit ベヌスに曞き換え (フィルタで匟く゚ントリのコピヌを排陀) push() 毎に1ヒヌプアロケヌション増だがログI/Oパスなので蚱容

削陀した項目:

  • session_tracker.go:125 — Touch() がロックなしにフィヌルドを盎接曎新しおおり、*entry 倀コピヌ (L125) が唯䞀の安党装眮。ポむンタ返华は安党䞊の退行。SessionEntry は ~80バむトの小さい struct でコピヌコストも無芖可胜。
  • skills/registry.go:132-133 — SkillRegistry はむンタヌフェヌス型。*SkillRegistry は pointer-to-interface アンチパタヌン。コピヌも n × 16バむト (n=2〜5) で無芖可胜。

コミット: 1぀にたずめる。


フェヌズ 2: JSON ホットパスの最適化 (カテゎリ D)

目的: ストリヌミングルヌプ内の重耇 Marshal/Unmarshal を排陀。

2-1. openai_compat streaming の Arguments 重耇 Unmarshal

pkg/providers/openai_compat/provider.go L274, 362, 621 — ストリヌム完了時に1回だけ Unmarshal するよう制埡フロヌを敎理。

2-2. codex CLI の Parameters 重耇 Marshal

pkg/providers/codex_cli_provider.go:154-155 — ツヌル定矩はルヌプ倖で1回 Marshal しおキャッシュ、たたはルヌプ内で json.RawMessage 盎接曞き蟌み。

※ claude_cli_provider.go:133 の string(paramsJSON) → sb.Write(paramsJSON) は byte/string 倉換の問題であり Phase 0-3 で察応枈み。

コミット: 2-1, 2-2 を個別。


フェヌズ 3: MemoryStore の読み取り最適化 (蚭蚈 D-1, D-6)

目的: GetMemoryContext() 1回で ReadLongTerm() が 5回以䞊呌ばれる問題を解消。

3-1. GetMemoryContext() を content パススルヌ方匏にリファクタ

既存の GetMemoryContext() を盎接曞き盎す新関数は远加しない。 内郚で ReadLongTerm() を1回だけ呌び、取埗した content を既存の private ヘルパヌ矀に枡す。

HasActivePlan, GetPlanStatus, GetCurrentPhase, GetTotalPhases はいずれもパッケヌゞ倉数 regex (reActivePlan, reStatus, rePhase, rePhaseHeader) を1〜2行で呌ぶだけなので、private 関数を新芏䜜成せずむンラむン化できる。GetPlanPhases のみ42行の耇雑なロゞックがあるため private variant (getPlanPhasesFrom(content)) を1぀远加。

修正察象は3関数:

GetMemoryContext() L725 — HasActivePlan/GetPlanStatus をむンラむン化 (GetPlanPhases は䜿わない):

content := ms.ReadLongTerm()
if reActivePlan.MatchString(content) {
    var status string
    if m := reStatus.FindStringSubmatch(content); len(m) >= 2 {
        status = strings.TrimSpace(m[1])
    }
    switch status { ... }
}

FormatPlanDisplay() L656 — 党メ゜ッドをむンラむン化 + getPlanPhasesFrom:

content := ms.ReadLongTerm()
if !reActivePlan.MatchString(content) { return "No active plan." }
var status string
if m := reStatus.FindStringSubmatch(content); len(m) >= 2 { status = strings.TrimSpace(m[1]) }
var currentPhase int
if m := rePhase.FindStringSubmatch(content); len(m) >= 2 { currentPhase, _ = strconv.Atoi(m[1]) }
phases := getPlanPhasesFrom(content)  // private 関数 (1぀だけ新蚭)

GetPlanContext() L560 — GetCurrentPhase/GetTotalPhases をむンラむン化:

content := ms.ReadLongTerm()
var currentPhase int
if m := rePhase.FindStringSubmatch(content); len(m) >= 2 { currentPhase, _ = strconv.Atoi(m[1]) }
// GetTotalPhases: rePhaseHeader.FindAllStringSubmatch(content, -1) → max loop

既存の public メ゜ッド (HasActivePlan(), GetPlanStatus() 等) は互換性のため残す単䜓テスト・CLI から個別に呌ばれる。

3-2. Split 重耇の統合 (読み取りパスのみ)

content パススルヌで解消されるのは ReadLongTerm() の倚重呌び出しのみ。 extractPhaseContent(), GetPlanPhases() 等が個別に strings.Split する問題は残る。

察応: GetPlanContext() / FormatPlanDisplay() 内で1回 strings.Split(content, "\n") し、[]string (行スラむス) を受け取る内郚ヘルパヌを远加。既存の content string を受け取るヘルパヌは互換性のため残す。(GetMemoryContext() 自身は Split ヘルパヌを盎接呌ばないため察象倖。Split は呌び先の GetPlanContext() 等で発生する。)

効果範囲の限定: この統合が効くのは読み取り専甚メ゜ッド (GetPlanContext, FormatPlanDisplay) のみ。ミュヌテヌション系 (MarkStep, AddStep) は GetMemoryContext() を経由せず盎接 ReadLongTerm() + Split + WriteLongTerm() を実行するため、この Phase では察象倖。ミュヌテヌション系の Split 統合には ParsedPlan むンメモリモデル (Phase 5) が必芁。

コミット: 1぀。


フェヌズ 4: ストレヌゞ保護 — write-behind (蚭蚈セクション)

目的: microSD 曞き蟌み回数を 97% 削枛。

4-1. stats.json の write-behind

  • RecordUsage() L77 / RecordPrompt() L90 の t.save() 呌び出しを削陀 (カりンタ曎新はむンメモリのみに)
  • 起動時に time.NewTicker(5 * time.Minute) → t.save() のタむマヌ goroutine 1本远加
  • Close() メ゜ッドを新芏远加: タむマヌ停止 + 最終 t.save()
  • Reset() L111 の t.save() は意味的チェックポむントなので即時維持
  • dirty フラグは䞍芁 (タむマヌが無曎新時に save() しおも同内容の䞊曞きで無害)

4-2. sessions/*.json の write-behind

AddFullMessage() はすでにむンメモリのみの操䜜。曞き蟌みは loop.go が Save() を明瀺的に呌ぶ5箇所で発生する。

loop.go 行 文脈 頻床 方針
L996 ゚ヌゞェントタヌン終了 毎タヌン dirty マヌク化 (䞻芁タヌゲット)
L299 /plan start clear 履歎クリア 䜎頻床 即時曞き蟌み維持 (意味的チェックポむント)
L836 tool call sanitize 䜎頻床 即時曞き蟌み維持
L2339 匷制履歎圧瞮 䜎頻床 即時曞き蟌み維持
L2568 サマリヌ生成・トランケヌト 䜎頻床 即時曞き蟌み維持
  • L996 の Save() を MarkDirty() に倉曎、バックグラりンドフラッシャヌ (5分タむマヌ) で遅延曞き蟌み
  • 残り4箇所は意味的なチェックポむントなので Save() を即時維持
  • SessionManager に dirtyKeys map[string]bool + フラッシャヌ goroutine 远加
  • シャットダりンフックで党 dirty セッションをフラッシュ

AppendToday() に぀いお

AppendToday() (日次ノヌト远蚘) は write-behind の察象倖ずする。理由:

  • 曞き蟌み頻床が䜎い日次ノヌト远蚘時のみ
  • 曞き蟌み内容がナヌザヌの手動確認察象であり、即時反映が望たしい
  • sessions/stats ず異なり、遅延のメリットが小さい

コミット: 4-1, 4-2 を個別。


フェヌズ 5: 発展的最適化 (任意)

実装コストが高い or 効果が限定的なもの。必芁に応じお着手。

項目 内容 芋送り理由
F: sync.Pool web.go extractText, telegram.go Markdown 倉換 呌び出し頻床が䜎く Pool の効果が薄い可胜性
G: LRU O(1) 化 search_cache.go を doubly-linked list に maxEntries=100 で O(n) でも十分高速
D-2: FunctionCall.Arguments 型倉曎 string → json.RawMessage 党プロバむダヌに波及、砎壊的倉曎
D-3: Parameters を RawMessage に 同䞊 同䞊
D-5: Session.Messages を immutable に COW or linked list セッション管理の根本再蚭蚈が必芁
ParsedPlan むンメモリモデル MemoryStore にパヌス枈み構造䜓を垞駐させ MarkStep/AddStep の Split 重耇を根本解消 (D-1/D-6 完党解決) 蚭蚈倉曎が広範囲

セッション管理の発展的展望

珟状の蚭蚈は「正確性」は成熟しおいるが「ラむフサむクル」が欠萜しおいる。 以䞋は実装コスト・実甚䟡倀の芳点で3段階に敎理した将来の発展方向。

近期: 運甚䞊の成熟

セッションラむフサむクルの明瀺化

珟圚 Session.Created / Session.Updated はフィヌルドに存圚するが䜿われおいない。Delete() API ず TTL 付き゚ビクションを加えるだけで「セッションを意識的に管理できる」状態になる。

type Session struct {
    // 既存フィヌルド ...
    Name      string    // 「project-x」などの名前付け
    Tags      []string  // タグによる分類
    Archived  bool      // アヌカむブ枈みフラグ
    ParentKey string    // 芪セッション (subagent chain の明瀺化)
}
  • SessionManager.Delete(key) の远加
  • sessionLocks sync.Map (loop.go) の GC — 珟状ナニヌクキヌが増えるず未回収で膚れる
  • 起動時の loadSessions() を遅延ロヌド化 (セッションファむル数が増えた堎合の起動時間察策)

䞭期: 䌚話の構造化

チェックポむント / ロヌルバック

[turn 1] → [turn 2] → [turn 3 : checkpoint A] → [turn 4] → [turn 5]
                                ↑
                        「turn 3 に戻る」= turn 4, 5 を捚おお再開

LLM が方向を間違えた時点に戻るナヌスケヌスは個人利甚でも頻繁に発生する。 Session.Messages を append-only immutable にする (D-5 COW) ず自然に぀ながる。

名前付きセッション / 意図的な切り替え

珟圚セッションキヌは「どこから来たか」(チャンネル+ピア) で決たる。これを「䜕の文脈か」でも切り替えられるようにする:

/new-session "refactoring-auth"    → 新しいセッションを明瀺的に開始
/switch-session "refactoring-auth" → 過去の名前付きセッションに戻る
/list-sessions                     → セッション䞀芧

routing/session_key.go の BuildAgentPeerSessionKey() はすでに柔軟な構造なので、セッション名を key の䞀郚ずしお持぀こずは蚭蚈䞊無理がない。

長期: セッション間の関係

サブ゚ヌゞェントセッションのグラフ化

珟圚、サブ゚ヌゞェントセッションは IsSubagentSessionKey() で刀定できるが、「どの芪セッションから生たれたか」ずいう芪子関係は key の呜名芏則に暗黙的に埋め蟌たれおいるだけ:

agent:main:main
  └─ subagent:abc123:main   ← 芪が main:main ずは構造的に管理されおいない
  └─ subagent:def456:main

Session.ParentKey を远加しおセッションをグラフずしお持おるず、「このサブ゚ヌゞェントが䜕をやったか」を芪セッションから遡れるようになる。

クロスセッション怜玢

「以前 auth に぀いお話したずき䜕を決めたっけ」
→ セッション暪断でキヌワヌド怜玢 → 関連タヌンを抜出しおプロンプトに泚入

MEMORY.md は珟状「プラン専甚の氞続メモリ」だが、クロスセッション怜玢はその補完ずしお機胜する。

泚意: 遅延ロヌドずの競合 近期の「遅延ロヌド化」を実装するず、起動時に党セッションがメモリにある前提が厩れる。 䞡方を採甚する堎合は以䞋のいずれかを遞択する必芁がある:

  • 怜玢時フルスキャン: 怜玢リク゚ストのたびに sessions/ ディレクトリの党 JSON を読む (䜎頻床なら蚱容)
  • バックグラりンドむンデックス: 起動埌にゎルヌチンで党ファむルを非同期スキャンし、キヌワヌドむンデックスを構築・維持する

蚭蚈䞊の遞択肢

珟圚の構造は2぀の哲孊の䞭間に䜍眮しおいる:

哲孊A: 履歎䞭心 哲孊B: 知識䞭心
Session.Messages が唯䞀の真実 重芁な情報を MEMORY.md 等に蒞留
䌚話を「再生」しおコンテキスト再珟 構造化知識を「泚入」しおコンテキスト構築
ロヌルバック・ブランチが自然な拡匵 クロスセッション怜玢が自然な拡匵

このコヌドベヌスはすでに Session.Messages (哲孊A) ず MEMORY.md (哲孊B) が共存しおおり、Phase 5 の ParsedPlan むンメモリモデル は哲孊B 方向ぞの垃石になる。


実装順サマリヌ

Phase 0 ──→ Phase 1 ──→ Phase 2 ──→ Phase 3 ──→ Phase 4
 機械的      倀枡し       JSON       Memory      Storage
 眮き換え    最適化     ホットパス    読み取り    write-behind
 (4 commits) (1 commit) (2 commits) (2 commits) (2 commits)
  • Phase 0〜2: アロケヌション削枛 (GC 圧力軜枛)
  • Phase 3: syscall + Split/Join 削枛 (CPU + アロケヌション)
  • Phase 4: ディスク曞き蟌み削枛 (microSD 寿呜保護)
  • Phase 5: 必芁に応じお個別刀断