Skip to content

Commit 1d52086

Browse files
claudeanandgupta42
authored andcommitted
Add self-improvement loop: applied tracking, insights, staleness detection
OpenClaw-inspired self-improvement mechanisms: 1. Wire up incrementApplied at injection time — counters now actually increment once per session per entry (deduped via session-scoped set), making "Most Applied" dashboard and priority sorting meaningful 2. TrainingInsights module analyzes training metadata and surfaces: - Stale entries (7+ days old, never applied) — suggests cleanup - High-value entries (5+ applications) — highlights most impactful - Near-limit warnings (18-19 of 20 entries per kind) - Consolidation opportunities (3+ entries with shared name prefix) 3. Insights automatically shown in training_list output 4. 24 new tests covering all insight types, boundary conditions, session tracking dedup, and format output 152 training tests + 305 memory tests all pass. https://claude.ai/code/session_01V17Kk3qCZFp9ZJiuNYucoq
1 parent 5cdec64 commit 1d52086

5 files changed

Lines changed: 579 additions & 2 deletions

File tree

packages/opencode/src/altimate/tools/training-list.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// altimate_change - Training list tool for AI Teammate learned knowledge
22
import z from "zod"
33
import { Tool } from "../../tool/tool"
4-
import { TrainingStore, TrainingPrompt } from "../training"
4+
import { TrainingStore, TrainingPrompt, TrainingInsights } from "../training"
55
import { TrainingKind } from "../training/types"
66

77
export const TrainingListTool = Tool.define("training_list", {
@@ -87,10 +87,14 @@ export const TrainingListTool = Tool.define("training_list", {
8787
sections.push("")
8888
}
8989

90+
// Self-improvement insights
91+
const insights = await TrainingInsights.analyze()
92+
const insightText = TrainingInsights.format(insights)
93+
9094
return {
9195
title: `Training: ${entries.length} entries`,
9296
metadata: { count: entries.length, budgetPercent: budget.percent },
93-
output: summary + highlights + sections.join("\n"),
97+
output: summary + highlights + sections.join("\n") + insightText,
9498
}
9599
} catch (e) {
96100
return {

packages/opencode/src/altimate/training/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// altimate_change - Training module exports
22
export { TrainingStore, type TrainingEntry } from "./store"
33
export { TrainingPrompt } from "./prompt"
4+
export { TrainingInsights, type TrainingInsight } from "./insights"
45
export {
56
TrainingKind,
67
TRAINING_TAG,
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// altimate_change - Training insights: self-improvement recommendations
2+
// Inspired by OpenClaw's crystallization pattern — surfaces actionable
3+
// recommendations based on training usage patterns.
4+
import { TrainingStore, type TrainingEntry } from "./store"
5+
import { TRAINING_MAX_PATTERNS_PER_KIND, type TrainingKind } from "./types"
6+
7+
export interface TrainingInsight {
8+
type: "stale" | "high-value" | "near-limit" | "budget-warning" | "consolidation"
9+
severity: "info" | "warning"
10+
message: string
11+
entries?: string[]
12+
}
13+
14+
export namespace TrainingInsights {
15+
/**
16+
* Analyze training entries and return actionable insights.
17+
* Lightweight — reads from disk only, no LLM calls.
18+
*/
19+
export async function analyze(): Promise<TrainingInsight[]> {
20+
const entries = await TrainingStore.list()
21+
if (entries.length === 0) return []
22+
23+
const insights: TrainingInsight[] = []
24+
25+
// 1. Stale entries: saved but never applied after being injected multiple sessions
26+
const stale = entries.filter((e) => e.meta.applied === 0 && isOlderThanDays(e.created, 7))
27+
if (stale.length > 0) {
28+
insights.push({
29+
type: "stale",
30+
severity: "info",
31+
message: `${stale.length} training entry/entries saved 7+ days ago but never applied. Consider reviewing or removing.`,
32+
entries: stale.map((e) => `${e.kind}/${e.name}`),
33+
})
34+
}
35+
36+
// 2. High-value entries: frequently applied, worth highlighting
37+
const highValue = entries.filter((e) => e.meta.applied >= 5).sort((a, b) => b.meta.applied - a.meta.applied)
38+
if (highValue.length > 0) {
39+
insights.push({
40+
type: "high-value",
41+
severity: "info",
42+
message: `${highValue.length} high-value entry/entries (applied 5+ times). These are your most impactful training.`,
43+
entries: highValue.slice(0, 5).map((e) => `${e.kind}/${e.name} (${e.meta.applied}x)`),
44+
})
45+
}
46+
47+
// 3. Near-limit warnings per kind
48+
const counts = await TrainingStore.count()
49+
for (const [kind, count] of Object.entries(counts)) {
50+
if (count >= TRAINING_MAX_PATTERNS_PER_KIND - 2 && count < TRAINING_MAX_PATTERNS_PER_KIND) {
51+
insights.push({
52+
type: "near-limit",
53+
severity: "warning",
54+
message: `${kind} entries near limit: ${count}/${TRAINING_MAX_PATTERNS_PER_KIND}. Consider consolidating before adding more.`,
55+
})
56+
}
57+
}
58+
59+
// 4. Consolidation opportunities: multiple entries of same kind with similar names
60+
const byKind = new Map<TrainingKind, TrainingEntry[]>()
61+
for (const e of entries) {
62+
const list = byKind.get(e.kind) ?? []
63+
list.push(e)
64+
byKind.set(e.kind, list)
65+
}
66+
for (const [kind, items] of byKind) {
67+
if (items.length < 2) continue
68+
// Find entries whose names share a common prefix (3+ chars)
69+
const groups = findRelatedEntries(items)
70+
for (const group of groups) {
71+
if (group.length >= 3) {
72+
insights.push({
73+
type: "consolidation",
74+
severity: "info",
75+
message: `${group.length} related ${kind} entries could potentially be consolidated into one.`,
76+
entries: group.map((e) => e.name),
77+
})
78+
}
79+
}
80+
}
81+
82+
return insights
83+
}
84+
85+
/**
86+
* Format insights for display in training_list output.
87+
*/
88+
export function format(insights: TrainingInsight[]): string {
89+
if (insights.length === 0) return ""
90+
const lines = ["\n### Insights"]
91+
for (const insight of insights) {
92+
const icon = insight.severity === "warning" ? "!" : "-"
93+
lines.push(`${icon} ${insight.message}`)
94+
if (insight.entries && insight.entries.length > 0) {
95+
for (const e of insight.entries.slice(0, 5)) {
96+
lines.push(` - \`${e}\``)
97+
}
98+
if (insight.entries.length > 5) {
99+
lines.push(` - ...and ${insight.entries.length - 5} more`)
100+
}
101+
}
102+
}
103+
return lines.join("\n")
104+
}
105+
}
106+
107+
function isOlderThanDays(dateStr: string, days: number): boolean {
108+
const created = new Date(dateStr)
109+
const cutoff = new Date()
110+
cutoff.setDate(cutoff.getDate() - days)
111+
return created < cutoff
112+
}
113+
114+
function findRelatedEntries(entries: TrainingEntry[]): TrainingEntry[][] {
115+
// Group entries that share a common prefix of 3+ characters
116+
const groups: TrainingEntry[][] = []
117+
const used = new Set<string>()
118+
119+
for (let i = 0; i < entries.length; i++) {
120+
if (used.has(entries[i].name)) continue
121+
const group = [entries[i]]
122+
const prefix = entries[i].name.split("-")[0]
123+
if (prefix.length < 3) continue
124+
125+
for (let j = i + 1; j < entries.length; j++) {
126+
if (used.has(entries[j].name)) continue
127+
if (entries[j].name.startsWith(prefix)) {
128+
group.push(entries[j])
129+
used.add(entries[j].name)
130+
}
131+
}
132+
if (group.length >= 2) {
133+
used.add(entries[i].name)
134+
groups.push(group)
135+
}
136+
}
137+
return groups
138+
}

packages/opencode/src/altimate/training/prompt.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,20 @@ const KIND_HEADERS: Record<TrainingKind, { header: string; instruction: string }
2121
},
2222
}
2323

24+
// Track which entries have been applied this session to avoid double-counting
25+
const appliedThisSession = new Set<string>()
26+
2427
export namespace TrainingPrompt {
2528
export function formatEntry(entry: TrainingEntry): string {
2629
const meta = entry.meta.applied > 0 ? ` (applied ${entry.meta.applied}x)` : ""
2730
return `#### ${entry.name}${meta}\n${entry.content}`
2831
}
2932

33+
/** Reset session tracking (call at session start) */
34+
export function resetSession(): void {
35+
appliedThisSession.clear()
36+
}
37+
3038
export async function inject(budget: number = TRAINING_BUDGET): Promise<string> {
3139
const entries = await TrainingStore.list()
3240
if (entries.length === 0) return ""
@@ -42,6 +50,7 @@ export namespace TrainingPrompt {
4250
"## Teammate Training\n\nYou have been trained on the following knowledge by your team. Apply it consistently.\n"
4351
let result = header
4452
let used = header.length
53+
const injected: TrainingEntry[] = []
4554

4655
for (const kind of ["rule", "pattern", "standard", "glossary"] as TrainingKind[]) {
4756
const items = grouped.get(kind)
@@ -61,6 +70,15 @@ export namespace TrainingPrompt {
6170
if (used + needed > budget) break
6271
result += "\n" + formatted + "\n"
6372
used += needed
73+
injected.push(entry)
74+
}
75+
}
76+
77+
// Increment applied count once per session per entry (fire-and-forget)
78+
for (const entry of injected) {
79+
if (!appliedThisSession.has(entry.id)) {
80+
appliedThisSession.add(entry.id)
81+
TrainingStore.incrementApplied(entry.scope, entry.kind, entry.name).catch(() => {})
6482
}
6583
}
6684

0 commit comments

Comments
 (0)