chore: version up to 2.80.35 and package with experience memory
This commit is contained in:
@@ -34,6 +34,8 @@ import { TelegramBot } from './integrations/telegram/telegramBot';
|
||||
import { AIService } from './core/services';
|
||||
import { SettingsPanelProvider } from './features/settings/settingsPanelProvider';
|
||||
import { resolveScopeForAgent, openKnowledgeMapEditor } from './skills/agentKnowledgeMap';
|
||||
import { getBrainTokenIndex } from './retrieval';
|
||||
import { lessonTemplate, lessonSlug, parseLessonFrontmatter, normalizeLessonTitle, bumpLessonOccurrences } from './retrieval/lessonHelpers';
|
||||
import { retrieveScoped, buildContextBlock } from './skills/scopedBrainRetriever';
|
||||
|
||||
let _lifecycleManager: ModelLifecycleManager | undefined;
|
||||
@@ -404,8 +406,151 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
vscode.commands.registerCommand('g1nation.skills.editKnowledgeMap', async () => {
|
||||
await openKnowledgeMapEditor();
|
||||
}),
|
||||
// Experience Memory — create / browse lesson cards in the active brain.
|
||||
vscode.commands.registerCommand('g1nation.lesson.create', () => createLessonCard()),
|
||||
vscode.commands.registerCommand('g1nation.lesson.fromConversation', () => {
|
||||
// Pre-fill the Situation section from the most recent user request + assistant reply.
|
||||
const history = agent.getHistory().filter((m: any) => !m.internal);
|
||||
const lastUser = [...history].reverse().find((m: any) => m.role === 'user');
|
||||
const lastAssistant = [...history].reverse().find((m: any) => m.role === 'assistant');
|
||||
if (!lastUser && !lastAssistant) {
|
||||
vscode.window.showInformationMessage('현재 대화 내용이 없습니다. 먼저 대화를 한 뒤 사용하세요. (빈 교훈을 만들려면 "Astra: New Lesson" 사용)');
|
||||
return;
|
||||
}
|
||||
const clip = (s: any, n: number) => { const t = String(s || '').replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n) + '…' : t; };
|
||||
const situation = [
|
||||
lastUser ? `요청: ${clip(lastUser.content, 600)}` : '',
|
||||
lastAssistant ? `Astra 답변(요약): ${clip(lastAssistant.content, 800)}` : '',
|
||||
'',
|
||||
'<위 작업에서 무엇이 잘못됐거나 위험했는지를 아래 Mistake/Root Cause 에 적으세요>',
|
||||
].filter(Boolean).join('\n');
|
||||
return createLessonCard(situation);
|
||||
}),
|
||||
vscode.commands.registerCommand('g1nation.lesson.manage', () => manageLessons()),
|
||||
);
|
||||
|
||||
/** All lesson/playbook/qa-finding cards in the active brain. Uses the brain index for the lesson-kind
|
||||
* filter (cheap when warm), then reads only those few files for their frontmatter title/occurrences. */
|
||||
function listLessonFiles(brainDir: string): Array<{ filePath: string; rel: string; title: string; kind: string; occurrences: number }> {
|
||||
const out: Array<{ filePath: string; rel: string; title: string; kind: string; occurrences: number }> = [];
|
||||
let files: string[] = [];
|
||||
try { files = findBrainFiles(brainDir); } catch { return out; }
|
||||
for (const d of getBrainTokenIndex(brainDir, files)) {
|
||||
if (!d.kind) continue;
|
||||
let content = '';
|
||||
try { content = fs.readFileSync(d.filePath, 'utf8').slice(0, 4000); } catch { continue; }
|
||||
const fm = parseLessonFrontmatter(content);
|
||||
out.push({ filePath: d.filePath, rel: d.relativePath, title: (fm.title || d.title).trim(), kind: d.kind, occurrences: fm.occurrences ?? 1 });
|
||||
}
|
||||
return out.sort((a, b) => a.rel.localeCompare(b.rel));
|
||||
}
|
||||
|
||||
/** Shared lesson-card creator used by the lesson commands. Dedup-merges into an existing lesson with the same title. */
|
||||
async function createLessonCard(situation?: string): Promise<void> {
|
||||
const brain = getActiveBrainProfile();
|
||||
const brainDir = brain?.localBrainPath;
|
||||
if (!brainDir || !path.isAbsolute(brainDir)) {
|
||||
vscode.window.showErrorMessage('활성 Second Brain 경로가 설정되지 않았습니다. Settings에서 localBrainPath / brainProfiles를 먼저 설정하세요.');
|
||||
return;
|
||||
}
|
||||
const title = (await vscode.window.showInputBox({
|
||||
title: 'New Lesson — Experience Memory',
|
||||
prompt: '이 교훈의 제목 (예: "Telegram 원격 실행은 allowlist 필수")',
|
||||
placeHolder: '한 줄 요약 — 다음에 같은 실수를 안 하려면 뭘 기억해야 하나',
|
||||
ignoreFocusOut: true,
|
||||
}))?.trim();
|
||||
if (!title) return;
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
// Dedup-merge: a recurring mistake should get LOUDER (occurrences++), not spawn a duplicate card.
|
||||
const norm = normalizeLessonTitle(title);
|
||||
const existing = norm ? listLessonFiles(brainDir).find((l) => normalizeLessonTitle(l.title) === norm) : undefined;
|
||||
if (existing) {
|
||||
const pick = await vscode.window.showInformationMessage(
|
||||
`이미 같은 제목의 교훈이 있습니다: "${existing.title}" (occurrences: ${existing.occurrences}). 갱신할까요?`,
|
||||
{ modal: false },
|
||||
'갱신 (occurrences +1)', '새로 만들기',
|
||||
);
|
||||
if (!pick) return;
|
||||
if (pick === '갱신 (occurrences +1)') {
|
||||
try {
|
||||
const cur = fs.readFileSync(existing.filePath, 'utf8');
|
||||
fs.writeFileSync(existing.filePath, bumpLessonOccurrences(cur, today), 'utf8');
|
||||
} catch (e: any) {
|
||||
vscode.window.showErrorMessage(`교훈 갱신 실패: ${e?.message ?? e}`);
|
||||
return;
|
||||
}
|
||||
const doc = await vscode.workspace.openTextDocument(existing.filePath);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
vscode.window.showInformationMessage(`기존 교훈을 갱신했습니다 (occurrences: ${existing.occurrences + 1}). 필요하면 내용을 보강하세요.`);
|
||||
return;
|
||||
}
|
||||
// else fall through and create a new one
|
||||
}
|
||||
|
||||
const dir = path.join(brainDir, 'lessons');
|
||||
try { fs.mkdirSync(dir, { recursive: true }); } catch { /* fall through to error below */ }
|
||||
let filePath = path.join(dir, `${today}-${lessonSlug(title)}.md`);
|
||||
let n = 2;
|
||||
while (fs.existsSync(filePath)) { filePath = path.join(dir, `${today}-${lessonSlug(title)}-${n++}.md`); }
|
||||
try {
|
||||
fs.writeFileSync(filePath, lessonTemplate(title, today, situation), 'utf8');
|
||||
} catch (e: any) {
|
||||
vscode.window.showErrorMessage(`교훈 파일 생성 실패: ${e?.message ?? e}`);
|
||||
return;
|
||||
}
|
||||
const doc = await vscode.workspace.openTextDocument(filePath);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
vscode.window.showInformationMessage('교훈 카드를 만들었습니다. 내용을 채워 저장하면 다음 유사 작업 시 자동으로 체크리스트에 들어갑니다.');
|
||||
}
|
||||
|
||||
/** Browse lesson cards: open one, or delete one (trash button). Also the "manage" surface for ignoring bad lessons. */
|
||||
async function manageLessons(): Promise<void> {
|
||||
const brain = getActiveBrainProfile();
|
||||
const brainDir = brain?.localBrainPath;
|
||||
if (!brainDir || !path.isAbsolute(brainDir)) {
|
||||
vscode.window.showErrorMessage('활성 Second Brain 경로가 설정되지 않았습니다.');
|
||||
return;
|
||||
}
|
||||
const lessons = listLessonFiles(brainDir);
|
||||
if (lessons.length === 0) {
|
||||
const make = await vscode.window.showInformationMessage('아직 교훈 카드가 없습니다.', '새 교훈 만들기');
|
||||
if (make) await createLessonCard();
|
||||
return;
|
||||
}
|
||||
const deleteBtn: vscode.QuickInputButton = { iconPath: new vscode.ThemeIcon('trash'), tooltip: '이 교훈 삭제' };
|
||||
const qp = vscode.window.createQuickPick<vscode.QuickPickItem & { _file: string }>();
|
||||
qp.title = 'Lessons — Experience Memory';
|
||||
qp.placeholder = '교훈을 선택하면 열립니다. 휴지통 아이콘으로 삭제. (삭제 = 더 이상 주입 안 됨)';
|
||||
qp.items = lessons.map((l) => ({
|
||||
label: `$(${l.kind === 'playbook' ? 'book' : l.kind === 'qa-finding' ? 'bug' : 'lightbulb'}) ${l.title}`,
|
||||
description: l.occurrences > 1 ? `×${l.occurrences}` : '',
|
||||
detail: l.rel,
|
||||
buttons: [deleteBtn],
|
||||
_file: l.filePath,
|
||||
}));
|
||||
qp.onDidTriggerItemButton(async (e) => {
|
||||
const file = e.item._file;
|
||||
const ok = await vscode.window.showWarningMessage(`교훈 "${e.item.label}" 을(를) 삭제할까요?`, { modal: true }, '삭제');
|
||||
if (ok === '삭제') {
|
||||
try { fs.unlinkSync(file); } catch (err: any) { vscode.window.showErrorMessage(`삭제 실패: ${err?.message ?? err}`); return; }
|
||||
qp.items = qp.items.filter((it) => it._file !== file);
|
||||
vscode.window.showInformationMessage('교훈을 삭제했습니다.');
|
||||
if (qp.items.length === 0) qp.hide();
|
||||
}
|
||||
});
|
||||
qp.onDidAccept(async () => {
|
||||
const sel = qp.selectedItems[0];
|
||||
qp.hide();
|
||||
if (sel) {
|
||||
const doc = await vscode.workspace.openTextDocument(sel._file);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
}
|
||||
});
|
||||
qp.onDidHide(() => qp.dispose());
|
||||
qp.show();
|
||||
}
|
||||
|
||||
// Astra Settings webview — single entry point for user-facing config (Phase 5-A: Telegram only).
|
||||
const settingsPanel = new SettingsPanelProvider({
|
||||
extensionUri: context.extensionUri,
|
||||
|
||||
Reference in New Issue
Block a user