perf: reduce ReadLongTerm calls from 5+ to 1 per GetMemoryContext

Refactor GetMemoryContext, GetPlanContext, FormatPlanDisplay to read
MEMORY.md once and pass content to internal *From() helpers. Inline
regex checks for HasActivePlan/GetPlanStatus/GetCurrentPhase/
GetTotalPhases within the hot path. Public methods preserved for
backward compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-24 15:46:05 +09:00
parent 346846be35
commit c12a99b846

View file

@ -268,12 +268,15 @@ type PlanStep struct {
// GetPlanPhases parses MEMORY.md and returns all phases with their steps. // GetPlanPhases parses MEMORY.md and returns all phases with their steps.
func (ms *MemoryStore) GetPlanPhases() []PlanPhase { func (ms *MemoryStore) GetPlanPhases() []PlanPhase {
content := ms.ReadLongTerm() return ms.getPlanPhasesFrom(ms.ReadLongTerm())
}
func (ms *MemoryStore) getPlanPhasesFrom(content string) []PlanPhase {
if !reActivePlan.MatchString(content) { if !reActivePlan.MatchString(content) {
return nil return nil
} }
totalPhases := ms.GetTotalPhases() totalPhases := maxPhaseNumber(content)
phases := make([]PlanPhase, 0, totalPhases) phases := make([]PlanPhase, 0, totalPhases)
for p := 1; p <= totalPhases; p++ { for p := 1; p <= totalPhases; p++ {
@ -495,8 +498,10 @@ func BuildInterviewSeed(task, workDir string) string {
// GetInterviewContext returns context for injection during the interviewing phase. // GetInterviewContext returns context for injection during the interviewing phase.
// Includes the full seed + interview guide + target format template. // Includes the full seed + interview guide + target format template.
func (ms *MemoryStore) GetInterviewContext() string { func (ms *MemoryStore) GetInterviewContext() string {
content := ms.ReadLongTerm() return ms.getInterviewContextFrom(ms.ReadLongTerm())
}
func (ms *MemoryStore) getInterviewContextFrom(content string) string {
var sb strings.Builder var sb strings.Builder
sb.WriteString("## Active Plan (interviewing)\n\n") sb.WriteString("## Active Plan (interviewing)\n\n")
sb.WriteString(content) sb.WriteString(content)
@ -543,8 +548,10 @@ func (ms *MemoryStore) GetInterviewContext() string {
// GetReviewContext returns context for injection during the review phase. // GetReviewContext returns context for injection during the review phase.
// Shows the full plan and instructs the AI to wait for user approval. // Shows the full plan and instructs the AI to wait for user approval.
func (ms *MemoryStore) GetReviewContext() string { func (ms *MemoryStore) GetReviewContext() string {
content := ms.ReadLongTerm() return ms.getReviewContextFrom(ms.ReadLongTerm())
}
func (ms *MemoryStore) getReviewContextFrom(content string) string {
var sb strings.Builder var sb strings.Builder
sb.WriteString("## Active Plan (awaiting approval)\n\n") sb.WriteString("## Active Plan (awaiting approval)\n\n")
sb.WriteString(content) sb.WriteString(content)
@ -558,9 +565,15 @@ func (ms *MemoryStore) GetReviewContext() string {
// Only the current phase is shown in detail; completed phases are compressed // Only the current phase is shown in detail; completed phases are compressed
// to one-line summaries; future phases are omitted. // to one-line summaries; future phases are omitted.
func (ms *MemoryStore) GetPlanContext() string { func (ms *MemoryStore) GetPlanContext() string {
content := ms.ReadLongTerm() return ms.getPlanContextFrom(ms.ReadLongTerm())
currentPhase := ms.GetCurrentPhase() }
totalPhases := ms.GetTotalPhases()
func (ms *MemoryStore) getPlanContextFrom(content string) string {
var currentPhase int
if m := rePhase.FindStringSubmatch(content); len(m) >= 2 {
currentPhase, _ = strconv.Atoi(m[1])
}
totalPhases := maxPhaseNumber(content)
taskLine := "" taskLine := ""
if m := reTaskLine.FindStringSubmatch(content); len(m) >= 2 { if m := reTaskLine.FindStringSubmatch(content); len(m) >= 2 {
@ -569,18 +582,18 @@ func (ms *MemoryStore) GetPlanContext() string {
var sb strings.Builder var sb strings.Builder
sb.WriteString("## Active Plan\n") sb.WriteString("## Active Plan\n")
sb.WriteString(fmt.Sprintf("Task: %s | Phase %d/%d\n", taskLine, currentPhase, totalPhases)) fmt.Fprintf(&sb, "Task: %s | Phase %d/%d\n", taskLine, currentPhase, totalPhases)
// Completed phases: one-line summaries // Completed phases: one-line summaries
for p := 1; p < currentPhase; p++ { for p := 1; p < currentPhase; p++ {
title := ms.getPhaseTitle(content, p) title := ms.getPhaseTitle(content, p)
sb.WriteString(fmt.Sprintf("Done: Phase %d (%s)\n", p, title)) fmt.Fprintf(&sb, "Done: Phase %d (%s)\n", p, title)
} }
// Current phase: full detail // Current phase: full detail
if currentPhase > 0 { if currentPhase > 0 {
title := ms.getPhaseTitle(content, currentPhase) title := ms.getPhaseTitle(content, currentPhase)
sb.WriteString(fmt.Sprintf("### Current: Phase %d — %s\n", currentPhase, title)) fmt.Fprintf(&sb, "### Current: Phase %d — %s\n", currentPhase, title)
phaseContent := ms.extractPhaseContent(content, currentPhase) phaseContent := ms.extractPhaseContent(content, currentPhase)
sb.WriteString(strings.TrimSpace(phaseContent)) sb.WriteString(strings.TrimSpace(phaseContent))
sb.WriteString("\n") sb.WriteString("\n")
@ -605,6 +618,21 @@ func (ms *MemoryStore) GetPlanContext() string {
return sb.String() return sb.String()
} }
// maxPhaseNumber returns the highest phase number found in content.
func maxPhaseNumber(content string) int {
matches := rePhaseHeader.FindAllStringSubmatch(content, -1)
max := 0
for _, m := range matches {
if len(m) >= 2 {
n, _ := strconv.Atoi(m[1])
if n > max {
max = n
}
}
}
return max
}
// getPhaseTitle extracts the title of a phase from "## Phase N: Title". // getPhaseTitle extracts the title of a phase from "## Phase N: Title".
func (ms *MemoryStore) getPhaseTitle(content string, phase int) string { func (ms *MemoryStore) getPhaseTitle(content string, phase int) string {
matches := rePhaseHeader.FindAllStringSubmatch(content, -1) matches := rePhaseHeader.FindAllStringSubmatch(content, -1)
@ -654,7 +682,7 @@ func (ms *MemoryStore) extractCommandsSection(content string) string {
// FormatPlanDisplay returns a user-facing display of the full plan with emoji indicators. // FormatPlanDisplay returns a user-facing display of the full plan with emoji indicators.
func (ms *MemoryStore) FormatPlanDisplay() string { func (ms *MemoryStore) FormatPlanDisplay() string {
content := ms.ReadLongTerm() content := ms.ReadLongTerm()
if !ms.HasActivePlan() { if !reActivePlan.MatchString(content) {
return "No active plan." return "No active plan."
} }
@ -662,9 +690,15 @@ func (ms *MemoryStore) FormatPlanDisplay() string {
if m := reTaskLine.FindStringSubmatch(content); len(m) >= 2 { if m := reTaskLine.FindStringSubmatch(content); len(m) >= 2 {
taskLine = strings.TrimSpace(m[1]) taskLine = strings.TrimSpace(m[1])
} }
status := ms.GetPlanStatus() var status string
currentPhase := ms.GetCurrentPhase() if m := reStatus.FindStringSubmatch(content); len(m) >= 2 {
phases := ms.GetPlanPhases() status = strings.TrimSpace(m[1])
}
var currentPhase int
if m := rePhase.FindStringSubmatch(content); len(m) >= 2 {
currentPhase, _ = strconv.Atoi(m[1])
}
phases := ms.getPlanPhasesFrom(content)
var sb strings.Builder var sb strings.Builder
sb.WriteString(fmt.Sprintf("Plan: %s\n", taskLine)) sb.WriteString(fmt.Sprintf("Plan: %s\n", taskLine))
@ -725,16 +759,21 @@ func (ms *MemoryStore) GetMemoryContext() string {
var parts []string var parts []string
longTerm := ms.ReadLongTerm() longTerm := ms.ReadLongTerm()
hasActivePlan := longTerm != "" && reActivePlan.MatchString(longTerm)
if longTerm != "" { if longTerm != "" {
if ms.HasActivePlan() { if hasActivePlan {
status := ms.GetPlanStatus() var status string
if m := reStatus.FindStringSubmatch(longTerm); len(m) >= 2 {
status = strings.TrimSpace(m[1])
}
switch status { switch status {
case "interviewing": case "interviewing":
parts = append(parts, ms.GetInterviewContext()) parts = append(parts, ms.getInterviewContextFrom(longTerm))
case "review": case "review":
parts = append(parts, ms.GetReviewContext()) parts = append(parts, ms.getReviewContextFrom(longTerm))
default: default:
parts = append(parts, ms.GetPlanContext()) parts = append(parts, ms.getPlanContextFrom(longTerm))
} }
} else { } else {
parts = append(parts, "## Long-term Memory\n\n"+longTerm) parts = append(parts, "## Long-term Memory\n\n"+longTerm)
@ -742,7 +781,7 @@ func (ms *MemoryStore) GetMemoryContext() string {
} }
// Suppress daily notes when a plan is active to save context // Suppress daily notes when a plan is active to save context
if !ms.HasActivePlan() { if !hasActivePlan {
recentNotes := ms.GetRecentDailyNotes(3) recentNotes := ms.GetRecentDailyNotes(3)
if recentNotes != "" { if recentNotes != "" {
parts = append(parts, "## Recent Daily Notes\n\n"+recentNotes) parts = append(parts, "## Recent Daily Notes\n\n"+recentNotes)