codemini-cli 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/OPERATIONS.md +4 -2
- package/README.md +89 -11
- package/deployment.md +14 -7
- package/package.json +1 -2
- package/src/cli.js +1 -1
- package/src/commands/skill.js +145 -53
- package/src/core/agent-loop.js +18 -311
- package/src/core/chat-runtime.js +389 -53
- package/src/core/command-loader.js +12 -5
- package/src/core/config-store.js +2 -0
- package/src/core/context-compact.js +34 -9
- package/src/core/default-system-prompt.js +5 -5
- package/src/core/dream-audit.js +12 -0
- package/src/core/dream-consolidate.js +131 -59
- package/src/core/dream-evaluator.js +86 -0
- package/src/core/fff-adapter.js +1 -1
- package/src/core/memory-store.js +145 -10
- package/src/core/provider/openai-compatible.js +40 -5
- package/src/core/reflect-skill.js +178 -0
- package/src/core/shell-profile.js +8 -8
- package/src/core/tool-args.js +181 -0
- package/src/core/tool-result-store.js +206 -0
- package/src/core/tools.js +144 -190
- package/src/tui/chat-app.js +270 -28
- package/src/tui/tool-activity/presenters/misc.js +14 -0
- package/src/core/provider/anthropic.sdk-backup.js +0 -439
- package/src/core/provider/openai-compatible.sdk-backup.js +0 -412
|
@@ -58,6 +58,12 @@ function isSafeEntry(entry) {
|
|
|
58
58
|
return entry !== '.' && entry !== '..' && !entry.includes('/') && !entry.includes('\\');
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function setCommand(out, name, command) {
|
|
62
|
+
const existing = out.get(name);
|
|
63
|
+
if (existing?.source === 'bundled-skill') return;
|
|
64
|
+
out.set(name, command);
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
function loadMarkdownCommandsFromDir(baseDir, source, out) {
|
|
62
68
|
if (!fs.existsSync(baseDir)) return;
|
|
63
69
|
for (const entry of safeEntries(baseDir)) {
|
|
@@ -70,7 +76,7 @@ function loadMarkdownCommandsFromDir(baseDir, source, out) {
|
|
|
70
76
|
if (fs.existsSync(commandFile)) {
|
|
71
77
|
const raw = fs.readFileSync(commandFile, 'utf8');
|
|
72
78
|
const parsed = parseFrontmatter(raw);
|
|
73
|
-
out
|
|
79
|
+
setCommand(out, entry, {
|
|
74
80
|
name: entry,
|
|
75
81
|
source,
|
|
76
82
|
path: commandFile,
|
|
@@ -85,7 +91,7 @@ function loadMarkdownCommandsFromDir(baseDir, source, out) {
|
|
|
85
91
|
const name = entry.replace(/\.md$/, '');
|
|
86
92
|
const raw = fs.readFileSync(full, 'utf8');
|
|
87
93
|
const parsed = parseFrontmatter(raw);
|
|
88
|
-
out
|
|
94
|
+
setCommand(out, name, {
|
|
89
95
|
name,
|
|
90
96
|
source,
|
|
91
97
|
path: full,
|
|
@@ -107,7 +113,7 @@ function loadLegacySkillsFromDir(baseDir, source, out) {
|
|
|
107
113
|
if (!fs.existsSync(skillFile)) continue;
|
|
108
114
|
const raw = fs.readFileSync(skillFile, 'utf8');
|
|
109
115
|
const parsed = parseFrontmatter(raw);
|
|
110
|
-
out
|
|
116
|
+
setCommand(out, entry, {
|
|
111
117
|
name: entry,
|
|
112
118
|
source: `${source}-skill`,
|
|
113
119
|
path: skillFile,
|
|
@@ -131,7 +137,7 @@ function loadBundledSkillsFromDir(baseDir, out) {
|
|
|
131
137
|
if (!fs.existsSync(skillFile)) continue;
|
|
132
138
|
const raw = fs.readFileSync(skillFile, 'utf8');
|
|
133
139
|
const parsed = parseFrontmatter(raw);
|
|
134
|
-
out
|
|
140
|
+
setCommand(out, entry, {
|
|
135
141
|
name: entry,
|
|
136
142
|
source: 'bundled-skill',
|
|
137
143
|
path: skillFile,
|
|
@@ -151,12 +157,13 @@ function loadInstalledSkillsFromRegistry(baseDir, registry, out) {
|
|
|
151
157
|
for (const skill of registry.skills) {
|
|
152
158
|
if (skill.enabled === false) continue;
|
|
153
159
|
const name = skill.name;
|
|
160
|
+
if (out.has(name)) continue;
|
|
154
161
|
const entry = skill.entryFile || 'SKILL.md';
|
|
155
162
|
const full = path.join(baseDir, name, entry);
|
|
156
163
|
if (!fs.existsSync(full)) continue;
|
|
157
164
|
const raw = fs.readFileSync(full, 'utf8');
|
|
158
165
|
const parsed = parseFrontmatter(raw);
|
|
159
|
-
out
|
|
166
|
+
setCommand(out, name, {
|
|
160
167
|
name,
|
|
161
168
|
source: 'registry-skill',
|
|
162
169
|
path: full,
|
package/src/core/config-store.js
CHANGED
|
@@ -65,6 +65,7 @@ const DEFAULT_CONFIG = {
|
|
|
65
65
|
memory: {
|
|
66
66
|
enabled: true,
|
|
67
67
|
auto_write: true,
|
|
68
|
+
auto_capture: true,
|
|
68
69
|
inject_on_session_start: true,
|
|
69
70
|
auto_dream_threshold: 10,
|
|
70
71
|
max_items_per_scope: 12,
|
|
@@ -165,6 +166,7 @@ function normalizePolicyLists(config) {
|
|
|
165
166
|
next.memory = next.memory || {};
|
|
166
167
|
next.memory.enabled = next.memory.enabled !== false;
|
|
167
168
|
next.memory.auto_write = next.memory.auto_write !== false;
|
|
169
|
+
next.memory.auto_capture = next.memory.auto_capture !== false;
|
|
168
170
|
next.memory.inject_on_session_start = next.memory.inject_on_session_start !== false;
|
|
169
171
|
next.memory.max_items_per_scope = Math.max(1, Number(next.memory.max_items_per_scope || 12));
|
|
170
172
|
next.memory.auto_dream_threshold = Number(next.memory.auto_dream_threshold ?? 10);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { trimInline } from './string-utils.js';
|
|
2
|
+
import { summarizeToolResult } from './tool-result-store.js';
|
|
2
3
|
|
|
3
4
|
function textFromContent(content) {
|
|
4
5
|
if (typeof content === 'string') return content;
|
|
@@ -37,20 +38,30 @@ function modeToKeepRecent(mode) {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
function buildLocalSummary(messages) {
|
|
40
|
-
const
|
|
41
|
+
const goal = [];
|
|
42
|
+
const constraints = [];
|
|
43
|
+
const changedFiles = new Set();
|
|
44
|
+
const verification = [];
|
|
45
|
+
const openThreads = [];
|
|
41
46
|
const limit = 16;
|
|
42
47
|
for (const msg of messages.slice(-limit)) {
|
|
43
48
|
if (msg.role === 'tool') {
|
|
44
|
-
// Try to parse tool result as JSON for semantic summary
|
|
45
49
|
const text = textFromContent(msg.content);
|
|
46
50
|
let parsed;
|
|
47
51
|
try { parsed = JSON.parse(text); } catch { parsed = null; }
|
|
48
52
|
if (parsed && typeof parsed === 'object') {
|
|
49
53
|
const summary = summarizeToolResult(parsed);
|
|
50
|
-
|
|
54
|
+
if (parsed.path) changedFiles.add(String(parsed.path));
|
|
55
|
+
if (parsed.command || parsed.code != null || parsed.stderr || parsed.stdout) {
|
|
56
|
+
verification.push(summary);
|
|
57
|
+
} else {
|
|
58
|
+
openThreads.push(`tool_result: ${summary}`);
|
|
59
|
+
}
|
|
51
60
|
} else {
|
|
52
61
|
const clipped = text.length > 120 ? `${text.slice(0, 117)}...` : text;
|
|
53
|
-
|
|
62
|
+
const match = clipped.match(/([A-Za-z0-9_./-]+\.[A-Za-z0-9]+):\d+/);
|
|
63
|
+
if (match) changedFiles.add(match[1]);
|
|
64
|
+
openThreads.push(`tool_result: ${clipped}`);
|
|
54
65
|
}
|
|
55
66
|
continue;
|
|
56
67
|
}
|
|
@@ -59,21 +70,35 @@ function buildLocalSummary(messages) {
|
|
|
59
70
|
const toolCallCount = Array.isArray(msg.tool_calls) ? msg.tool_calls.length : 0;
|
|
60
71
|
const toolInfo = toolCallCount > 0 ? ` [called ${toolCallCount} tool(s)]` : '';
|
|
61
72
|
const clipped = text.length > 300 ? `${text.slice(0, 297)}...` : text;
|
|
62
|
-
|
|
73
|
+
if (clipped) openThreads.push(`assistant: ${clipped}${toolInfo}`);
|
|
63
74
|
continue;
|
|
64
75
|
}
|
|
65
76
|
if (msg.role === 'user') {
|
|
66
77
|
const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
|
|
67
78
|
const clipped = text.length > 200 ? `${text.slice(0, 197)}...` : text;
|
|
68
|
-
|
|
79
|
+
if (goal.length === 0) goal.push(clipped);
|
|
80
|
+
else constraints.push(clipped);
|
|
69
81
|
continue;
|
|
70
82
|
}
|
|
71
83
|
const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
|
|
72
84
|
if (!text) continue;
|
|
73
85
|
const clipped = text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
74
|
-
|
|
86
|
+
openThreads.push(`${msg.role}: ${clipped}`);
|
|
75
87
|
}
|
|
76
|
-
|
|
88
|
+
const lines = [
|
|
89
|
+
'Context Summary',
|
|
90
|
+
'Goal:',
|
|
91
|
+
goal.length > 0 ? `- ${goal[0]}` : '- Unknown from compacted context',
|
|
92
|
+
'Key Constraints:',
|
|
93
|
+
...(constraints.length > 0 ? constraints.slice(-4).map((item) => `- ${item}`) : ['- None recorded']),
|
|
94
|
+
'Changed Files:',
|
|
95
|
+
...(changedFiles.size > 0 ? [...changedFiles].slice(0, 8).map((item) => `- ${item}`) : ['- None recorded']),
|
|
96
|
+
'Verification:',
|
|
97
|
+
...(verification.length > 0 ? verification.slice(-4).map((item) => `- ${item}`) : ['- None recorded']),
|
|
98
|
+
'Open Threads:',
|
|
99
|
+
...(openThreads.length > 0 ? openThreads.slice(-8).map((item) => `- ${item}`) : ['- None recorded'])
|
|
100
|
+
];
|
|
101
|
+
return lines.join('\n').trim();
|
|
77
102
|
}
|
|
78
103
|
|
|
79
104
|
export function compactMessagesLocally(messages, { mode = 'default' } = {}) {
|
|
@@ -9,14 +9,14 @@ function getToolFewShotBlock() {
|
|
|
9
9
|
Use these as style examples for tool calls:
|
|
10
10
|
|
|
11
11
|
Current working directory: ${cwd}
|
|
12
|
-
When a tool takes
|
|
12
|
+
When a tool takes path, build it from the current working directory and prefer absolute paths.
|
|
13
13
|
If the user mentions a project-relative path like src/app.ts, resolve it from ${cwd} instead of guessing parent directories.
|
|
14
14
|
|
|
15
15
|
1. File discovery then read
|
|
16
16
|
User: compare the auth flow
|
|
17
17
|
Assistant: first narrow the search with the project index
|
|
18
18
|
Tool: query_project_index({"query":"auth flow","path":"src","max_results":3})
|
|
19
|
-
Tool: read({"
|
|
19
|
+
Tool: read({"path":"${cwd}/src/auth/service.ts"})
|
|
20
20
|
|
|
21
21
|
If the visible tool list does not include a needed capability, load it with tool_search instead of assuming it does not exist.
|
|
22
22
|
Example:
|
|
@@ -27,7 +27,7 @@ Tool: glob({"pattern":"src/**/*.ts"})
|
|
|
27
27
|
User: rename loginUser to signInUser
|
|
28
28
|
Assistant: first find the exact occurrences
|
|
29
29
|
Tool: grep({"pattern":"loginUser","path":"src"})
|
|
30
|
-
Tool: edit({"
|
|
30
|
+
Tool: edit({"path":"${cwd}/src/auth/service.ts","old_text":"loginUser","new_text":"signInUser"})
|
|
31
31
|
|
|
32
32
|
3. Read a specific range
|
|
33
33
|
User: inspect the reducer around line 120
|
|
@@ -43,7 +43,7 @@ Assistant: keep the checklist updated as each phase finishes, and do not give a
|
|
|
43
43
|
5. Create a new file
|
|
44
44
|
User: add a notes file
|
|
45
45
|
Assistant: create the file directly
|
|
46
|
-
Tool: write({"
|
|
46
|
+
Tool: write({"path":"${cwd}/notes.txt","content":"todo\\n"})
|
|
47
47
|
|
|
48
48
|
6. Save a high-signal observation to memory
|
|
49
49
|
When you notice a reusable pattern, a user correction, a repeated failure, or a stable preference — save it to persistent memory. Choose scope carefully:
|
|
@@ -73,7 +73,7 @@ Tool: tool_search({"query":"web_search"})
|
|
|
73
73
|
Tool: web_search({"query":"latest pnpm release","max_results":5})
|
|
74
74
|
|
|
75
75
|
Prefer these direct tool shapes over multi-step metadata reads or shell fallbacks.
|
|
76
|
-
Prefer explicit absolute
|
|
76
|
+
Prefer explicit absolute path values when the current working directory is known.`;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
function getEnvBlock() {
|
package/src/core/dream-audit.js
CHANGED
|
@@ -56,6 +56,18 @@ function renderReport(report) {
|
|
|
56
56
|
lines.push('');
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
if (report.maintenance?.length) {
|
|
60
|
+
lines.push('## Memory Maintenance');
|
|
61
|
+
for (const item of report.maintenance) {
|
|
62
|
+
if (item.skipped) {
|
|
63
|
+
lines.push(`- [${item.scope}] skipped: ${item.reason}`);
|
|
64
|
+
} else {
|
|
65
|
+
lines.push(`- [${item.scope}] ${item.before} -> ${item.after} item(s)${item.changed ? ' changed' : ' marked clean'}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
lines.push('');
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
if (report.disagreements?.length) {
|
|
60
72
|
lines.push('## Reviewer Disagreements');
|
|
61
73
|
for (const d of report.disagreements) {
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getMemoryBucketMaintenance,
|
|
3
|
+
listMemories,
|
|
4
|
+
listInbox,
|
|
5
|
+
archiveEntry,
|
|
6
|
+
promoteMemory,
|
|
7
|
+
replaceMemoryBucket
|
|
8
|
+
} from './memory-store.js';
|
|
2
9
|
import { writeDreamAuditReport } from './dream-audit.js';
|
|
3
|
-
import { evaluateInboxBatch } from './dream-evaluator.js';
|
|
10
|
+
import { evaluateInboxBatch, evaluateMemoryMaintenance } from './dream-evaluator.js';
|
|
4
11
|
|
|
5
12
|
const LONGTERM_TYPES = new Set(['preference', 'pattern', 'win', 'decision']);
|
|
6
13
|
const OPERATIONAL_TYPES = new Set(['correction', 'failure', 'gap', 'observation']);
|
|
@@ -22,6 +29,76 @@ function memoryContainsSummary(memory, summaryKey) {
|
|
|
22
29
|
return content.includes(summaryKey) || summary.includes(summaryKey);
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
function maintenanceScopes(scopeFilter) {
|
|
33
|
+
const scope = normalizeText(scopeFilter);
|
|
34
|
+
if (!scope) return ['user', 'global', 'project'];
|
|
35
|
+
if (scope === 'repo') return ['project'];
|
|
36
|
+
if (['user', 'global', 'project'].includes(scope)) return [scope];
|
|
37
|
+
return ['user', 'global', 'project'];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function runMemoryMaintenance({
|
|
41
|
+
dryRun = false,
|
|
42
|
+
scope = null,
|
|
43
|
+
workspaceRoot = process.cwd(),
|
|
44
|
+
config = {}
|
|
45
|
+
} = {}) {
|
|
46
|
+
const reports = [];
|
|
47
|
+
const filesChanged = [];
|
|
48
|
+
|
|
49
|
+
for (const memoryScope of maintenanceScopes(scope)) {
|
|
50
|
+
const maintenance = await getMemoryBucketMaintenance({ scope: memoryScope, workspaceRoot });
|
|
51
|
+
const items = await listMemories({ scope: memoryScope, workspaceRoot });
|
|
52
|
+
if (items.length === 0) {
|
|
53
|
+
reports.push({ scope: memoryScope, skipped: true, reason: 'empty' });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (maintenance.fresh) {
|
|
57
|
+
reports.push({ scope: memoryScope, skipped: true, reason: 'already-maintained', itemCount: items.length });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const evaluated = dryRun
|
|
62
|
+
? { items, archives: [] }
|
|
63
|
+
: await evaluateMemoryMaintenance({ scope: memoryScope, items, config, workspaceRoot });
|
|
64
|
+
if (evaluated.error) {
|
|
65
|
+
reports.push({ scope: memoryScope, skipped: true, reason: `maintenance-error: ${evaluated.error}`, itemCount: items.length });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const nextItems = Array.isArray(evaluated.items) && evaluated.items.length > 0 ? evaluated.items : items;
|
|
69
|
+
const changed =
|
|
70
|
+
JSON.stringify(nextItems.map((item) => [item.kind, item.content, item.summary, item.lifecycle || ''])) !==
|
|
71
|
+
JSON.stringify(items.map((item) => [item.kind, item.content, item.summary, item.lifecycle || '']));
|
|
72
|
+
|
|
73
|
+
if (!dryRun) {
|
|
74
|
+
await replaceMemoryBucket({
|
|
75
|
+
scope: memoryScope,
|
|
76
|
+
items: nextItems,
|
|
77
|
+
workspaceRoot,
|
|
78
|
+
markMaintained: true
|
|
79
|
+
});
|
|
80
|
+
filesChanged.push({
|
|
81
|
+
file: memoryScope === 'project' ? 'memory/project/*.json' : `memory/${memoryScope}.json`,
|
|
82
|
+
why: changed
|
|
83
|
+
? `LLM-maintained ${items.length} item(s) into ${nextItems.length} item(s)`
|
|
84
|
+
: `Marked ${items.length} item(s) as maintained`
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
reports.push({
|
|
89
|
+
scope: memoryScope,
|
|
90
|
+
skipped: false,
|
|
91
|
+
before: items.length,
|
|
92
|
+
after: nextItems.length,
|
|
93
|
+
changed,
|
|
94
|
+
archives: evaluated.archives || [],
|
|
95
|
+
dryRun
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { reports, filesChanged };
|
|
100
|
+
}
|
|
101
|
+
|
|
25
102
|
export async function runDreamConsolidation({
|
|
26
103
|
dryRun = false,
|
|
27
104
|
scope = null,
|
|
@@ -31,9 +108,6 @@ export async function runDreamConsolidation({
|
|
|
31
108
|
} = {}) {
|
|
32
109
|
const scopeFilter = scope || null;
|
|
33
110
|
const inbox = await listInbox({ scope: scopeFilter });
|
|
34
|
-
if (inbox.length === 0) {
|
|
35
|
-
return { ok: true, dryRun, message: 'No inbox entries to consolidate.', promotions: [], rejections: [], archives: [] };
|
|
36
|
-
}
|
|
37
111
|
|
|
38
112
|
const [globalMemories, userMemories, projectMemories] = await Promise.all([
|
|
39
113
|
listMemories({ scope: 'global', workspaceRoot }),
|
|
@@ -77,67 +151,63 @@ export async function runDreamConsolidation({
|
|
|
77
151
|
candidates.push(entry);
|
|
78
152
|
}
|
|
79
153
|
|
|
80
|
-
if (candidates.length
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
return { ok: true, dryRun, ...report };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/* ── Phase 2: LLM 批量评估(质量门控 + scope 分类 + 内容提炼) ── */
|
|
90
|
-
const llmResults = dryRun
|
|
91
|
-
? candidates.map((e) => ({ id: e.id, action: 'keep', scope: 'global', kind: e.type || 'observation', content: e.details || e.summary, summary: e.summary, confidence: 0.9 }))
|
|
92
|
-
: await evaluateInboxBatch({ entries: candidates, config, workspaceRoot });
|
|
154
|
+
if (candidates.length > 0) {
|
|
155
|
+
/* ── Phase 2: LLM 批量评估(质量门控 + scope 分类 + 内容提炼) ── */
|
|
156
|
+
const llmResults = dryRun
|
|
157
|
+
? candidates.map((e) => ({ id: e.id, action: 'keep', scope: 'global', kind: e.type || 'observation', content: e.details || e.summary, summary: e.summary, confidence: 0.9 }))
|
|
158
|
+
: await evaluateInboxBatch({ entries: candidates, config, workspaceRoot });
|
|
93
159
|
|
|
94
|
-
|
|
160
|
+
const resultMap = new Map(llmResults.map((r) => [r.id, r]));
|
|
95
161
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
162
|
+
/* ── Phase 3: 按评估结果 promote 或 archive ─────────────────── */
|
|
163
|
+
for (const entry of candidates) {
|
|
164
|
+
const evaluation = resultMap.get(entry.id);
|
|
99
165
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const promoteScope = evaluation.scope || 'global';
|
|
108
|
-
const lifecycle = chooseLifecycle(evaluation.kind);
|
|
109
|
-
const enrichedEntry = {
|
|
110
|
-
...entry,
|
|
111
|
-
/* 用 LLM 提炼后的内容覆盖原始报错 */
|
|
112
|
-
summary: evaluation.summary || entry.summary,
|
|
113
|
-
details: evaluation.content || entry.details || entry.summary,
|
|
114
|
-
type: evaluation.kind || entry.type || 'observation'
|
|
115
|
-
};
|
|
166
|
+
if (!evaluation || evaluation.action === 'discard') {
|
|
167
|
+
const reason = evaluation?.reason || 'LLM discarded';
|
|
168
|
+
if (!dryRun) await archiveEntry(entry, 'discarded-by-evaluator', reason);
|
|
169
|
+
rejections.push({ summary: entry.summary, reason: `evaluator-discard: ${reason}` });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
116
172
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
173
|
+
const promoteScope = evaluation.scope || 'global';
|
|
174
|
+
const lifecycle = chooseLifecycle(evaluation.kind);
|
|
175
|
+
const enrichedEntry = {
|
|
176
|
+
...entry,
|
|
177
|
+
/* 用 LLM 提炼后的内容覆盖原始报错 */
|
|
178
|
+
summary: evaluation.summary || entry.summary,
|
|
179
|
+
details: evaluation.content || entry.details || entry.summary,
|
|
180
|
+
type: evaluation.kind || entry.type || 'observation'
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
if (!dryRun) {
|
|
184
|
+
try {
|
|
185
|
+
await promoteMemory({
|
|
186
|
+
entry: enrichedEntry,
|
|
187
|
+
scope: promoteScope,
|
|
188
|
+
lifecycle,
|
|
189
|
+
workspaceRoot,
|
|
190
|
+
config,
|
|
191
|
+
confidence: evaluation.confidence || 0.8
|
|
192
|
+
});
|
|
193
|
+
filesChanged.push({ file: `memory/${promoteScope}.json`, why: `Promoted "${enrichedEntry.summary}" as ${lifecycle} (${promoteScope})` });
|
|
194
|
+
promotions.push({ summary: enrichedEntry.summary, scope: promoteScope, lifecycle, rationale: evaluation.kind, confidence: evaluation.confidence });
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const reason = String(error?.message || error || 'promotion failed').slice(0, 180);
|
|
197
|
+
await archiveEntry(entry, 'promotion-failed', reason);
|
|
198
|
+
rejections.push({ summary: entry.summary, reason: `promotion-failed: ${reason}` });
|
|
199
|
+
archives.push({ summary: entry.summary, reason: 'promotion-failed' });
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
134
202
|
}
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
203
|
|
|
138
|
-
|
|
204
|
+
promotions.push({ summary: enrichedEntry.summary, scope: promoteScope, lifecycle, rationale: evaluation.kind, confidence: evaluation.confidence, dryRun: true });
|
|
205
|
+
}
|
|
139
206
|
}
|
|
140
207
|
|
|
208
|
+
const maintenance = await runMemoryMaintenance({ dryRun, scope: scopeFilter, workspaceRoot, config });
|
|
209
|
+
filesChanged.push(...maintenance.filesChanged);
|
|
210
|
+
|
|
141
211
|
const report = {
|
|
142
212
|
timestamp: new Date().toISOString(),
|
|
143
213
|
filesRead,
|
|
@@ -145,7 +215,9 @@ export async function runDreamConsolidation({
|
|
|
145
215
|
candidatesGenerated: inbox.length,
|
|
146
216
|
promotions,
|
|
147
217
|
rejections,
|
|
148
|
-
archives
|
|
218
|
+
archives,
|
|
219
|
+
maintenance: maintenance.reports,
|
|
220
|
+
...(inbox.length === 0 ? { message: 'No inbox entries to consolidate; maintained existing memory buckets.' } : {})
|
|
149
221
|
};
|
|
150
222
|
|
|
151
223
|
if (!dryRun && writeAudit) {
|
|
@@ -22,6 +22,26 @@ Rules:
|
|
|
22
22
|
- General coding/environment knowledge → scope "global"
|
|
23
23
|
- If in doubt, discard. Memory is expensive; only promote what future sessions will genuinely benefit from.`;
|
|
24
24
|
|
|
25
|
+
const MAINTENANCE_SYSTEM_PROMPT = `You are maintaining an existing persistent memory bucket for a coding assistant.
|
|
26
|
+
|
|
27
|
+
Your job:
|
|
28
|
+
1. Merge duplicates and near-duplicates.
|
|
29
|
+
2. Summarize clusters into fewer, higher-signal memories.
|
|
30
|
+
3. Remove stale, contradictory, trivial, or overly specific noise.
|
|
31
|
+
4. Preserve important exact commands, file paths, preferences, and constraints.
|
|
32
|
+
5. Keep memories scoped exactly to the bucket you receive.
|
|
33
|
+
|
|
34
|
+
Respond with valid JSON only, no markdown fences:
|
|
35
|
+
{"items":[{"kind":"preference|workflow|pattern|observation|correction|decision|failure|architecture|module|note","content":"durable memory text","summary":"under 80 chars","confidence":0.5,"pinned":false,"lifecycle":"longterm|operational"}],"archives":[{"source_ids":["mem_..."],"reason":"merged|stale|duplicate|noise|contradiction"}]}
|
|
36
|
+
|
|
37
|
+
Rules:
|
|
38
|
+
- Prefer fewer, clearer items, but do not collapse unrelated facts.
|
|
39
|
+
- User preferences belong in user memory and should not become project rules.
|
|
40
|
+
- Project conventions belong in project memory and should not become user preferences.
|
|
41
|
+
- Global memory is only for reusable cross-project/tool/environment knowledge.
|
|
42
|
+
- If a pinned item is still valid, keep it.
|
|
43
|
+
- Return at least one item if the input has useful durable content.`;
|
|
44
|
+
|
|
25
45
|
function parseResults(text) {
|
|
26
46
|
try {
|
|
27
47
|
const json = JSON.parse(text);
|
|
@@ -97,3 +117,69 @@ export async function evaluateInboxBatch({ entries, config, workspaceRoot }) {
|
|
|
97
117
|
}));
|
|
98
118
|
}
|
|
99
119
|
}
|
|
120
|
+
|
|
121
|
+
function parseMaintenanceResult(text) {
|
|
122
|
+
try {
|
|
123
|
+
const json = JSON.parse(text);
|
|
124
|
+
const items = Array.isArray(json?.items) ? json.items : [];
|
|
125
|
+
const archives = Array.isArray(json?.archives) ? json.archives : [];
|
|
126
|
+
return {
|
|
127
|
+
items: items
|
|
128
|
+
.map((item) => ({
|
|
129
|
+
kind: String(item.kind || 'note').slice(0, 40),
|
|
130
|
+
content: String(item.content || '').slice(0, 600),
|
|
131
|
+
summary: String(item.summary || item.content || '').slice(0, 120),
|
|
132
|
+
confidence: Math.min(1, Math.max(0.5, Number(item.confidence) || 0.8)),
|
|
133
|
+
pinned: item.pinned === true,
|
|
134
|
+
lifecycle: ['longterm', 'operational'].includes(String(item.lifecycle || '')) ? String(item.lifecycle) : undefined
|
|
135
|
+
}))
|
|
136
|
+
.filter((item) => item.content.trim()),
|
|
137
|
+
archives: archives.map((archive) => ({
|
|
138
|
+
source_ids: Array.isArray(archive.source_ids) ? archive.source_ids.map((id) => String(id)).filter(Boolean) : [],
|
|
139
|
+
reason: String(archive.reason || '').slice(0, 160)
|
|
140
|
+
}))
|
|
141
|
+
};
|
|
142
|
+
} catch {
|
|
143
|
+
return { items: [], archives: [] };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function evaluateMemoryMaintenance({ scope, items, config, workspaceRoot }) {
|
|
148
|
+
const sourceItems = Array.isArray(items) ? items : [];
|
|
149
|
+
if (sourceItems.length === 0) return { items: [], archives: [] };
|
|
150
|
+
|
|
151
|
+
const compactItems = sourceItems.map((item) => ({
|
|
152
|
+
id: item.id,
|
|
153
|
+
kind: item.kind,
|
|
154
|
+
content: String(item.content || '').slice(0, 600),
|
|
155
|
+
summary: String(item.summary || '').slice(0, 160),
|
|
156
|
+
confidence: item.confidence,
|
|
157
|
+
pinned: item.pinned === true,
|
|
158
|
+
lifecycle: item.lifecycle || ''
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const result = await createChatCompletion({
|
|
163
|
+
sdkProvider: config?.sdk?.provider,
|
|
164
|
+
baseUrl: config?.gateway?.base_url,
|
|
165
|
+
apiKey: config?.gateway?.api_key,
|
|
166
|
+
model: config?.model?.name,
|
|
167
|
+
messages: [
|
|
168
|
+
{ role: 'system', content: MAINTENANCE_SYSTEM_PROMPT },
|
|
169
|
+
{
|
|
170
|
+
role: 'user',
|
|
171
|
+
content: `Maintain this ${scope} memory bucket. Workspace: ${workspaceRoot || process.cwd()}\n\n${JSON.stringify(compactItems, null, 2)}`
|
|
172
|
+
}
|
|
173
|
+
],
|
|
174
|
+
temperature: 0,
|
|
175
|
+
timeoutMs: EVAL_TIMEOUT_MS
|
|
176
|
+
});
|
|
177
|
+
return parseMaintenanceResult(result?.text || '');
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return {
|
|
180
|
+
items: sourceItems,
|
|
181
|
+
archives: [],
|
|
182
|
+
error: String(error?.message || error || 'memory maintenance failed')
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|