Compare commits

...

2 Commits

Author SHA1 Message Date
koriweb 7d6b8b509f v2.2.311~312: 응답 지연 근본 개선(KV 캐시 분리·제2뇌 상주 캐시) + 보고 품질 수술
v2.2.311 — 응답 지연 (실측: 출력 22토큰에 94.7초, 원인은 매 턴 13k+ 토큰 전체 재프리필)
- KV 캐시 친화 프롬프트 분리(kvCachePromptSplit, 기본 ON): message[0]을 불변
  정적 본문으로 고정, 날짜/RAG/[CONTEXT]/동적 블록을 마지막 user 메시지 직전의
  internal system 메시지로 이동 — llama.cpp prompt cache 프리픽스 재사용으로
  턴당 재프리필을 "직전 교환 + 동적 컨텍스트"로 축소. truncation 도 tail 적용.
- 검색 토큰 예산 현실화(retrievalTokenBudget, 0=자동): 창의 25%(8k~80k) →
  12%(2.5k~6k 클램프).
- continuation 은 depth-0 memoryCtx 재사용: 라운드당 재검색 3~8초 제거 +
  빈 쿼리 재검색으로 청크가 갈리던 문제 제거 + 턴 내 프롬프트 안정화.
- 제2뇌 상주 캐시(신규 brainWatch.ts): 재귀 fs.watch 세대 카운터로 변경 없으면
  디렉터리 워크·파일별 statSync 전면 생략. 수정 직후 3초 창은 신뢰 제외(이벤트
  지연 레이스 가드), 워처 불가 시 종전 폴백, 활성화 시 백그라운드 워밍,
  유휴 해제 30분→2시간.

v2.2.312 — 보고 품질 (실사례: "## 4."부터 시작하는 7줄 일반론 보고서)
- 중간 라운드 본문 표시 버그 수정: 액션과 함께 작성된 섹션(1~3)이 화면에 한 번도
  안 나가고 최종 라운드만 표시되던 근본 원인 제거 — stripForDisplay.ts 로 액션
  태그만 걷어내고 라운드 순서대로 버블에 표시.
- '분석 보고' 업무 유형 신설(requirementGraph): 보고 개요(첫 줄 자기선언)·파일
  근거(주장마다 실제 읽은 파일 인용, 일반론 금지)·구조·발견·다음 단계 강제,
  유형 감지 시 "최대 3섹션" 규칙보다 필수 요소 커버 우선.
- 근거 없는 분석 감지: 파일을 읽고도 인용 2개 미만인 장문 분석에 "근거 인용
  없음" footer 경고 (헛조사 감지의 반대 방향).

검증: 전체 테스트 954건 통과(신규 28), tsc 무오류, vsix 패키징·설치 확인.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 13:21:41 +09:00
koriweb a31d273bfe chore(records): .astra 런타임 자산 + ConnectAI 기록 동기화 (테스트 실행분·대화 기록)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 13:21:15 +09:00
35 changed files with 2013 additions and 558 deletions
+94 -94
View File
@@ -3,15 +3,15 @@
<!-- ASTRA:AUTO-START --> <!-- ASTRA:AUTO-START -->
## Snapshot ## Snapshot
- **Workspace**: `connectai` `v2.2.248` _(absolute path varies by environment; resolved from the active VS Code workspace)_ - **Workspace**: `connectai` `v2.2.312` _(absolute path varies by environment; resolved from the active VS Code workspace)_
- **Description**: The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making. - **Description**: The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.
- **Stack**: TypeScript, Node.js, VS Code Extension, LM Studio SDK, Test runner - **Stack**: TypeScript, Node.js, VS Code Extension, LM Studio SDK, Test runner
- **Stats**: 560 source files, ~83,596 lines across 5 top-level modules. - **Stats**: 617 source files, ~90,403 lines across 5 top-level modules.
## Last Refresh ## Last Refresh
- **Time**: 2026-06-15T02:56:35.947Z - **Time**: 2026-07-20T04:20:03.384Z
- **Files newly analysed**: 1 - **Files newly analysed**: 1
- **Files reused from cache**: 559 - **Files reused from cache**: 616
## Directory Map ## Directory Map
```mermaid ```mermaid
@@ -19,11 +19,11 @@ mindmap
root((connectai)) root((connectai))
src/ src/
features/ features/
sidebar/
lib/ lib/
agent/ agent/
intelligence/ sidebar/
retrieval/ retrieval/
intelligence/
media/ media/
tests/ tests/
helpers/ helpers/
@@ -41,11 +41,11 @@ mindmap
> Arrows: which top-level module imports from which. > Arrows: which top-level module imports from which.
```mermaid ```mermaid
flowchart LR flowchart LR
src["src/<br/>317 files"] src["src/<br/>342 files"]
media["media/<br/>6 files"] media["media/<br/>6 files"]
tests["tests/<br/>58 files"] tests["tests/<br/>86 files"]
core_py["core_py/<br/>6 files"] core_py["core_py/<br/>6 files"]
docs["docs/<br/>173 files"] docs["docs/<br/>177 files"]
tests --> src tests --> src
``` ```
@@ -57,71 +57,71 @@ flowchart LR
## Hub Files ## Hub Files
> Imported by many other files — touching these has wide blast radius. > Imported by many other files — touching these has wide blast radius.
- `src/utils.ts` — referenced by **102** files - `src/utils.ts` — referenced by **116** files
- `src/config.ts` — referenced by **38** files - `src/config.ts` — referenced by **44** files
- `src/agent.ts` — referenced by **34** files - `src/agent.ts` — referenced by **37** files
- `src/core/services.ts` — referenced by **17** files - `src/core/services.ts` — referenced by **23** files
- `src/features/company/index.ts` — referenced by **14** files · Public API for 1인 기업 모드. Consumers (sidebarProvider, chatHandlers, command handlers) import from this barrel so internal layout can move around without touching every call site. - `src/retrieval/brainIndex.ts` — referenced by **14** files · Brain Index — persistent, mtime-keyed tokenized cache of the Second Brain RAG 검색은 매 질의마다 브레인의 모든 .md 파일을 읽고 토크나이즈해서 TF-IDF 점수를 계산했습니다 — 파일 수가 많아지면 그게 병목입니다. 이 모듈은 <brainPath>/.astra/brain-index.json 에
- `src/features/company/types.ts` — referenced by **14** files · Type definitions for the 1인 기업 (One-Person Company) mode. The mode turns the user into a virtual CEO that dispatches work to a roster of specialist agents. Each turn produces a session directory conta - `src/agent/actions/types.ts` — referenced by **13** files
- `src/retrieval/brainIndex.ts` — referenced by **12** files · Brain Index — persistent, mtime-keyed tokenized cache of the Second Brain RAG 검색은 매 질의마다 브레인의 모든 .md 파일을 읽고 토크나이즈해서 TF-IDF 점수를 계산했습니다 — 파일 수가 많아지면 그게 병목입니다. 이 모듈은 <brainPath>/.astra/brain-index.json 에 - `src/integrations/telegram/telegramClient.ts` — referenced by **13** files
- `src/integrations/telegram/telegramClient.ts` — referenced by **12** files - `src/features/company/types.ts` — referenced by **13** files · Type definitions for the 1인 기업 (One-Person Company) mode. The mode turns the user into a virtual CEO that dispatches work to a roster of specialist agents. Each turn produces a session directory conta
## Modules ## Modules
### `src/` — 317 files, ~61,246 lines ### `src/` — 342 files, ~65,381 lines
**Sub-directories** **Sub-directories**
- `src/features/` (112) — Astra Office — public API. 다음 세션에서 추가될 OfficeSnapshot presenter / schema 도 같은 entry 로 노출 예정. 현재 노출: full webview panel H - `src/features/` (126) — 데일리 브리핑 워처 — 평일(월~금) KST 09:30 에 "오늘의 할 일"을 텔레그램으로 발송. 소스: 1. Google Calendar — iCal 캐시 새로고침 후 오늘 일정 (readCalendarEvents
- `src/sidebar/` (35) — Brain profile lifecycle 의 pure helpers — sidebarProvider 의 add/edit/delete 흐름에서 modal UI 와 config 쓰기를 제외한 데이터 변환 만 격리. 현 - `src/lib/` (37) — Astra Mode Architecture Context Builder.도: 사용자가 Astra 자체의 mode 디자인 (Guard vs Multi-Agent 가 별도 모드여야 하는지) 을 묻는 메타 질문에 답할
- `src/lib/` (33) — Astra Mode Architecture Context Builder. 의도: 사용자가 Astra 자체의 mode 디자인 (Guard vs Multi-Agent 가 별도 모드여야 하는지) 을 묻는 메타 질문에 답할 - `src/agent/` (36) — <investigatefiles path="..." focus="..."/> — 폴더(또는 단일 파일) 강제 정독 조사. listfiles 가 "이름 목록"을 주는 것과 달리, 이 액션은 대상 파일들을 코드가 직접
- `src/agent/` (31) — 한·영 깨진 토큰 감지·수리 — 소형 로컬 모델의 토큰 붕괴 보정. 증상: 한국어 단어 중간에 영문 토큰이 섞임 — "덩어리"→"덩ey", "결과적으로"→"결ently". 프롬프트 규칙([출력 위생])으로는 못 막는 - `src/sidebar/` (32) — Brain profile lifecycle 의 pure helpers — sidebarProvider 의 add/edit/delete 흐름에서 modal UI 와 config 쓰기를 제외한 데이터 변환 만 격리. 현
- `src/intelligence/` (18) — Confidence Engine답변 확신도 0~100 결정론적 산출. Self-Evolving OS 마스터 플랜 Phase 2 / Track 1-1. 신뢰 조건 T4 "확신이 없으면 사람에게 묻는다" 의 측정 - `src/retrieval/` (20) — Actionability Scoring검색 결과를 "현재 작업 상태" 신호로 재가중. 기존 TF-IDF (단어 매칭) + recency (시간) 만으로는 "지금 이 사용자가 하고 있는 작업과 직접 연결 된 문서
- `src/retrieval/` (18) — Actionability Scoring검색 결과를 "현재 작업 상태" 신호로 재가중. 기존 TF-IDF (단어 매칭) + recency (시간) 만으로는 "지금 이 사용자가 하고 있는 작업과 직접 연결 된 문서 - `src/intelligence/` (19) — Confidence Engine답변 확신도 0~100 결정론적 산출. Self-Evolving OS 마스터 플랜 Phase 2 / Track 1-1. 신뢰 조건 T4 "확신이 없으면 사람에게 묻는다" 의 측정
- `src/core/` (15) — Astra Path Resolver (경로 해결기) Astra의 모든 데이터 파일(.astra 디렉토리)의 경로를 중앙에서 관리합니다. 확장 프로그램의 설치 경로(extensionUri) 기반으로 .astra 디렉토 - `src/core/` (15) — Astra Path Resolver (경로 해결기) Astra의 모든 데이터 파일(.astra 디렉토리)의 경로를 중앙에서 관리합니다. 확장 프로그램의 설치 경로(extensionUri) 기반으로 .astra 디렉토
- `src/extension/` (13) — 두뇌(Second Brain) 기본 위치 부트스트랩 — 첫 실행 온보딩. 문제: 두뇌 미설정 시 config 가 ~/.g1nation-brain(숨김 점폴더)로 조용히 폴백했다. - 폴더가 실제로 생성되지 않고, 설 - `src/extension/` (13) — 두뇌(Second Brain) 기본 위치 부트스트랩 — 첫 실행 온보딩. 문제: 두뇌 미설정 시 config 가 ~/.g1nation-brain(숨김 점폴더)로 조용히 폴백했다. - 폴더가 실제로 생성되지 않고, 설
- `src/memory/` (9) — Distillation Loop — stale Episodic Memory → Long-Term "episode-digest" 승급. 배경: Episodic Memory 가 무한히 누적되면 검색 노이즈. 30일+ 지 - `src/memory/` (9) — Distillation Loop — stale Episodic Memory → Long-Term "episode-digest" 승급. 배경: Episodic Memory 가 무한히 누적되면 검색 노이즈. 30일+ 지
- `src/integrations/` (7) — Per-chat conversation history for the Telegram bot. Why this exists: the previous bot was stateless — every inbound mess
- `src/docs/` (6) — Bug: Edited agent.ts Edited agent.ts Edited agent.ts Edited agent.ts Edited agent.ts ... - `src/docs/` (6) — Bug: Edited agent.ts Edited agent.ts Edited agent.ts Edited agent.ts Edited agent.ts ...
- `src/integrations/` (6) — Per-chat conversation history for the Telegram bot. Why this exists: the previous bot was stateless — every inbound mess - `src/lmstudio/` (4) — [메모리·기동 개선] SDK 를 타입 전용으로만 import — 값 import 는 번들 로드(=확장 활성화) 시점에 SDK 모듈 초기화를 실행시킨다. 실제 모듈 로드는 getSdk() 첫 호출 때 require 로
- `src/lmstudio/` (4) — 4 files (.ts)
**Key files** **Key files**
- `src/utils.ts` (485 lines) - `src/utils.ts` (538 lines)
- `src/config.ts` (661 lines) - `src/config.ts` (727 lines)
- `src/agent.ts` (1677 lines) - `src/agent.ts` (1979 lines)
- `src/core/services.ts` (218 lines)
- `src/features/company/types.ts` (446 lines) — Type definitions for the 1인 기업 (One-Person Company) mode. The mode turns the user into a virtual CEO that dispatches work to a roster of specialist agents. Each turn produces a session directory conta - `src/features/company/types.ts` (446 lines) — Type definitions for the 1인 기업 (One-Person Company) mode. The mode turns the user into a virtual CEO that dispatches work to a roster of specialist agents. Each turn produces a session directory conta
- `src/core/services.ts` (176 lines) - `src/sidebarProvider.ts` (2992 lines)
- `src/sidebarProvider.ts` (3468 lines)
- `src/integrations/telegram/telegramClient.ts` (154 lines)
- `src/agent/actions/types.ts` (41 lines) - `src/agent/actions/types.ts` (41 lines)
- `src/lib/contextManager.ts` (278 lines) — Context Manager (컨텍스트 한계 관리) "context length = 132k" 는 "답변을 132k 토큰까지 생성해도 된다" 가 아닙니다. 시스템 프롬프트 + 대화 기록 + 입력 문서 + 생성될 답변 + 여유분 ≤ context length 이 모듈은 요청을 보내기 전에 입력 토큰을 추정하고, - 동적으로 출력 상한(maxTokens)을 계 - `src/integrations/telegram/telegramClient.ts` (154 lines)
- `src/retrieval/brainIndex.ts` (566 lines) — Brain Index — persistent, mtime-keyed tokenized cache of the Second Brain RAG 검색은 매 질의마다 브레인의 모든 .md 파일을 읽고 토크나이즈해서 TF-IDF 점수를 계산했습니다 — 파일 수가 많아지면 그게 병목입니다. 이 모듈은 <brainPath>/.astra/brain-index.json 에 - `src/lib/contextManager.ts` (296 lines) — Context Manager (컨텍스트 한계 관리) "context length = 132k" 는 "답변을 132k 토큰까지 생성해도 된다" 가 아닙니다. 시스템 프롬프트 + 대화 기록 + 입력 문서 + 생성될 답변 + 여유분 ≤ context length 이 모듈은 요청을 보내기 전에 입력 토큰을 추정하고, - 동적으로 출력 상한(maxTokens)을 계
- `src/retrieval/brainIndex.ts` (645 lines) — Brain Index — persistent, mtime-keyed tokenized cache of the Second Brain RAG 검색은 매 질의마다 브레인의 모든 .md 파일을 읽고 토크나이즈해서 TF-IDF 점수를 계산했습니다 — 파일 수가 많아지면 그게 병목입니다. 이 모듈은 <brainPath>/.astra/brain-index.json 에
- `src/features/datacollect/slashRouter.ts` (211 lines)
- `src/retrieval/scoring.ts` (541 lines) — Scoring Engine — TF-IDF + Bilingual Tokenizer 단순 includes() 키워드 매칭을 넘어서, TF-IDF 가중치 기반의 문서 스코어링을 제공합니다. 한국어/영어 양국어 토크나이저를 포함합니다. - `src/retrieval/scoring.ts` (541 lines) — Scoring Engine — TF-IDF + Bilingual Tokenizer 단순 includes() 키워드 매칭을 넘어서, TF-IDF 가중치 기반의 문서 스코어링을 제공합니다. 한국어/영어 양국어 토크나이저를 포함합니다.
- `src/features/company/companyConfig.ts` (896 lines) — State + config plumbing for 1인 기업 모드. Two surfaces: - CompanyState (runtime data: enabled flag, company name, which agents are active, per-agent model overrides). Persisted in VS Code's globalState so - `src/features/company/companyConfig.ts` (896 lines) — State + config plumbing for 1인 기업 모드. Two surfaces: - CompanyState (runtime data: enabled flag, company name, which agents are active, per-agent model overrides). Persisted in VS Code's globalState so
- `src/features/stocks/types.ts` (61 lines) — Stocks 모듈 공유 타입. investresults/targetstocks.json 스키마를 그대로 받아서, ConnectAI 의 <workspace>/.astra/stocks.json 으로 옮긴 뒤 같은 필드명을 유지. 한글 필드명은 사용자의 도메인 데이터라 변경하지 않는다 — 마이그레이션 충돌 회피 + 사용자가 직접 JSON 편집할 때 frictio
- `src/lib/paths.ts` (151 lines) - `src/lib/paths.ts` (151 lines)
- `src/skills/agentKnowledgeMap.ts` (374 lines) - `src/skills/agentKnowledgeMap.ts` (374 lines)
- `src/features/datacollect/slashRouter.ts` (201 lines)
- `src/retrieval/types.ts` (66 lines) — Retrieval Types (검색 결과 통합 타입) 모든 검색 소스(Brain, Memory, Project, Episode)의 결과를 통합 인터페이스로 정의합니다. - `src/retrieval/types.ts` (66 lines) — Retrieval Types (검색 결과 통합 타입) 모든 검색 소스(Brain, Memory, Project, Episode)의 결과를 통합 인터페이스로 정의합니다.
- `src/intelligence/requirementGraph.ts` (273 lines) — Requirement Graph — 업무 유형별 필수 요소 정의 + 감지 + 커버리지 검사. Self-Evolving Digital Employee OS 마스터 플랜(docs/SELFEVOLVINGOSMASTERPLAN.md) Phase 1 / Track 2-1. 신뢰 조건 T3 "품질이 일관적이다 — 필수 요소 누락 없음" 담당. 동작 2단계: 1. In - `src/intelligence/requirementGraph.ts` (312 lines) — Requirement Graph — 업무 유형별 필수 요소 정의 + 감지 + 커버리지 검사. Self-Evolving Digital Employee OS 마스터 플랜(docs/SELFEVOLVINGOSMASTERPLAN.md) Phase 1 / Track 2-1. 신뢰 조건 T3 "품질이 일관적이다 — 필수 요소 누락 없음" 담당. 동작 2단계: 1. In
- `src/security.ts` (159 lines)
- `src/lib/contextBuilders/promptDetection.ts` (107 lines) — 사용자 prompt 의 의도 분류 류 detection helpers. 모두 stateless 정규식 매칭. 옛 코드는 agent.ts 의 private 메서드로 박혀 있었는데, system prompt 빌더 (buildJarvisProjectBriefContext 등) 가 이걸 의존하면서 god-file 안에서 서로 얽힘. 헬퍼만 먼저 떼면 의존 그래프가 - `src/lib/contextBuilders/promptDetection.ts` (107 lines) — 사용자 prompt 의 의도 분류 류 detection helpers. 모두 stateless 정규식 매칭. 옛 코드는 agent.ts 의 private 메서드로 박혀 있었는데, system prompt 빌더 (buildJarvisProjectBriefContext 등) 가 이걸 의존하면서 god-file 안에서 서로 얽힘. 헬퍼만 먼저 떼면 의존 그래프가
- `src/memory/types.ts` (151 lines) — Memory Type Definitions (메모리 타입 정의) Astra의 5-Layer Cognitive Memory System의 모든 타입을 정의합니다. ① Short-Term ② Long-Term ③ Project ④ Procedural ⑤ Episodic - `src/memory/types.ts` (151 lines) — Memory Type Definitions (메모리 타입 정의) Astra의 5-Layer Cognitive Memory System의 모든 타입을 정의합니다. ① Short-Term ② Long-Term ③ Project ④ Procedural ⑤ Episodic
- `src/features/stocks/types.ts` (53 lines) — Stocks 모듈 공유 타입. investresults/targetstocks.json 스키마를 그대로 받아서, ConnectAI 의 <workspace>/.astra/stocks.json 으로 옮긴 뒤 같은 필드명을 유지. 한글 필드명은 사용자의 도메인 데이터라 변경하지 않는다 — 마이그레이션 충돌 회피 + 사용자가 직접 JSON 편집할 때 frictio
- `src/retrieval/lessonHelpers.ts` (325 lines) — Lesson / Experience Memory — pure helpers (no vscode dependency) "Lesson" = a markdown file in the active brain that captures a past mistake/risk and how to avoid repeating it. Identified by a lessons
- `src/intelligence/confidenceEngine.ts` (165 lines) — Confidence Engine — 답변 확신도 0~100 결정론적 산출. Self-Evolving OS 마스터 플랜 Phase 2 / Track 1-1. 신뢰 조건 T4 "확신이 없으면 사람에게 묻는다" 의 측정 기반 — Escalation Engine 의 입력. 설계 원칙 (termValidator 와 동일): LLM 호출 없음. 검색 그라운딩 신호(턴
- `src/intelligence/reflectionStore.ts` (162 lines) — Reflection Store — 업무 turn 회고 기록 + Failure Pattern 집계. Self-Evolving OS 마스터 플랜 Phase 1 / Track 2-4 (Reflection Engine v1) + Phase 3 / Track 3-6 (Failure Pattern DB v1 시드). 신뢰 조건 T5 "같은 실수를 반복하지 않는다" 의
- `src/extension/telegramCommands.ts` (103 lines) - `src/extension/telegramCommands.ts` (103 lines)
- `src/security.ts` (159 lines) - `src/lib/execUtil.ts` (104 lines)
- `src/retrieval/lessonHelpers.ts` (325 lines) — Lesson / Experience Memory — pure helpers (no vscode dependency) "Lesson" = a markdown file in the active brain that captures a past mistake/risk and how to avoid repeating it. Identified by a lessons
- `src/features/calendar/calendarCache.ts` (271 lines) — Google Calendar (iCal) 캐시 — fetch + parse + 회사 shared/calendarcache.md 에 저장. Connectorigin 의 googlecalendar.py 를 TypeScript / native fetch 로 옮김. OAuth 없음. 사용자가 Google Calendar 설정 → "비공개 주소(iCal 형식)" 복
### `media/` — 6 files, ~7,836 lines ### `media/` — 6 files, ~7,582 lines
**Key files** **Key files**
- `media/sidebar.css` (2132 lines) — Stylesheet - `media/sidebar.css` (1957 lines) — Stylesheet
- `media/sidebar.js` (3966 lines) - `media/sidebar.js` (3767 lines)
- `media/sidebar.html` (539 lines) — Astra - `media/settings-panel.html` (541 lines) — Astra Settings
- `media/settings-panel.html` (440 lines) — Astra Settings - `media/sidebar.html` (486 lines) — Astra
- `media/settings-panel.js` (505 lines) - `media/settings-panel.js` (577 lines)
- `media/settings-panel.css` (254 lines) — Stylesheet - `media/settings-panel.css` (254 lines) — Stylesheet
### `tests/` — 58 files, ~8,276 lines ### `tests/` — 86 files, ~11,085 lines
*Depends on*: `src/` *Depends on*: `src/`
**Sub-directories** **Sub-directories**
@@ -132,29 +132,29 @@ flowchart LR
**Key files** **Key files**
- `tests/helpers/mockLLMClient.ts` (112 lines) — MockLLMClient — IAIService 의 Mock 구현체. 의도: 회사 모드 dispatcher / ChunkedWriter / ceoPlanner 등 LLM 을 호출하는 코드 경로를 CI 환경에서도 테스트 가능하게. 실제 Ollama / LM Studio 없이도 응답을 미리 정의하거나 동적으로 생성 가능. 사용 예: const ai = new - `tests/helpers/mockLLMClient.ts` (112 lines) — MockLLMClient — IAIService 의 Mock 구현체. 의도: 회사 모드 dispatcher / ChunkedWriter / ceoPlanner 등 LLM 을 호출하는 코드 경로를 CI 환경에서도 테스트 가능하게. 실제 Ollama / LM Studio 없이도 응답을 미리 정의하거나 동적으로 생성 가능. 사용 예: const ai = new
- `tests/agentEngine.test.ts` (413 lines) — AgentEngine Tests — Chunked Writer Architecture 예전 buildup(planner → researcher → reflector → writer → synthesizer)을 단일 ChunkedWriter 의 outline → section[N] → polish 로 교체한 뒤의 회귀 테스트. 다루는 범위: 1. ErrorC - `tests/agentEngine.test.ts` (413 lines) — AgentEngine Tests — Chunked Writer Architecture 예전 buildup(planner → researcher → reflector → writer → synthesizer)을 단일 ChunkedWriter 의 outline → section[N] → polish 로 교체한 뒤의 회귀 테스트. 다루는 범위: 1. ErrorC
- `tests/lmStudioLifecycle.test.ts` (330 lines) — Unit tests for ModelLifecycleManager. Strategy: inject mock ILMStudioClient and a simple in-memory IActivityTracker. No real LM Studio or SDK is touched — the manager file does not import the SDK dire - `tests/lmStudioLifecycle.test.ts` (334 lines) — Unit tests for ModelLifecycleManager. Strategy: inject mock ILMStudioClient and a simple in-memory IActivityTracker. No real LM Studio or SDK is touched — the manager file does not import the SDK dire
- `tests/localPathPreflight.test.ts` (520 lines) - `tests/localPathPreflight.test.ts` (520 lines)
- `tests/telegramBot.test.ts` (363 lines) — Unit tests for TelegramBot + truncateForTelegram. Strategy: - TelegramBot is driven by an injected ITelegramClient stub. We script getUpdates to return queued batches and assert that: - the offset cur - `tests/telegramBot.test.ts` (363 lines) — Unit tests for TelegramBot + truncateForTelegram. Strategy: - TelegramBot is driven by an injected ITelegramClient stub. We script getUpdates to return queued batches and assert that: - the offset cur
- `tests/criticReflectionEval.test.ts` (200 lines) — Critic Agent / Reflection Store / Task Eval Harness (Self-Evolving OS P1 잔여 + P3) 테스트. - `tests/criticReflectionEval.test.ts` (200 lines) — Critic Agent / Reflection Store / Task Eval Harness (Self-Evolving OS P1 잔여 + P3) 테스트.
- `tests/lmStudioStreamer.test.ts` (222 lines) — Unit tests for LMStudioStreamer. Strategy: inject a fake ILMStudioClient that returns a fake model handle whose respond() yields a controllable async iterable. No real SDK or WebSocket touched. - `tests/emailCommand.test.ts` (282 lines) — v2.2.269 — /email (Gmail + Outlook) 순수 로직 테스트. COM/네트워크는 통합 영역이라 제외 — 참조 파싱·프롬프트·트랜스크립트 컷만 고정.
- `tests/lmStudioStreamer.test.ts` (295 lines) — Unit tests for LMStudioStreamer. Strategy: inject a fake ILMStudioClient that returns a fake model handle whose respond() yields a controllable async iterable. No real SDK or WebSocket touched.
- `tests/meetRegistration.test.ts` (232 lines) — /meet 확신 게이트 — 분류·confirm 파싱·날짜 정규화 테스트. 정책: 확정+기한만 자동, 진행미정/기한미정/조건부는 보류, 반복은 첫 1회, 과거 날짜는 등록하되 완료확인 표기, 기한 해석 불가 확정건은 보류(추측 등록 금지).
- `tests/secondBrainTrace.test.ts` (407 lines) - `tests/secondBrainTrace.test.ts` (407 lines)
- `tests/approvalQueue.test.ts` (164 lines) — Unit tests for ApprovalQueue. Strategy: drive enqueue → approve / reject / clear / pre-empt directly, confirm the onChange event fires at the right moments and callbacks fire exactly once. - `tests/approvalQueue.test.ts` (164 lines) — Unit tests for ApprovalQueue. Strategy: drive enqueue → approve / reject / clear / pre-empt directly, confirm the onChange event fires at the right moments and callbacks fire exactly once.
- `tests/compositeScore.test.ts` (131 lines) — 멀티팩터 합성점수 (v2.2.266) — 백분위 랭크·축 합성·value trap 가드·결측 처리.
- `tests/computeBudgetedRequest.test.ts` (147 lines) — Phase 1 — context-window alignment. The budgeter must clamp to the model's ACTUALLY-loaded window when it's smaller than the user's contextLength setting, so a model loaded with a smaller window than
- `tests/confidenceEngine.test.ts` (175 lines) — Confidence Engine + Escalation Engine (Self-Evolving OS Phase 2) 단위 테스트. 순수 함수만 검증 — vscode 의존 없음. - `tests/confidenceEngine.test.ts` (175 lines) — Confidence Engine + Escalation Engine (Self-Evolving OS Phase 2) 단위 테스트. 순수 함수만 검증 — vscode 의존 없음.
- `tests/correctionLoop.test.ts` (158 lines) — Correction Loop 단위 테스트 — 순수 로직 (감지·프로필·레슨·큐 등록·영속화). LLM 의존 부분(classifyCorrection)은 엔드포인트 실패 → 휴리스틱 fallback 경로만 검증. - `tests/correctionLoop.test.ts` (158 lines) — Correction Loop 단위 테스트 — 순수 로직 (감지·프로필·레슨·큐 등록·영속화). LLM 의존 부분(classifyCorrection)은 엔드포인트 실패 → 휴리스틱 fallback 경로만 검증.
- `tests/emailState.test.ts` (121 lines) — v2.2.271 — 이메일 영속 상태 머신 (syncThreads / detectOverdue / setThreadState). "명령 안 쳐도 f/u 되는" 핵심 로직: 창 밖 미회신 보존, done 자동 재개, 알림 중복 차단.
- `tests/investigationPipeline.test.ts` (193 lines) — Investigation Pipeline — 파일명만 보고 상상하는 헛조사 차단 3중 방어. (v2.2.309) ① 조사형 요청 감지 + 증거 게이트 ② 강제 정독 Map + 해시 캐시 ③ Hollow 감지
- `tests/knowledgeOps.test.ts` (174 lines) — Knowledge Validation / Belief Revision / Decay / Debt (Self-Evolving OS Phase 4 — 지식 운영) 테스트. - `tests/knowledgeOps.test.ts` (174 lines) — Knowledge Validation / Belief Revision / Decay / Debt (Self-Evolving OS Phase 4 — 지식 운영) 테스트.
- `tests/meetRegistration.test.ts` (110 lines) — /meet 확신 게이트 — 분류·confirm 파싱·날짜 정규화 테스트. 정책: 확정+기한만 자동, 진행미정/기한미정/조건부는 보류, 반복은 첫 1회, 과거 날짜는 등록하되 완료확인 표기, 기한 해석 불가 확정건은 보류(추측 등록 금지). - `tests/largeInputMapReduce.test.ts` (159 lines) — Phase 2 — large-input map-reduce core. Pure orchestration with an injected callLLM, so no network / SDK is touched.
- `tests/needEngineQueue.test.ts` (159 lines) — Gap Detector / Need Engine / Knowledge Inventory / Learning Queue (Self-Evolving OS Phase 3 — 성장 루프 코어) 테스트. - `tests/needEngineQueue.test.ts` (159 lines) — Gap Detector / Need Engine / Knowledge Inventory / Learning Queue (Self-Evolving OS Phase 3 — 성장 루프 코어) 테스트.
- `tests/projectScaffolder.test.ts` (135 lines) — Unit tests for FileSystemProjectScaffolder. Drives against a real temp directory so end-to-end file IO + path-traversal defenses are exercised. - `tests/projectScaffolder.test.ts` (135 lines) — Unit tests for FileSystemProjectScaffolder. Drives against a real temp directory so end-to-end file IO + path-traversal defenses are exercised.
- `tests/requirementGraph.test.ts` (126 lines) — Requirement Graph (Self-Evolving OS Phase 1 / Track 2-1) 단위 테스트. 순수 함수만 검증 — vscode 의존 없음. - `tests/requirementGraph.test.ts` (146 lines) — Requirement Graph (Self-Evolving OS Phase 1 / Track 2-1) 단위 테스트. 순수 함수만 검증 — vscode 의존 없음.
- `tests/researchSkill.test.ts` (122 lines) — Research Agent / Skill Score / Success Pattern DB (Self-Evolving OS Phase 6) 테스트. - `tests/researchSkill.test.ts` (122 lines) — Research Agent / Skill Score / Success Pattern DB (Self-Evolving OS Phase 6) 테스트.
- `tests/resilience_stress.test.ts` (197 lines) — Resilience & Boundary Stress Test Suite (v2.77.3) 이 테스트는 ConnectAI 엔진이 극한의 환경(인증 실패, 네트워크 차단, 타임아웃 등)에서 얼마나 안정적으로 복구되고, 신뢰성 지표(Resilience Metrics)를 정확히 기록하는지 검증합니다. - `tests/resilience_stress.test.ts` (197 lines) — Resilience & Boundary Stress Test Suite (v2.77.3) 이 테스트는 ConnectAI 엔진이 극한의 환경(인증 실패, 네트워크 차단, 타임아웃 등)에서 얼마나 안정적으로 복구되고, 신뢰성 지표(Resilience Metrics)를 정확히 기록하는지 검증합니다.
- `tests/retrievalEvalEmbedding.test.ts` (106 lines) — 하이브리드(sparse+dense) 검색 측정 — 청크 TF-IDF vs 청크+임베딩 (alpha sweep). 평소 테스트 런에서는 skip (실제 두뇌 + 로컬 임베딩 서버 필요). 수동 실행: ASTRAEVALBRAIN="E:/Wiki/2nd/10Wiki/Topics" \ ASTRAEVALEMBEDMODEL="text-embedding-nomic-em - `tests/retrievalEvalEmbedding.test.ts` (106 lines) — 하이브리드(sparse+dense) 검색 측정 — 청크 TF-IDF vs 청크+임베딩 (alpha sweep). 평소 테스트 런에서는 skip (실제 두뇌 + 로컬 임베딩 서버 필요). 수동 실행: ASTRAEVALBRAIN="E:/Wiki/2nd/10Wiki/Topics" \ ASTRAEVALEMBEDMODEL="text-embedding-nomic-em
- `tests/skillInjectionService.test.ts` (172 lines) — Unit tests for FileSystemSkillInjectionService. Strategy: drive the service against a real temp directory so path-traversal defenses and writeFileSync paths are exercised end-to-end. The service accep
- `tests/sleepDigest.test.ts` (101 lines) — Sleep-time 사전 소화 — 순수 로직 테스트 (대상 선정·노후화 판정·노트 형식). LLM 호출(runSleepDigestOnce)은 제외 — 통합 검증은 수동 명령으로.
- `tests/stocksCriteria.test.ts` (129 lines) — criteriaEval — /stocks judge 결정론 평가기 테스트. 픽스처는 옛 LLM 프롬프트에 명시돼 있던 사용자의 실제 분류 예시 3종 (마녀공장/기가비스/엔켐) — 코드 판정이 사용자 패턴과 일치해야 한다.
- `tests/alignmentResearch.test.ts` (208 lines)
- `tests/conflictCheck.test.ts` (65 lines) — Schedule Conflict Check (Self-Evolving OS Track 6-2/6-3) 테스트.
- `tests/dataProcessor.test.ts` (87 lines) — / <reference types="jest" />
### `core_py/` — 6 files, ~409 lines ### `core_py/` — 6 files, ~409 lines
@@ -166,16 +166,16 @@ flowchart LR
- `core_py/optimizer.py` (55 lines) - `core_py/optimizer.py` (55 lines)
- `core_py/queue_worker.py` (82 lines) - `core_py/queue_worker.py` (82 lines)
### `docs/` — 173 files, ~5,829 lines ### `docs/` — 177 files, ~5,946 lines
**Sub-directories** **Sub-directories**
- `docs/records/` (157) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 프로젝트 코드 리뷰 해줄 수 있어? 개선할 부분이 있는지, 그러고... - `docs/records/` (160) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 프로젝트 코드 리뷰 해줄 수 있어? 개선할 부분이 있는지, 그러고...
- `docs/docs/` (5) — Bug: Viewed integrationretrieval.test.ts:1-59 integrationretrieval.test.ts를 통해 ... - `docs/docs/` (5) — Bug: Viewed integrationretrieval.test.ts:1-59 integrationretrieval.test.ts를 통해 ...
- `docs/plans/` (2) — Alignment Self-Learning 개선 계획 (v2 — 적대적 리뷰 반영) - `docs/plans/` (2) — Alignment Self-Learning 개선 계획 (v2 — 적대적 리뷰 반영)
- `docs/Meeting/` (0) - `docs/Meeting/` (0)
**Key files** **Key files**
- `docs/records/ConnectAI/timeline.md` (422 lines) — Project Timeline - `docs/records/ConnectAI/timeline.md` (431 lines) — Project Timeline
- `docs/TELEGRAM_REMOTE_EXECUTION_PLAN.md` (452 lines) — Telegram Remote Execution 기획서 - `docs/TELEGRAM_REMOTE_EXECUTION_PLAN.md` (452 lines) — Telegram Remote Execution 기획서
- `docs/AgentEngine_Architecture.md` (314 lines) — AgentEngine Architecture Document - `docs/AgentEngine_Architecture.md` (314 lines) — AgentEngine Architecture Document
- `docs/SELF_EVOLVING_OS_MASTER_PLAN.md` (275 lines) — ASTRA Self-Evolving Digital Employee OS — 마스터 개발 계획 v1.1 - `docs/SELF_EVOLVING_OS_MASTER_PLAN.md` (275 lines) — ASTRA Self-Evolving Digital Employee OS — 마스터 개발 계획 v1.1
@@ -209,6 +209,7 @@ flowchart LR
- `g1nation.eval.retrieval` — Astra: 검색 평가 실행 (recall@k / MRR) - `g1nation.eval.retrieval` — Astra: 검색 평가 실행 (recall@k / MRR)
- `g1nation.eval.tasks` — Astra: 업무 평가 실행 (회의록 골든셋) - `g1nation.eval.tasks` — Astra: 업무 평가 실행 (회의록 골든셋)
- `g1nation.growth.report` — Astra: 성장 리포트 (Reflection 추이) - `g1nation.growth.report` — Astra: 성장 리포트 (Reflection 추이)
- `g1nation.growth.standingRules` — Astra: 상시 행동 규칙 (Standing Rules)
- `g1nation.growth.learningQueue` — Astra: 학습 큐 갱신 (Need Engine) - `g1nation.growth.learningQueue` — Astra: 학습 큐 갱신 (Need Engine)
- `g1nation.knowledge.decayAudit` — Astra: 지식 노후 점검 (Knowledge Decay) - `g1nation.knowledge.decayAudit` — Astra: 지식 노후 점검 (Knowledge Decay)
- `g1nation.research.runQueue` — Astra: 학습 실행 (Research Agent — 승인된 큐 항목) - `g1nation.research.runQueue` — Astra: 학습 실행 (Research Agent — 승인된 큐 항목)
@@ -239,17 +240,21 @@ flowchart LR
- `g1nation.company.toggle` — Astra: Toggle 1인 기업 Mode - `g1nation.company.toggle` — Astra: Toggle 1인 기업 Mode
- `g1nation.company.manage` — Astra: Manage 1인 기업 Agents - `g1nation.company.manage` — Astra: Manage 1인 기업 Agents
- `g1nation.company.openSessions` — Astra: Open 1인 기업 Sessions Folder - `g1nation.company.openSessions` — Astra: Open 1인 기업 Sessions Folder
- `g1nation.company.pixelOffice.open` — Astra: Open Pixel Office (Full Screen)
- `g1nation.calendar.connect` — Astra: Google Calendar (iCal) 연결 📅 - `g1nation.calendar.connect` — Astra: Google Calendar (iCal) 연결 📅
- `g1nation.calendar.refresh` — Astra: Google Calendar 새로고침 📅 - `g1nation.calendar.refresh` — Astra: Google Calendar 새로고침 📅
- `g1nation.calendar.connectOAuth` — Astra: Google Calendar OAuth 연결 (쓰기) 🔐 - `g1nation.calendar.connectOAuth` — Astra: Google Calendar OAuth 연결 (쓰기) 🔐
- `g1nation.devilAgent.toggle` — Astra: Toggle Devil Agent 🎭 - `g1nation.devilAgent.toggle` — Astra: Toggle Devil Agent 🎭
- **Configuration** (149 settings): - **Configuration** (183 settings):
- `g1nation.multiAgentEnabled` *(boolean)* _(default: `false`)_ — Enable Multi-Agent Workflow (Planner -> Researcher -> Writer) for complex tasks. - `g1nation.multiAgentEnabled` *(boolean)* _(default: `false`)_ — Enable Multi-Agent Workflow (Planner -> Researcher -> Writer) for complex tasks.
- `g1nation.datacollectBridgeTarget` *(string)* _(default: `"local"`)_ - `g1nation.datacollectBridgeTarget` *(string)* _(default: `"nas"`)_
- `g1nation.datacollectBridgeUrl` *(string)* _(default: `"http://127.0.0.1:3002"`)_ — [local 타깃] Wiki/Datacollect MCP Bridge URL. /benchmark, /youtube, /wikify chat slash commands route here. The Bridge must be running (`npm run bridge` in the Datacollect project). - `g1nation.datacollectBridgeUrl` *(string)* _(default: `"http://127.0.0.1:3002"`)_ — [local 타깃] Wiki/Datacollect MCP Bridge URL. /benchmark, /youtube, /wikify chat slash commands route here. The Bridge must be running (`npm run bridge` in the Datacollect project). 백엔드를 NAS로 옮긴 경우 보통 사
- `g1nation.datacollectBridgeNasUrl` *(string)* _(default: `""`)_ - `g1nation.datacollectBridgeNasUrl` *(string)* _(default: `"https://dc.koritips.com"`)_
- `g1nation.datacollectBridgeNasToken` *(string)* _(default: `""`)_ - `g1nation.datacollectBridgeNasToken` *(string)* _(default: `""`)_
- `g1nation.domainKnowledge.generalFolder` *(string)* _(default: `""`)_
- `g1nation.domainKnowledge.mathFolder` *(string)* _(default: `""`)_
- `g1nation.domainKnowledge.codingFolder` *(string)* _(default: `""`)_
- `g1nation.domainKnowledge.factsFolder` *(string)* _(default: `""`)_
- `g1nation.datacollectLocalProjectPath` *(string)* _(default: `""`)_
- `g1nation.datacollectSavePath` *(string)* _(default: `""`)_ - `g1nation.datacollectSavePath` *(string)* _(default: `""`)_
- `g1nation.datacollectCrawlDepth` *(number)* _(default: `1`)_ - `g1nation.datacollectCrawlDepth` *(number)* _(default: `1`)_
- `g1nation.datacollectMaxPages` *(number)* _(default: `8`)_ - `g1nation.datacollectMaxPages` *(number)* _(default: `8`)_
@@ -257,7 +262,24 @@ flowchart LR
- `g1nation.chatTemperature` *(number)* _(default: `0.3`)_ - `g1nation.chatTemperature` *(number)* _(default: `0.3`)_
- `g1nation.meetUsesTasks` *(boolean)* _(default: `true`)_ - `g1nation.meetUsesTasks` *(boolean)* _(default: `true`)_
- `g1nation.meetUsesCalendar` *(boolean)* _(default: `false`)_ - `g1nation.meetUsesCalendar` *(boolean)* _(default: `false`)_
- `g1nation.meetVerifyPass` *(boolean)* _(default: `false`)_ - `g1nation.meetVerifyPass` *(boolean)* _(default: `true`)_
- `g1nation.meetVerifyMaxRounds` *(number)* _(default: `2`)_
- `g1nation.meetEditorPass` *(boolean)* _(default: `true`)_
- `g1nation.meetTaskDateFallback` *(string)* _(default: `"today"`)_
- `g1nation.meetTaskDetailExpand` *(boolean)* _(default: `true`)_
- `g1nation.meetTeamRoster` *(string)* _(default: `""`)_
- `g1nation.meetMaxOutputTokens` *(number)* _(default: `8000`)_
- `g1nation.email.watcherEnabled` *(boolean)* _(default: `true`)_
- `g1nation.email.watchIntervalMinutes` *(number)* _(default: `60`)_
- `g1nation.email.overdueAlertHours` *(number)* _(default: `0`)_
- `g1nation.email.outlookEnabled` *(boolean)* _(default: `true`)_
- `g1nation.email.gmailEnabled` *(boolean)* _(default: `true`)_
- `g1nation.email.includeSubfolders` *(boolean)* _(default: `true`)_
- `g1nation.email.wikifySavePath` *(string)* _(default: `""`)_
- `g1nation.email.syncDays` *(number)* _(default: `7`)_
- `g1nation.messenger.stocksReportTimes` *(string)* _(default: `"09:00,15:00"`)_
- `g1nation.messenger.buySignalAlert` *(boolean)* _(default: `true`)_
- `g1nation.messenger.discoverTopFive` *(boolean)* _(default: `true`)_
- `g1nation.dailyBriefing.enabled` *(boolean)* _(default: `true`)_ - `g1nation.dailyBriefing.enabled` *(boolean)* _(default: `true`)_
- `g1nation.dailyBriefing.time` *(string)* _(default: `"09:30"`)_ - `g1nation.dailyBriefing.time` *(string)* _(default: `"09:30"`)_
- `g1nation.sleepDigest.enabled` *(boolean)* _(default: `true`)_ - `g1nation.sleepDigest.enabled` *(boolean)* _(default: `true`)_
@@ -280,32 +302,10 @@ flowchart LR
- `g1nation.contextOverflowPolicy` *(string)* _(default: `"stopAtLimit"`)_ — Fallback behavior (LM Studio) if the prompt still exceeds the context window after Astra's own budgeting. 'stopAtLimit' fails clearly so you notice; 'truncateMiddle'/'rollingWindow' drop content silen - `g1nation.contextOverflowPolicy` *(string)* _(default: `"stopAtLimit"`)_ — Fallback behavior (LM Studio) if the prompt still exceeds the context window after Astra's own budgeting. 'stopAtLimit' fails clearly so you notice; 'truncateMiddle'/'rollingWindow' drop content silen
- `g1nation.autoCompactHistory` *(boolean)* _(default: `true`)_ — Automatically drop the oldest conversation messages from the request when the prompt would exceed the context budget (the on-screen chat history is unaffected). Default: true - `g1nation.autoCompactHistory` *(boolean)* _(default: `true`)_ — Automatically drop the oldest conversation messages from the request when the prompt would exceed the context budget (the on-screen chat history is unaffected). Default: true
- `g1nation.smallModelContextCap` *(number)* _(default: `0`)_ — Optional safety knob, OFF by default (0). Some very small models (≤3B) emit an empty/EOS response when given a prompt near their context window even though it nominally fits. If you observe that with - `g1nation.smallModelContextCap` *(number)* _(default: `0`)_ — Optional safety knob, OFF by default (0). Some very small models (≤3B) emit an empty/EOS response when given a prompt near their context window even though it nominally fits. If you observe that with
- `g1nation.autoContinueOnOutputLimit` *(boolean)* _(default: `true`)_ — When a reply is cut off because it hit the output-token limit, Astra continues it internally (compressed request — original question + the answer so far, not the whole context again) and shows one mer - `g1nation.largeInputMapReduce` *(boolean)* _(default: `true`)_ — When a single message is too large to fit the model's context window, split it into chunks, extract only the request-relevant facts from each (no hallucination/summary), integrate them, and answer fro
- `g1nation.maxAutoContinuations` *(number)* _(default: `4`)_ — Maximum number of automatic continuation rounds per reply (prevents runaway loops). Raise it (e.g. 56) for long-form answers on slow local models; set 0 to disable auto-continuation. Default: 4 - `g1nation.mapReduceTriggerRatio` *(number)* _(default: `0.6`)_ — Map-reduce kicks in when a single message exceeds (effective context window × this ratio). Lower = engages sooner (safer for big inputs, more LLM calls). Default: 0.6
- `g1nation.finalOnlyRetryOnThoughtLeak` *(boolean)* _(default: `true`)_If the model emits only hidden reasoning (<think>, <|channel|>thought, "Thinking Process:" …) and no user-visible answer, Astra silently re-asks it for the final answer only. Hidden reasoning is never - `g1nation.mapReduceConcurrency` *(number)* _(default: `2`)_How many chunk extractions run in parallel. Keep low on a single local GPU (one model serves them sequentially anyway). Default: 2
- `g1nation.lmStudio.idleTimeoutMs` *(number)* _(default: `300000`)_ — Auto-eject the loaded LM Studio model after this many milliseconds of inactivity. Set to 0 to disable. Default: 300000 (5 minutes). - _…and 123 more_
- `g1nation.lmStudio.autoLoadOnSelect` *(boolean)* _(default: `true`)_ — Automatically load LM Studio models into memory when selected from the Astra sidebar.
- `g1nation.lmStudio.sampling.topP` *(number)* _(default: `0.9`)_ — Nucleus sampling cutoff. Small / quantized models often spew wrong-neighbour tokens (한글 깨짐: 붕괴→붕점) when the tail is wide. Lower (0.80.9) tightens; 1.0 disables. Applied to both SDK and REST paths.
- `g1nation.lmStudio.sampling.topK` *(number)* _(default: `20`)_ — Top-K sampling cutoff. 0 disables. Default 20 — tighter for small models, raise to 4080 for large models that already sample well.
- `g1nation.lmStudio.sampling.minP` *(number)* _(default: `0.05`)_ — Min-P floor — discards tokens with probability below this fraction of the top token. Good defence against rare-token glitches. 0 disables.
- `g1nation.lmStudio.sampling.repeatPenalty` *(number)* _(default: `1.1`)_ — Repeat / frequency penalty to curb stutter (것입니다서입니다…). 1.0 disables. Values 1.051.2 are typical.
- `g1nation.lmStudio.statsInBudget` *(boolean)* _(default: `true`)_ — Show token/s and time-to-first-token from LM Studio prediction stats in the context-budget badge after each turn (SDK path only).
- `g1nation.lmStudio.draftModel` *(string)* _(default: `""`)_ — [Speculative decoding] LM Studio model key of a small draft model (e.g. 'gemma-2b-it') used to accelerate the main model. Empty disables. 1.53x throughput on large models. The draft must be downloade
- `g1nation.lmStudio.load.flashAttention` *(boolean)* _(default: `true`)_ — [Load option] Enable Flash Attention when loading models. Faster generation + lower memory on compatible hardware, especially helpful for long contexts. Default: true.
- `g1nation.lmStudio.load.gpuOffloadRatio` *(string)* _(default: `"max"`)_ — [Load option] How much of the model to offload to GPU. 'max' = all (default), 'off' = CPU only, or a number 01 (e.g. '0.5' = half). Numeric strings are parsed.
- `g1nation.lmStudio.load.offloadKVCacheToGpu` *(boolean)* _(default: `true`)_ — [Load option] Keep KV cache on GPU memory. Faster but requires VRAM headroom. Default: true.
- `g1nation.lmStudio.load.keepModelInMemory` *(boolean)* _(default: `true`)_ — [Load option] Prevent the model from being swapped out of system memory. Improves interactive responsiveness; raises RAM use. Default: true.
- `g1nation.lmStudio.load.useFp16ForKVCache` *(boolean)* _(default: `false`)_ — [Load option] Store KV cache in FP16 (halves cache memory). Tiny quality impact for most models — try if you run out of VRAM at long contexts. Default: false.
- `g1nation.lmStudio.load.evalBatchSize` *(number)* _(default: `0`)_ — [Load option] Token batch size during evaluation. 0 = engine default. Higher (5121024) improves prefill speed on GPU at the cost of memory.
- `g1nation.localBrainPath` *(string)* _(default: `""`)_ — Folder path for your local Second Brain knowledge base. Leave empty to use the default folder.
- `g1nation.brainProfiles` *(array)* _(default: `[]`)_ — Multiple brain profiles. Each item supports id, name, localBrainPath, secondBrainRepo, and description.
- `g1nation.activeBrainId` *(string)* _(default: `""`)_ — Active brain profile id used for the current chat context.
- `g1nation.secondBrainRepo` *(string)* _(default: `""`)_ — Optional GitHub repository URL used for Second Brain sync.
- `g1nation.autoPushBrain` *(boolean)* _(default: `false`)_ — Automatically commit and push Second Brain changes after updates.
- `g1nation.maxContextSize` *(number)* _(default: `32000`)_ — Maximum character count for active file context. Default: 32000
- `g1nation.maxAutoSteps` *(number)* _(default: `50`)_ — Maximum autonomous steps the agent can take per request. Default: 50
- `g1nation.dryRun` *(boolean)* _(default: `false`)_ — If enabled, the agent will ask for approval before committing any file changes.
- _…and 89 more_
## Dependencies ## Dependencies
- **Runtime** (2): `@lmstudio/sdk`, `pdf-parse` - **Runtime** (2): `@lmstudio/sdk`, `pdf-parse`
@@ -353,7 +353,7 @@ Astra는 대표님의 명시적인 승인 하에 로컬 시스템의 강력한
**Designed for High-Performance Decision Making.** **Designed for High-Performance Decision Making.**
Copyright (C) **g1nation**. All rights reserved. Copyright (C) **g1nation**. All rights reserved.
_Last auto-scan: 2026-06-15T02:56:35.947Z · signature `c66695ef`_ _Last auto-scan: 2026-07-20T04:20:03.384Z · signature `ff87bb88`_
<!-- ASTRA:AUTO-END --> <!-- ASTRA:AUTO-END -->
## Purpose ## Purpose
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
{ {
"result": "직답 결과 — single-pass mock 응답입니다.", "result": "직답 결과 — single-pass mock 응답입니다.",
"createdAt": 1784020111869, "createdAt": 1784521057661,
"modelVersion": "unknown" "modelVersion": "unknown"
} }
@@ -1,5 +1,5 @@
{ {
"result": "---\nid: wiki_on\ndate: 2026-07-14T09:08:31.873Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\n직답 결과 — single-pass mock 응답입니다.\n\n직답 결과 — single-pass mock 응답입니다.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `0/100` | ✅ Low |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[DIRECT]** 답변 작성 중... (단일 호출 fast-path) (18ms)\n", "result": "---\nid: wiki_on\ndate: 2026-07-20T04:17:37.663Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\n직답 결과 — single-pass mock 응답입니다.\n\n직답 결과 — single-pass mock 응답입니다.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `0/100` | ✅ Low |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[DIRECT]** 답변 작성 중... (단일 호출 fast-path) (18ms)\n",
"createdAt": 1784020111873, "createdAt": 1784521057663,
"modelVersion": "unknown" "modelVersion": "unknown"
} }
@@ -1,8 +1,8 @@
{ {
"missionId": "wiki_on", "missionId": "wiki_on",
"status": "completed", "status": "completed",
"startTime": "2026-07-14T09:08:31.847Z", "startTime": "2026-07-20T04:17:37.641Z",
"totalElapsedMs": 27, "totalElapsedMs": 24,
"results": { "results": {
"direct": "직답 결과 — single-pass mock 응답입니다." "direct": "직답 결과 — single-pass mock 응답입니다."
}, },
@@ -14,14 +14,14 @@
"to": "direct", "to": "direct",
"durationMs": 18, "durationMs": 18,
"message": "답변 작성 중... (단일 호출 fast-path)", "message": "답변 작성 중... (단일 호출 fast-path)",
"ts": "2026-07-14T09:08:31.865Z" "ts": "2026-07-20T04:17:37.659Z"
}, },
{ {
"from": "direct", "from": "direct",
"to": "completed", "to": "completed",
"durationMs": 9, "durationMs": 5,
"message": "미션 완료", "message": "미션 완료",
"ts": "2026-07-14T09:08:31.874Z" "ts": "2026-07-20T04:17:37.664Z"
} }
], ],
"resilienceMetrics": { "resilienceMetrics": {
@@ -1,5 +1,5 @@
{ {
"result": "Final report with inconsistencies. This should be long enough to pass validation.", "result": "Final report with inconsistencies. This should be long enough to pass validation.",
"createdAt": 1784020117514, "createdAt": 1784521065426,
"modelVersion": "unknown" "modelVersion": "unknown"
} }
@@ -1,5 +1,5 @@
{ {
"result": "Final report with inconsistencies. This should be long enough to pass validation.", "result": "Final report with inconsistencies. This should be long enough to pass validation.",
"createdAt": 1784020117513, "createdAt": 1784521065425,
"modelVersion": "unknown" "modelVersion": "unknown"
} }
@@ -1,5 +1,5 @@
{ {
"result": "[{\"heading\":\"본문\",\"scope\":\"전체 답변\"}]", "result": "[{\"heading\":\"본문\",\"scope\":\"전체 답변\"}]",
"createdAt": 1784020117508, "createdAt": 1784521065420,
"modelVersion": "unknown" "modelVersion": "unknown"
} }
@@ -1,5 +1,5 @@
{ {
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", "result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
"createdAt": 1784020117511, "createdAt": 1784521065423,
"modelVersion": "unknown" "modelVersion": "unknown"
} }
@@ -1,8 +1,8 @@
{ {
"missionId": "stress_conflict_1784020117492", "missionId": "stress_conflict_1784521065403",
"status": "completed", "status": "completed",
"startTime": "2026-07-14T09:08:37.493Z", "startTime": "2026-07-20T04:17:45.404Z",
"totalElapsedMs": 22, "totalElapsedMs": 23,
"results": { "results": {
"outline": "[{\"heading\":\"본문\",\"scope\":\"전체 답변\"}]", "outline": "[{\"heading\":\"본문\",\"scope\":\"전체 답변\"}]",
"section_0": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", "section_0": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
@@ -14,30 +14,30 @@
{ {
"from": "idle", "from": "idle",
"to": "outline", "to": "outline",
"durationMs": 14, "durationMs": 15,
"message": "답변 구조 잡는 중...", "message": "답변 구조 잡는 중...",
"ts": "2026-07-14T09:08:37.507Z" "ts": "2026-07-20T04:17:45.419Z"
}, },
{ {
"from": "outline", "from": "outline",
"to": "section", "to": "section",
"durationMs": 3, "durationMs": 3,
"message": "본문 작성 중...", "message": "본문 작성 중...",
"ts": "2026-07-14T09:08:37.510Z" "ts": "2026-07-20T04:17:45.422Z"
}, },
{ {
"from": "section", "from": "section",
"to": "polish", "to": "polish",
"durationMs": 2, "durationMs": 2,
"message": "최종 다듬기 중...", "message": "최종 다듬기 중...",
"ts": "2026-07-14T09:08:37.512Z" "ts": "2026-07-20T04:17:45.424Z"
}, },
{ {
"from": "polish", "from": "polish",
"to": "completed", "to": "completed",
"durationMs": 3, "durationMs": 3,
"message": "미션 완료", "message": "미션 완료",
"ts": "2026-07-14T09:08:37.515Z" "ts": "2026-07-20T04:17:45.427Z"
} }
], ],
"resilienceMetrics": { "resilienceMetrics": {
+18
View File
@@ -1,5 +1,23 @@
# Astra Patch Notes # Astra Patch Notes
## v2.2.312 (2026-07-20)
### 📋 보고 품질 — "## 4.부터 시작하는 보고서" 버그 + 일반론 총평 차단
실사례: "connectai 프로젝트를 분석하고 보고해줘" → 답변이 "## 4. 종합 분석 결과"부터 시작(1~3 증발), 내용은 프로젝트명을 바꿔도 성립하는 7줄 일반론.
- **중간 라운드 본문 표시 버그 수정 (근본 원인)**: 액션 루프에서 액션과 *함께* 본문 섹션을 쓴 라운드는 화면에 한 번도 표시되지 않고(라이브 스트리밍은 depth 0 전용) 최종 라운드만 append 됐다. 모델은 히스토리에서 자기 이전 섹션(1~3)을 보고 "## 4."부터 이어 써서 사용자에겐 4번부터 도착. 이제 중간 라운드의 가시 본문을 액션 태그만 걷어내고(신규 stripForDisplay.ts) 버블에 순서대로 표시한다.
- **분석 보고 업무 유형 신설 (requirementGraph)**: "분석하고 보고해줘/현황 보고/검토 보고"가 어떤 유형에도 안 걸리던 갭을 막음. 필수 요소: 보고 개요(첫 줄에 대상—근거—결론 자기선언), **파일 근거**(주장마다 실제 읽은 파일 인용, 일반론 금지), 구조·현황, 핵심 발견, 다음 단계. 유형 감지 시 "최대 3섹션" 일반 규칙보다 필수 요소 커버 우선 명시(보고서가 얕아지는 상충 제거).
- **근거 없는 분석 감지 (Hollow 의 반대 방향)**: 파일을 읽고도(read/investigate ≥1) 답변에 파일 인용이 2개 미만인 장문 분석이면 "근거 인용 없음" footer 로 경고 — 그럴듯한 총평이 검증된 분석처럼 통과하는 것을 차단.
검증: tsc 무오류 + jest 954 통과(신규 13: analysis-report 3 + ungrounded 5 + stripForDisplay 5) + esbuild 정상.
## v2.2.311 (2026-07-20)
### ⚡ 응답 지연 근본 개선 — KV 캐시 친화 프롬프트 + 제2뇌 상주 캐시
실측(usage.jsonl): 출력 22토큰 답변에 94.7초 — 지연의 90%+ 가 생성이 아니라 **매 턴 13k+ 토큰 전체 재프리필**이었다. 원인은 (a) 시스템 프롬프트 상단의 분 단위 시각이 llama.cpp prompt cache 를 첫 토큰부터 무효화, (b) 턴마다 바뀌는 RAG/동적 블록이 전부 message[0] 에 있어 히스토리 캐시까지 연쇄 무효화, (c) 32K 창에서 검색 결과에만 8k 토큰 예산, (d) continuation 마다 빈 쿼리로 재검색(3~8초), (e) 매 질의 6,500 파일 디렉터리 워크 + statSync.
- **KV 캐시 분리 (`kvCachePromptSplit`, 기본 ON)**: message[0] 을 불변 정적 본문(페르소나·규칙·환경)으로 고정하고, 날짜/RAG/[CONTEXT]/동적 블록 전부를 *마지막 user 메시지 직전*의 internal system 메시지로 이동. 프리픽스(정적 프롬프트+과거 히스토리)가 캐시에서 재사용되어 재프리필이 "직전 교환 + 동적 컨텍스트"로 줄어든다. [CONTEXT] truncation 도 tail 에 적용. 커스텀 systemPrompt 호출자(멀티에이전트 등)는 종전 경로 유지.
- **검색 토큰 예산 현실화 (`retrievalTokenBudget`, 0=자동)**: 자동값을 창의 25%(8k~80k) → 12%(2.5k~6k 클램프)로. 매 턴 재프리필되는 tail 을 직접 얇게 만든다.
- **continuation 은 depth-0 memoryCtx 재사용**: 액션 라운드마다의 재검색 제거 — 라운드당 3~8초 절감 + 빈 쿼리 재검색으로 청크가 갈리던 문제 제거 + 턴 내 프롬프트 안정화.
- **제2뇌 상주 캐시 (신규 brainWatch.ts)**: 브레인 루트에 재귀 fs.watch 를 걸고 세대 카운터로 무효화 — 변경 없으면 디렉터리 워크와 파일별 statSync(수천 회)를 통째로 생략하고 메모리의 파일 목록·토큰 인덱스를 신뢰한다. "제2뇌를 항상 메모리에" 아이디어의 안전한 구현: 원본 대신 검색 파생물을 상주시키고 워처가 정합성을 맡는다. 수정 직후 3초 창은 신뢰 명부에서 제외(이벤트 지연 레이스 가드), 워처 불가 환경은 자동으로 종전(매번 stat) 폴백. 활성화 시 백그라운드 워밍으로 첫 질의의 인덱스 파싱 비용도 선불 처리, 유휴 해제 30분→2시간.
- 기대 효과: 동일 세션 반복 턴의 프리필이 13k → 수백~2k 토큰 수준. LM Studio 쪽 GPU 오프로드/Flash Attention 설정과 병행 권장.
검증: tsc 무오류 + jest 941 통과(신규 15: split 6 + brainCache 4 + promptSplit 5) + esbuild 정상.
## v2.2.307 (2026-07-11) ## v2.2.307 (2026-07-11)
### 🏗️ /benchmark 레이아웃 골격 분석 + 합성 안정화 ### 🏗️ /benchmark 레이아웃 골격 분석 + 합성 안정화
koritips 실측 피드백 반영: ① 합성이 파트 2 에서 타임아웃("This operation was aborted")으로 죽으면서 105초 걸린 파트 1까지 폐기 ② 남은 원시 요약이 색·폰트뿐이라 "배너가 롤링인지, 각 페이지가 어떤 블록 순서인지" 알 길이 없었음. koritips 실측 피드백 반영: ① 합성이 파트 2 에서 타임아웃("This operation was aborted")으로 죽으면서 105초 걸린 파트 1까지 폐기 ② 남은 원시 요약이 색·폰트뿐이라 "배너가 롤링인지, 각 페이지가 어떤 블록 순서인지" 알 길이 없었음.
+1 -1
View File
@@ -7,5 +7,5 @@
"corePurpose": "", "corePurpose": "",
"detailLevel": "standard", "detailLevel": "standard",
"createdAt": "2026-05-20T09:42:40.003Z", "createdAt": "2026-05-20T09:42:40.003Z",
"updatedAt": "2026-06-15T03:00:02.383Z" "updatedAt": "2026-07-20T04:19:49.700Z"
} }
@@ -0,0 +1,16 @@
# Discussion: E:\Wiki\connectai 프로젝트를 분석하고 구조에 대해서 설명해줘
## User Request Summary
E:\Wiki\connectai 프로젝트를 분석하고 구조에 대해서 설명해줘
## Interpreted Intent
Capture a meaningful project discussion automatically instead of requiring manual record selection.
## Questions
No explicit question was captured.
## Main Discussion
- E:\Wiki\connectai 프로젝트를 분석하고 구조에 대해서 설명해줘
## Decisions
No decisions captured yet.
@@ -0,0 +1,16 @@
# Discussion: E:\Wiki\connectai 프로젝트를 분석하고 구조에 대해서 설명해줘
## User Request Summary
E:\Wiki\connectai 프로젝트를 분석하고 구조에 대해서 설명해줘
## Interpreted Intent
Capture a meaningful project discussion automatically instead of requiring manual record selection.
## Questions
No explicit question was captured.
## Main Discussion
- 반가워요. 어떻게 도와드릴까요?
## Decisions
No decisions captured yet.
@@ -0,0 +1,16 @@
# Discussion: E:\Wiki\connectai 프로젝트를 분석하고 어떻게 되어 있는지 보고해줘.
## User Request Summary
E:\Wiki\connectai 프로젝트를 분석하고 어떻게 되어 있는지 보고해줘.
## Interpreted Intent
Capture a meaningful project discussion automatically instead of requiring manual record selection.
## Questions
No explicit question was captured.
## Main Discussion
- ## 4. 종합 분석 결과 현재 ConnectAI 프로젝트는 로컬 LLM과 RAG 기술을 VS Code라는 개발 환경에 밀접하게 결합하려는 시도가 매우 인상적입니다. 구조적으로 에이전트 기반의 워크플로우와 계층적 메모리 시스템이 설계되어 있어, 단순한 챗봇 이상의 '지능형 코딩 어시스턴트'로 발전할 가능성이 높습니다. **핵심 요약:** - **강점**: 로컬 LLM 사용을 통한 보안성 및 비용 효율성 확보, RAG를 활용한 개인화된 지식 검색 기능의 구조적 완성도. - **기회**: 에이전트 구조(Planner, Researcher, Writer)를 통한 복잡한 개발 태스크 자동화 가능성. - **과제**: 대규모 코드베이스 인덱싱 시의 성능 최적화 및 사용자 경험(UX)의 직관성 강화. 결론적으로, 이 프로젝트는 단순한 도구 모음을 넘어 사용자의 코딩 맥락을 이해하는 '지능형 레이어'를 구축하는 데 초점이 맞춰져 있습니다.
## Decisions
No decisions captured yet.
+9
View File
@@ -420,3 +420,12 @@
## 2026-06-12 ## 2026-06-12
- Auto development record created: development\2026-06-12_지금까지-너는-나에게-아이디어-핵심-내용-기대-효과-내용으로-써줬는데-법정-의무-교육은-다른-포멧으로-줬어-_implementation.md - Auto development record created: development\2026-06-12_지금까지-너는-나에게-아이디어-핵심-내용-기대-효과-내용으로-써줬는데-법정-의무-교육은-다른-포멧으로-줬어-_implementation.md
## 2026-07-20
- Auto discussion record created: discussions\2026-07-20_e-wiki-connectai-프로젝트를-분석하고-구조에-대해서-설명해줘.md
## 2026-07-20
- Auto discussion record created: discussions\2026-07-20_e-wiki-connectai-프로젝트를-분석하고-구조에-대해서-설명해줘-2.md
## 2026-07-20
- Auto discussion record created: discussions\2026-07-20_e-wiki-connectai-프로젝트를-분석하고-어떻게-되어-있는지-보고해줘.md
+11 -1
View File
@@ -2,7 +2,7 @@
"name": "astra", "name": "astra",
"displayName": "Astra", "displayName": "Astra",
"description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.", "description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.",
"version": "2.2.310", "version": "2.2.312",
"publisher": "g1nation", "publisher": "g1nation",
"license": "MIT", "license": "MIT",
"icon": "assets/icon.png", "icon": "assets/icon.png",
@@ -1294,6 +1294,16 @@
"type": "string", "type": "string",
"default": "", "default": "",
"markdownDescription": "조사형 요청('~폴더 조사해줘') 전용 모델 오버라이드. 예: 'claude-code:sonnet'. 소형 로컬 모델이 파일명만 보고 내용을 상상하는 헛조사 방지책의 하나 — 비우면 현재 선택 모델 사용." "markdownDescription": "조사형 요청('~폴더 조사해줘') 전용 모델 오버라이드. 예: 'claude-code:sonnet'. 소형 로컬 모델이 파일명만 보고 내용을 상상하는 헛조사 방지책의 하나 — 비우면 현재 선택 모델 사용."
},
"g1nation.kvCachePromptSplit": {
"type": "boolean",
"default": true,
"markdownDescription": "**KV 캐시 친화 프롬프트 분리** — 시스템 프롬프트를 '불변 본문(message[0])'과 '턴별 동적 컨텍스트(마지막 질문 직전 주입)'로 분리해 LM Studio/llama.cpp 의 prompt cache 프리픽스 재사용을 극대화. 매 턴 전체 재프리필(1만+ 토큰, 수십 초)을 직전 교환 분량으로 줄인다. 특정 모델 템플릿에서 응답 품질 문제가 보이면 끄세요."
},
"g1nation.retrievalTokenBudget": {
"type": "number",
"default": 0,
"markdownDescription": "**RAG 검색 토큰 예산 상한** — 두뇌/메모리 검색 결과가 프롬프트에서 차지할 최대 토큰. `0 = 자동`(컨텍스트 창의 12%, 2,500~6,000 클램프). 값이 클수록 근거는 풍부해지지만 로컬 모델 프리필이 느려진다."
} }
} }
} }
+69 -9
View File
@@ -5,6 +5,8 @@ import * as fs from 'fs';
import { import {
findBrainFiles, findBrainFiles,
getSystemPrompt, getSystemPrompt,
getStaticSystemPrompt,
getDateTimeContextBlock,
shouldAutoPushBrain, shouldAutoPushBrain,
buildApiUrl, buildApiUrl,
getActiveBrainProfile, getActiveBrainProfile,
@@ -322,6 +324,14 @@ export class AgentExecutor {
actionStats: { reads: number; lists: number; investigates: number }; actionStats: { reads: number; lists: number; investigates: number };
/** [v2.2.309] 조사 턴 모델 오버라이드 — depth 0 에서 결정, continuation 에도 유지. */ /** [v2.2.309] 조사 턴 모델 오버라이드 — depth 0 에서 결정, continuation 에도 유지. */
investigationModelOverride: string | null; investigationModelOverride: string | null;
/**
* [v2.2.311] depth 0 에서 빌드한 memoryCtx 문자열 캐시 — continuation depth 는
* 재검색하지 않고 이걸 재사용한다. 종전엔 depth 마다 buildMemoryContext 를 다시
* 돌렸는데, (a) 검색 3~8초가 라운드마다 추가되고 (b) continuation 의 prompt 는
* null 이라 *빈 쿼리로* 재검색해 엉뚱한 청크로 갈아끼우고 (c) 프롬프트가 흔들려
* KV 캐시도 깨졌다. actionStats 와 같은 이유로 depth 0 진입부에서만 초기화.
*/
memoryCtxCache: string | null;
} = { } = {
retrieval: null, retrieval: null,
lessons: [], lessons: [],
@@ -331,6 +341,7 @@ export class AgentExecutor {
confidenceSignals: null, confidenceSignals: null,
actionStats: { reads: 0, lists: 0, investigates: 0 }, actionStats: { reads: 0, lists: 0, investigates: 0 },
investigationModelOverride: null, investigationModelOverride: null,
memoryCtxCache: null,
}; };
/** Per-turn state 일괄 정리. turn 시작/abort/load session 시 호출. */ /** Per-turn state 일괄 정리. turn 시작/abort/load session 시 호출. */
@@ -560,6 +571,7 @@ export class AgentExecutor {
// [v2.2.309] turn 전체(모든 depth) 누적 상태 — depth 0 에서만 초기화. // [v2.2.309] turn 전체(모든 depth) 누적 상태 — depth 0 에서만 초기화.
this._turnCtx.actionStats = { reads: 0, lists: 0, investigates: 0 }; this._turnCtx.actionStats = { reads: 0, lists: 0, investigates: 0 };
this._turnCtx.investigationModelOverride = null; this._turnCtx.investigationModelOverride = null;
this._turnCtx.memoryCtxCache = null;
} }
// 1. Prepare Context // 1. Prepare Context
@@ -755,11 +767,17 @@ export class AgentExecutor {
? `\n\n${renderSecondBrainTraceContext(secondBrainTrace)}` ? `\n\n${renderSecondBrainTraceContext(secondBrainTrace)}`
: ''; : '';
const retrievalStartMs = Date.now(); const retrievalStartMs = Date.now();
const memoryCtx = isCasualConversation // [v2.2.311] continuation depth 는 depth 0 의 memoryCtx 를 재사용 — 재검색
? '' // 3~8초 제거 + 빈 쿼리 재검색으로 청크가 갈리는 문제 제거 + 프롬프트 안정화
: await (async () => { // (dynamicBlocks 등 turnCtx 파생물도 depth 0 것이 그대로 유지된다).
let memoryCtx: string;
if (isCasualConversation) {
memoryCtx = '';
} else if (loopDepth > 0 && this._turnCtx.memoryCtxCache !== null) {
memoryCtx = this._turnCtx.memoryCtxCache;
} else {
this.resetTurnContext(); this.resetTurnContext();
return buildMemoryContextFn({ memoryCtx = await buildMemoryContextFn({
currentPrompt: prompt || '', currentPrompt: prompt || '',
activeBrain, activeBrain,
agentSkillFile: options.agentSkillFile, agentSkillFile: options.agentSkillFile,
@@ -770,7 +788,8 @@ export class AgentExecutor {
currentTaskId: this.currentTaskId, currentTaskId: this.currentTaskId,
turnCtx: this._turnCtx, turnCtx: this._turnCtx,
}); });
})(); this._turnCtx.memoryCtxCache = memoryCtx;
}
if (loopDepth === 0 && !isCasualConversation && this._turnCtx.retrieval) { if (loopDepth === 0 && !isCasualConversation && this._turnCtx.retrieval) {
recordTelemetry({ recordTelemetry({
kind: 'retrieval', kind: 'retrieval',
@@ -806,9 +825,19 @@ export class AgentExecutor {
? buildPriorTurnConclusionContext(this.chatHistory) ? buildPriorTurnConclusionContext(this.chatHistory)
: ''; : '';
// System prompt build (agent vs astra mode) → src/agent/handlePrompt/{buildAgentModeSystemPrompt,buildAstraModeSystemPrompt}.ts // System prompt build (agent vs astra mode) → src/agent/handlePrompt/{buildAgentModeSystemPrompt,buildAstraModeSystemPrompt}.ts
const fullSystemPrompt: string = isAgentMode //
// [KV 캐시 분리 v2.2.311] 기본 경로(호출자가 systemPrompt 를 넘기지 않은 경우)는
// message[0] 을 *정적 본문만* 으로 고정하고, 날짜/RAG/[CONTEXT]/동적 블록 전부를
// dynamicContextTail 로 분리해 computeBudgetedRequest 가 마지막 user 메시지 직전에
// 삽입한다. llama.cpp prompt cache 가 정적 프롬프트+과거 히스토리를 재사용하게 되어
// 매 턴 전체 재프리필(실측 13k 토큰 ≈ 90초)이 "직전 교환 + tail" 로 줄어든다.
// 커스텀 systemPrompt 호출자(멀티에이전트 등)는 종전 단일-시스템 경로 유지.
const kvSplitEnabled = getConfig().kvCachePromptSplit !== false
&& options.systemPrompt === undefined;
const builderBasePrompt = kvSplitEnabled ? '' : systemPrompt;
const builtSystemPrompt: string = isAgentMode
? buildAgentModeSystemPrompt({ ? buildAgentModeSystemPrompt({
systemPrompt, systemPrompt: builderBasePrompt,
agentSkillContext: options.agentSkillContext || '', agentSkillContext: options.agentSkillContext || '',
modeBridgeCtx, modeBridgeCtx,
priorConclusionCtx, priorConclusionCtx,
@@ -824,7 +853,7 @@ export class AgentExecutor {
}) })
: buildAstraModeSystemPrompt({ : buildAstraModeSystemPrompt({
prompt, prompt,
systemPrompt, systemPrompt: builderBasePrompt,
modeBridgeCtx, modeBridgeCtx,
priorConclusionCtx, priorConclusionCtx,
designerCtx, designerCtx,
@@ -839,6 +868,12 @@ export class AgentExecutor {
knowledgeMix: this._turnCtx.knowledgeMix, knowledgeMix: this._turnCtx.knowledgeMix,
dynamicBlocks: this._turnCtx.dynamicBlocks, dynamicBlocks: this._turnCtx.dynamicBlocks,
}); });
// Split 모드: head = 정적 프롬프트(불변), tail = 날짜 + 빌더 산출(동적 전부).
// Legacy 모드: 종전 그대로 head 에 전부.
const fullSystemPrompt: string = kvSplitEnabled ? getStaticSystemPrompt() : builtSystemPrompt;
const dynamicContextTail: string | undefined = kvSplitEnabled
? `${getDateTimeContextBlock()}${builtSystemPrompt}`
: undefined;
// Context budget computation → src/agent/handlePrompt/computeBudgetedRequest.ts // Context budget computation → src/agent/handlePrompt/computeBudgetedRequest.ts
const imageCount = (reqMessages as any[]) const imageCount = (reqMessages as any[])
.reduce((n, m) => n + (Array.isArray(m?.images) ? m.images.length : 0), 0); .reduce((n, m) => n + (Array.isArray(m?.images) ? m.images.length : 0), 0);
@@ -874,7 +909,7 @@ export class AgentExecutor {
const lastUserIdx = reqMessages.map((m) => m.role).lastIndexOf('user'); const lastUserIdx = reqMessages.map((m) => m.role).lastIndexOf('user');
const lastUser = lastUserIdx >= 0 ? reqMessages[lastUserIdx] : undefined; const lastUser = lastUserIdx >= 0 ? reqMessages[lastUserIdx] : undefined;
const content = typeof lastUser?.content === 'string' ? lastUser.content : ''; const content = typeof lastUser?.content === 'string' ? lastUser.content : '';
const sysTokens = estimateTokens(fullSystemPrompt) + 4; const sysTokens = estimateTokens(fullSystemPrompt) + (dynamicContextTail ? estimateTokens(dynamicContextTail) : 0) + 4;
const mrCfg = { const mrCfg = {
enabled: true, enabled: true,
triggerRatio: config.mapReduceTriggerRatio, triggerRatio: config.mapReduceTriggerRatio,
@@ -934,6 +969,7 @@ export class AgentExecutor {
const _budget = computeBudgetedRequest({ const _budget = computeBudgetedRequest({
fullSystemPrompt, fullSystemPrompt,
dynamicContextTail,
reqMessages, reqMessages,
actualModel, actualModel,
config, config,
@@ -1462,6 +1498,22 @@ export class AgentExecutor {
await this.context.workspaceState.update('lastActionStr', currentActionStr); await this.context.workspaceState.update('lastActionStr', currentActionStr);
logInfo('Autonomous loop continuing after actions.', { loopDepth: loopDepth + 1, actions: report }); logInfo('Autonomous loop continuing after actions.', { loopDepth: loopDepth + 1, actions: report });
// [v2.2.312] 중간 라운드 본문 표시 — 액션과 *함께* 작성된 섹션이 화면에서
// 증발하던 버그 수정. 종전엔 액션이 있는 라운드는 여기서 return 하며 본문을
// 한 번도 webview 에 보내지 않았고(표시는 라이브 스트리밍뿐 — depth 0 전용),
// 최종 라운드만 streamChunk 로 붙었다. 그 결과 모델이 히스토리에서 자기 이전
// 섹션(1~3)을 보고 "## 4."부터 이어 써서, 사용자에게는 4번부터 시작하는
// 보고서가 도착했다 (실사례). 라이브로 이미 표시된 depth 0 는 중복 방지로 제외.
if (loopDepth > 0 || !postLiveDeltas) {
try {
const { stripActionTagsForDisplay } = await import('./agent/actions/stripForDisplay');
const roundVisible = stripActionTagsForDisplay(finalAssistantContent);
if (roundVisible) {
this.webview.postMessage({ type: 'streamChunk', value: `${loopDepth > 0 ? '\n\n' : ''}${roundVisible}` });
}
} catch { /* 표시 실패가 루프를 막지 않음 */ }
}
// Explicitly tell the AI to look at the results and continue // Explicitly tell the AI to look at the results and continue
const continuationPrompt = `The requested local action has been executed.\nAction report:\n${report.join('\n')}\nUse the action result messages already in the conversation to answer the user's original request directly, in the user's language. Do not say you are waiting for the next instruction.`; const continuationPrompt = `The requested local action has been executed.\nAction report:\n${report.join('\n')}\nUse the action result messages already in the conversation to answer the user's original request directly, in the user's language. Do not say you are waiting for the next instruction.`;
@@ -1554,6 +1606,14 @@ export class AgentExecutor {
if (hollowInv.hollow) { if (hollowInv.hollow) {
this.webview.postMessage({ type: 'streamChunk', value: formatHollowInvestigationFooter(hollowInv.fileMentions) }); this.webview.postMessage({ type: 'streamChunk', value: formatHollowInvestigationFooter(hollowInv.fileMentions) });
logInfo('Hollow Investigation 감지 (continuation).', { files: hollowInv.fileMentions, stats: this._turnCtx.actionStats }); logInfo('Hollow Investigation 감지 (continuation).', { files: hollowInv.fileMentions, stats: this._turnCtx.actionStats });
} else {
// [v2.2.312] 반대 방향 — 파일을 읽고도 근거 인용 없는 일반론 분석 경고.
const { detectUngroundedAnalysis, formatUngroundedAnalysisFooter } = await import('./intelligence/investigationPipeline');
const ug = detectUngroundedAnalysis(finalAssistantContent, this._turnCtx.actionStats);
if (ug.ungrounded) {
this.webview.postMessage({ type: 'streamChunk', value: formatUngroundedAnalysisFooter(ug.readCount) });
logInfo('Ungrounded Analysis 감지 (continuation).', { readCount: ug.readCount, fileRefs: ug.fileRefs });
}
} }
} catch { /* 감지 실패가 답변을 막지 않음 */ } } catch { /* 감지 실패가 답변을 막지 않음 */ }
} }
+31
View File
@@ -0,0 +1,31 @@
/**
* [v2.2.312] 화면 표시용 액션 태그 제거.
*
* 액션 루프의 중간 라운드(액션과 함께 본문 섹션을 쓴 라운드)를 사용자 버블에
* 표시할 때, <read_file/> 같은 실행 태그 원문이 그대로 노출되지 않도록 걷어낸다.
* 실행 자체는 이 함수와 무관 — executeActions 는 원문(cleanedVisible)을 그대로
* 받는다. 여기는 *표시 전용* 경로다.
*/
const ACTION_TAG_NAMES = [
'read_file', 'list_files', 'investigate_files',
'create_file', 'edit_file', 'delete_file',
'run_command', 'run_code',
'read_brain', 'list_brain',
'read_sheet', 'write_sheet', 'append_sheet',
'fetch_url', 'calculate',
'create_calendar_event', 'add_task', 'update_task', 'complete_task',
// edit_file 내부 서브 태그 — 블록 제거가 실패한 잔재까지 정리.
'search', 'replace',
];
export function stripActionTagsForDisplay(text: string): string {
let t = text || '';
for (const name of ACTION_TAG_NAMES) {
// 자기닫힘 <tag ... /> → 블록 <tag ...>...</tag> → 잔재 여는/닫는 태그 순.
t = t.replace(new RegExp(`<${name}\\b[^>]*/>`, 'gi'), '');
t = t.replace(new RegExp(`<${name}\\b[^>]*>[\\s\\S]*?</${name}>`, 'gi'), '');
t = t.replace(new RegExp(`</?${name}\\b[^>]*>`, 'gi'), '');
}
return t.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
}
@@ -13,6 +13,14 @@ import { buildDroppedHistorySummary } from '../../lib/contextBuilders/droppedHis
export interface ComputeBudgetedRequestInput { export interface ComputeBudgetedRequestInput {
fullSystemPrompt: string; fullSystemPrompt: string;
/**
* [KV 캐시 분리 v2.2.311] 턴마다 변하는 컨텍스트(RAG/[CONTEXT]/날짜/동적 블록)를
* message[0] 이 아니라 *마지막 user 메시지 직전*의 internal system 메시지로 싣는다.
* message[0](fullSystemPrompt)은 턴 사이에 불변 → llama.cpp prompt cache 가
* "정적 프롬프트 + 과거 히스토리" 프리픽스를 재사용하고, 매 턴 프리필은
* "직전 교환 + 이 블록"만으로 줄어든다. undefined 면 종전 단일-시스템 동작.
*/
dynamicContextTail?: string;
/** Caller is expected to have run `capChatHistory` on this already. */ /** Caller is expected to have run `capChatHistory` on this already. */
reqMessages: ChatMessage[]; reqMessages: ChatMessage[];
actualModel: string; actualModel: string;
@@ -126,12 +134,27 @@ export function computeBudgetedRequest(input: ComputeBudgetedRequestInput): Comp
1024, 1024,
Math.floor((ctxLimits.contextLength - ctxLimits.safetyMargin - preferredOutputReserve - imageTokenReserve) * 0.65) Math.floor((ctxLimits.contextLength - ctxLimits.safetyMargin - preferredOutputReserve - imageTokenReserve) * 0.65)
); );
const { prompt: budgetedSystemPrompt, truncated: systemTruncated } = // Split 모드면 [CONTEXT] 는 dynamicContextTail 쪽에 있으므로 truncation 도 tail 에 적용.
truncateSystemPromptContext(fullSystemPrompt, systemCapTokens); // (정적 head 에는 [CONTEXT] 마커가 없어 truncate 가 no-op — head 는 그대로 둔다.)
const splitMode = typeof input.dynamicContextTail === 'string' && input.dynamicContextTail.trim().length > 0;
let budgetedSystemPrompt = fullSystemPrompt;
let budgetedTail = splitMode ? input.dynamicContextTail! : '';
let systemTruncated = false;
if (splitMode) {
const headTokens = estimateTokens(fullSystemPrompt);
const tailCap = Math.max(512, systemCapTokens - headTokens);
const t = truncateSystemPromptContext(budgetedTail, tailCap);
budgetedTail = t.prompt;
systemTruncated = t.truncated;
} else {
const t = truncateSystemPromptContext(fullSystemPrompt, systemCapTokens);
budgetedSystemPrompt = t.prompt;
systemTruncated = t.truncated;
}
if (systemTruncated) { if (systemTruncated) {
logInfo('System prompt context truncated to fit the context window.', { model: actualModel, systemCapTokens }); logInfo('System prompt context truncated to fit the context window.', { model: actualModel, systemCapTokens });
} }
const systemTokens = estimateTokens(budgetedSystemPrompt) + 4; const systemTokens = estimateTokens(budgetedSystemPrompt) + (splitMode ? estimateTokens(budgetedTail) + 4 : 0) + 4;
// (2) 대화 기록 압축. // (2) 대화 기록 압축.
const historyBudget = Math.max( const historyBudget = Math.max(
@@ -156,9 +179,27 @@ export function computeBudgetedRequest(input: ComputeBudgetedRequestInput): Comp
} }
} }
// Split 모드: 동적 컨텍스트를 마지막 user 메시지 *직전*에 삽입. 이 위치라야
// (a) 이전 요청과의 공통 프리픽스(정적 시스템 + 과거 히스토리)가 최대로 보존되고
// (b) continuation(액션 결과가 뒤에 붙는 라운드)에서도 같은 자리라 턴 내 캐시가 유지된다.
// user 메시지가 없으면(이론상) 히스토리 끝에 붙인다 — 생성 직전이므로 여전히 유효.
let historyWithTail = budgetedHistory;
if (splitMode) {
const tailMsg: ChatMessage = { role: 'system', content: budgetedTail, internal: true };
const lastUserIdx = (() => {
for (let i = budgetedHistory.length - 1; i >= 0; i--) {
if (budgetedHistory[i].role === 'user') return i;
}
return -1;
})();
historyWithTail = lastUserIdx >= 0
? [...budgetedHistory.slice(0, lastUserIdx), tailMsg, ...budgetedHistory.slice(lastUserIdx)]
: [...budgetedHistory, tailMsg];
}
const messagesForRequest: ChatMessage[] = [ const messagesForRequest: ChatMessage[] = [
{ role: 'system', content: budgetedSystemPrompt, internal: true }, { role: 'system', content: budgetedSystemPrompt, internal: true },
...budgetedHistory ...historyWithTail
]; ];
// (3) 동적 출력 상한. // (3) 동적 출력 상한.
+15
View File
@@ -233,6 +233,19 @@ export interface IAgentConfig {
* 할루시네이션 대책. 빈 문자열이면 비활성 (현재 선택 모델 그대로). * 할루시네이션 대책. 빈 문자열이면 비활성 (현재 선택 모델 그대로).
*/ */
investigationModel: string; investigationModel: string;
/**
* [v2.2.311] KV 캐시 친화 프롬프트 분리 — message[0] 을 정적 본문으로 고정하고
* 날짜/RAG/[CONTEXT]/동적 블록을 마지막 user 메시지 직전의 system 메시지로 옮긴다.
* llama.cpp prompt cache 프리픽스 재사용으로 턴당 재프리필을 대폭 축소. 기본 true.
* 특정 모델 템플릿에서 문제 시 false 로 종전 단일-시스템 프롬프트 복귀.
*/
kvCachePromptSplit: boolean;
/**
* [v2.2.311] RAG/메모리 검색 토큰 예산 상한. 0 = 자동(컨텍스트 창의 12%,
* 2,500~6,000 클램프). 종전 자동값(창의 25%, 최대 80k)은 32k 창에서 8k 토큰을
* 검색 결과에 써서 로컬 프리필만 수십 초를 태웠다 — 상한을 낮춰 답변 지연을 줄인다.
*/
retrievalTokenBudget: number;
/** Self-check 전용 모델 ID. 비면 defaultModel. 빠른 작은 모델 권장. */ /** Self-check 전용 모델 ID. 비면 defaultModel. 빠른 작은 모델 권장. */
selfCheckModel: string; selfCheckModel: string;
/** Self-check LLM 호출 타임아웃 (초). 기본 6. */ /** Self-check LLM 호출 타임아웃 (초). 기본 6. */
@@ -583,6 +596,8 @@ export function getConfig(): IAgentConfig {
intentClarificationStrictness: (cfg.get<string>('intentClarificationStrictness', 'medium') as 'low' | 'medium' | 'high') || 'medium', intentClarificationStrictness: (cfg.get<string>('intentClarificationStrictness', 'medium') as 'low' | 'medium' | 'high') || 'medium',
citationTraceEnabled: cfg.get<boolean>('citationTraceEnabled', true), citationTraceEnabled: cfg.get<boolean>('citationTraceEnabled', true),
investigationModel: (cfg.get<string>('investigationModel', '') || '').trim(), investigationModel: (cfg.get<string>('investigationModel', '') || '').trim(),
kvCachePromptSplit: cfg.get<boolean>('kvCachePromptSplit', true),
retrievalTokenBudget: Math.max(0, cfg.get<number>('retrievalTokenBudget', 0) || 0),
selfCheckEnabled: cfg.get<boolean>('selfCheckEnabled', false), selfCheckEnabled: cfg.get<boolean>('selfCheckEnabled', false),
selfCheckModel: cfg.get<string>('selfCheckModel', '') || '', selfCheckModel: cfg.get<string>('selfCheckModel', '') || '',
selfCheckTimeoutSec: Math.max(1, Math.min(60, cfg.get<number>('selfCheckTimeoutSec', 6))), selfCheckTimeoutSec: Math.max(1, Math.min(60, cfg.get<number>('selfCheckTimeoutSec', 6))),
+11
View File
@@ -86,6 +86,17 @@ export async function activate(context: vscode.ExtensionContext) {
// 두뇌에 기록. 자기 평가가 구식 스냅샷 대신 항상 현행 소스를 근거로 하게 한다. // 두뇌에 기록. 자기 평가가 구식 스냅샷 대신 항상 현행 소스를 근거로 하게 한다.
void ensureFeatureInventory(context); void ensureFeatureInventory(context);
// [v2.2.311] 제2뇌 캐시 워밍 — 활성화 직후 백그라운드로 파일 목록 + 토큰 인덱스
// (수십 MB JSON)를 메모리에 올려, 세션 첫 질의가 그 비용(수 초)을 뒤집어쓰지
// 않게 한다. 워처가 같이 걸려 이후 턴은 변경 없으면 워크/stat 을 통째로 생략.
try {
const { getActiveBrainProfile } = require('./utils') as typeof import('./utils');
const { warmBrainCache } = require('./retrieval/brainWatch') as typeof import('./retrieval/brainWatch');
const { getBrainTokenIndex } = require('./retrieval/brainIndex') as typeof import('./retrieval/brainIndex');
const brainPath = getActiveBrainProfile()?.localBrainPath;
if (brainPath) warmBrainCache(brainPath, (files) => { getBrainTokenIndex(brainPath, files); });
} catch { /* 워밍 실패는 비치명 — 첫 질의가 종전 경로로 처리 */ }
// Initialize Astra Path Resolver (.astra → ConnectAI/.astra/) // Initialize Astra Path Resolver (.astra → ConnectAI/.astra/)
initAstraPathResolver(context); initAstraPathResolver(context);
+28
View File
@@ -226,3 +226,31 @@ export function formatHollowInvestigationFooter(fileMentions: string[]): string
'내용 검증이 필요하면 다시 요청해 주세요 — 다음 턴에서 `<investigate_files>`로 각 파일을 실제로 읽고 답합니다.', '내용 검증이 필요하면 다시 요청해 주세요 — 다음 턴에서 `<investigate_files>`로 각 파일을 실제로 읽고 답합니다.',
].join('\n'); ].join('\n');
} }
/**
* [v2.2.312] 헛조사의 반대 방향 실패 — 파일을 *읽고도* 답변에 파일 근거를 하나도
* 인용하지 않은 "일반론 분석" 감지. 실사례: 프로젝트 분석 보고가 7줄짜리
* 프로젝트명만 바꿔도 성립하는 총평("~가 인상적입니다")으로 끝남.
* 조건: read/investigate 를 1회 이상 했고, 답변이 실질 분량(300자+)인데
* 파일 참조가 2개 미만 → ungrounded. (짧은 확인형 응답은 오탐 방지로 제외.)
*/
export function detectUngroundedAnalysis(
answer: string,
stats: TurnActionStats | null | undefined,
): { ungrounded: boolean; readCount: number; fileRefs: number } {
const readCount = (stats?.reads ?? 0) + (stats?.investigates ?? 0);
if (!stats || readCount === 0) return { ungrounded: false, readCount, fileRefs: 0 };
const body = (answer || '').trim();
if (body.length < 300) return { ungrounded: false, readCount, fileRefs: 0 };
const refs = new Set(Array.from(body.matchAll(FILE_MENTION_RE), m => m[0]));
return { ungrounded: refs.size < 2, readCount, fileRefs: refs.size };
}
export function formatUngroundedAnalysisFooter(readCount: number): string {
return [
'',
'---',
`⚠️ **근거 인용 없음** — 파일을 ${readCount}회 읽고도 답변에 실제 파일·코드 근거가 인용되지 않았습니다. 위 내용은 일반론일 수 있습니다.`,
'구체 근거가 필요하면 "실제 읽은 파일명을 인용해서 다시 정리해줘"라고 요청해 주세요.',
].join('\n');
}
+39
View File
@@ -154,6 +154,42 @@ export const DEFAULT_TASK_REQUIREMENTS: TaskRequirement[] = [
}, },
], ],
}, },
{
// [v2.2.312] 실사례 대응 — "E:\...프로젝트를 분석하고 어떻게 되어 있는지 보고해줘"가
// 어떤 유형에도 안 걸려 7줄짜리 일반론 총평으로 끝났다. '보고해줘/현황 보고' 류를
// 잡아 파일 근거 인용을 필수 요소로 강제한다.
id: 'analysis-report',
label: '분석 보고',
detectKeywords: ['분석[^\\n]{0,20}보고', '보고해\\s*줘?', '현황\\s*(보고|정리)', '어떻게\\s*되어\\s*있는지', '검토[^\\n]{0,10}보고'],
coverageCheck: true,
elements: [
{
id: 'overview', label: '보고 개요',
hint: '첫 줄에 "보고 개요: (분석 대상) — (확인한 근거: 실제 읽은 파일 수) — (한 줄 결론)" 형식으로 시작. 요청자가 첫 줄만 읽어도 무엇을 어떤 근거로 판단했는지 알 수 있게.',
detectPatterns: ['보고 ?개요', '분석 ?대상', '개요'],
},
{
id: 'file-evidence', label: '파일 근거',
hint: '모든 주요 주장에 이번 대화에서 실제로 읽은 파일명·경로를 인용. 프로젝트 이름을 바꿔도 성립하는 일반론("~가 인상적입니다", "~할 가능성이 높습니다")은 금지 — 근거 없는 문장은 쓰지 말 것.',
detectPatterns: ['\\.(ts|tsx|js|mjs|cjs|py|md|json|html|css|java|cs)\\b', 'src[\\\\/]', 'package\\.json'],
},
{
id: 'structure', label: '구조·현황',
hint: '프로젝트/대상의 실제 구성 — 핵심 모듈, 진행 상태, 확인된 사실. STATUS.md 등 현재 상태 기록이 있으면 그것을 기준으로.',
detectPatterns: ['구조', '구성', '모듈', '아키텍처', '현황', '상태'],
},
{
id: 'findings', label: '핵심 발견·이슈',
hint: '분석에서 드러난 강점·문제·이슈. 각 항목에 근거 파일 연결.',
detectPatterns: ['발견', '강점', '문제', '이슈', '리스크', '과제'],
},
{
id: 'next-steps', label: '다음 단계',
hint: '이 보고를 받은 사람이 취할 수 있는 구체적 다음 행동.',
detectPatterns: ['다음 ?단계', '권장', '제안', '후속'],
},
],
},
{ {
id: 'work-research', id: 'work-research',
label: '업무조사', label: '업무조사',
@@ -233,6 +269,9 @@ export function buildRequirementGraphBlock(
} }
lines.push(''); lines.push('');
lines.push('제출 전 위 체크리스트를 스스로 점검하고, 누락 요소가 있으면 보완 후 답변할 것.'); lines.push('제출 전 위 체크리스트를 스스로 점검하고, 누락 요소가 있으면 보완 후 답변할 것.');
// [v2.2.312] 업무 유형이 감지된 답변은 일반 "최대 3섹션" 규칙보다 필수 요소 커버가
// 우선 — 보고서가 3섹션 제한 때문에 얕아지는 상충 제거.
lines.push('이 업무 유형이 감지된 답변에서는 "최대 3섹션" 일반 규칙보다 필수 요소 커버가 우선한다 — 요소별로 섹션을 나눠도 된다.');
lines.push('[/TASK REQUIREMENTS]'); lines.push('[/TASK REQUIREMENTS]');
return lines.join('\n'); return lines.join('\n');
} }
+7 -5
View File
@@ -166,11 +166,13 @@ export async function buildMemoryContext(deps: MemoryContextDeps): Promise<strin
if (domainScope.folders.length > 0) effectiveScopeFolders = domainScope.folders; if (domainScope.folders.length > 0) effectiveScopeFolders = domainScope.folders;
} }
// Context 윈도우 비례 retrieval 예산. 32K → 8K, 230K → 57K, 80K cap (scoring 속도). // Context 윈도우 비례 retrieval 예산.
const scaledTotalBudget = Math.min( // [v2.2.311] 종전 자동값(창의 25%, 8k~80k)은 32K 창에서 검색 결과에만 8k 토큰을 태워
80000, // 로컬 프리필 수십 초의 주범이었다. 자동값을 창의 12%(2.5k~6k 클램프)로 낮추고,
Math.max(8000, Math.floor(config.contextLength * 0.25)), // 더 풍부한 근거가 필요하면 g1nation.retrievalTokenBudget 으로 명시 상향.
); const scaledTotalBudget = config.retrievalTokenBudget > 0
? config.retrievalTokenBudget
: Math.min(6000, Math.max(2500, Math.floor(config.contextLength * 0.12)));
// medium-term layer 용 옛 세션 후보. sidebar 가 직접 쓰는 key 를 read-through. // medium-term layer 용 옛 세션 후보. sidebar 가 직접 쓰는 key 를 read-through.
const rawSessions = deps.context.globalState.get<any[]>('chat_sessions', []) || []; const rawSessions = deps.context.globalState.get<any[]>('chat_sessions', []) || [];
+50 -1
View File
@@ -17,6 +17,7 @@ import { tokenize, countConflictIndicators } from './scoring';
import { detectLessonKind } from './lessonHelpers'; import { detectLessonKind } from './lessonHelpers';
import { splitIntoSections } from './chunker'; import { splitIntoSections } from './chunker';
import { logInfo } from '../utils'; import { logInfo } from '../utils';
import { ensureBrainWatcher, getBrainGeneration } from './brainWatch';
// v5 adds optional per-file `chunks` (section-level index, Phase 1-가) alongside the // v5 adds optional per-file `chunks` (section-level index, Phase 1-가) alongside the
// v4 per-file `embedding`. Older indexes are auto-rebuilt on first load — no migration // v4 per-file `embedding`. Older indexes are auto-rebuilt on first load — no migration
@@ -109,6 +110,16 @@ interface BrainState {
diskPath: string | null; // null if we can't determine a writable path diskPath: string | null; // null if we can't determine a writable path
writeTimer?: ReturnType<typeof setTimeout>; writeTimer?: ReturnType<typeof setTimeout>;
evictTimer?: ReturnType<typeof setTimeout>; evictTimer?: ReturnType<typeof setTimeout>;
/**
* [v2.2.311] statSync . statValidatedGen
* stat( ) statValidated
* -stat . ( )
* stat . **
* 이유: 스코프 ( ) ,
* .
*/
statValidatedGen?: number;
statValidated?: Set<string>;
} }
const _states = new Map<string, BrainState>(); const _states = new Map<string, BrainState>();
@@ -117,7 +128,16 @@ const _states = new Map<string, BrainState>();
// 확장 호스트 메모리에 세션 내내 상주했다. 인덱스는 mtime 키 디스크 캐시에서 언제든 싸게 // 확장 호스트 메모리에 세션 내내 상주했다. 인덱스는 mtime 키 디스크 캐시에서 언제든 싸게
// 재로드되므로, 마지막 사용 후 IDLE_EVICT_MS 가 지나면 dirty 를 flush 하고 메모리에서 내린다. // 재로드되므로, 마지막 사용 후 IDLE_EVICT_MS 가 지나면 dirty 를 flush 하고 메모리에서 내린다.
// 상시 인터벌 없이 접근할 때마다 재예약되는 one-shot unref 타이머라 그 자체의 발열 비용은 없다. // 상시 인터벌 없이 접근할 때마다 재예약되는 one-shot unref 타이머라 그 자체의 발열 비용은 없다.
const IDLE_EVICT_MS = 30 * 60_000; // [v2.2.311] 30분 → 2시간: 점심/회의 뒤 첫 질의가 대형 인덱스(수십 MB JSON) 재파싱을
// 뒤집어쓰는 빈도를 줄인다. 메모리 절약보다 응답 지연이 더 아픈 로컬-퍼스트 워크로드.
const IDLE_EVICT_MS = 120 * 60_000;
/**
* [v2.2.311] stat "갓 수정된 파일" . fs.watch
* (+), bump
* stat .
*/
const RECENT_MTIME_GUARD_MS = 3000;
function touchEviction(st: BrainState, brainPath: string): void { function touchEviction(st: BrainState, brainPath: string): void {
if (st.evictTimer) clearTimeout(st.evictTimer); if (st.evictTimer) clearTimeout(st.evictTimer);
@@ -206,8 +226,35 @@ export function getBrainTokenIndex(brainPath: string, files: string[]): IndexedB
const seen = new Set<string>(); const seen = new Set<string>();
let reindexed = 0; let reindexed = 0;
// [v2.2.311] 워처 세대가 마지막 stat 검증 이후 그대로면 — 파일시스템 변경 없음 —
// 그 세대에서 이미 stat 을 거친 파일의 statSync 를 생략하고 캐시 항목을 신뢰.
// 워처가 없으면(gen === -1) 종전대로 매번 stat — 정확성 저하 없음.
ensureBrainWatcher(brainPath);
const watchGen = getBrainGeneration(brainPath);
if (watchGen >= 0 && st.statValidatedGen !== watchGen) {
st.statValidatedGen = watchGen;
st.statValidated = new Set();
}
const validated = watchGen >= 0 ? (st.statValidated ?? (st.statValidated = new Set())) : null;
for (const file of files) { for (const file of files) {
seen.add(file); seen.add(file);
const cachedFast = validated && validated.has(file) ? st.index.entries[file] : undefined;
if (cachedFast
&& Array.isArray(cachedFast.tokens)
&& Array.isArray(cachedFast.titleTokens)) {
out.push({
filePath: file,
relativePath: cachedFast.relativePath,
title: cachedFast.title,
tokens: cachedFast.tokens,
titleTokens: cachedFast.titleTokens,
conflictCount: cachedFast.conflictCount || 0,
mtimeMs: cachedFast.mtimeMs,
kind: cachedFast.kind || '',
});
continue;
}
let stat: fs.Stats; let stat: fs.Stats;
try { try {
stat = fs.statSync(file); stat = fs.statSync(file);
@@ -220,6 +267,7 @@ export function getBrainTokenIndex(brainPath: string, files: string[]): IndexedB
&& cached.size === stat.size && cached.size === stat.size
&& Array.isArray(cached.tokens) && Array.isArray(cached.tokens)
&& Array.isArray(cached.titleTokens)) { && Array.isArray(cached.titleTokens)) {
if (Date.now() - stat.mtimeMs > RECENT_MTIME_GUARD_MS) validated?.add(file);
out.push({ out.push({
filePath: file, filePath: file,
relativePath: cached.relativePath, relativePath: cached.relativePath,
@@ -255,6 +303,7 @@ export function getBrainTokenIndex(brainPath: string, files: string[]): IndexedB
st.index.entries[file] = entry; st.index.entries[file] = entry;
st.dirty = true; st.dirty = true;
reindexed++; reindexed++;
if (Date.now() - entry.mtimeMs > RECENT_MTIME_GUARD_MS) validated?.add(file);
out.push({ out.push({
filePath: file, filePath: file,
relativePath, relativePath,
+140
View File
@@ -0,0 +1,140 @@
/**
* ============================================================
* Brain Watch 2 + (v2.2.311)
*
* 문제: 검색은 mtime (brain-index.json) **
* , (a) ( ) (b)
* fs.statSync "
* stat" . 6,500 0.5~1.5.
*
* 해결: 브레인 fs.watch (generation)
* . , mtime /stat
* · . "2
* " 형태다: 원본 ( MB),
* ( + ) .
*
* (/ ) generation -1
* ( stat) .
* ============================================================
*/
import * as fs from 'fs';
import * as path from 'path';
import { findBrainFiles, invalidateBrainFilesCache, logInfo } from '../utils';
interface WatchState {
watcher: fs.FSWatcher | null;
/** 파일시스템 변경마다 증가. 워처 없으면 상태 자체가 failed=true. */
generation: number;
failed: boolean;
/** generation 기준 파일 목록 캐시. */
listGeneration: number;
list: string[] | null;
/** 이벤트 폭주(대량 sync) 시 bump 를 묶는 디바운스 타이머. */
bumpTimer: ReturnType<typeof setTimeout> | null;
}
const _watches = new Map<string, WatchState>();
/** 인덱스 캐시 등 우리 자신의 산출물 경로 — 이 아래 변경은 무효화 사유가 아니다. */
function isSelfNoise(relPath: string): boolean {
// .astra (brain-index.json, eval, growth …) 와 .git 은 검색 대상도 아니고
// 턴마다 우리가 직접 쓰는 곳이라, 여기서 세대를 올리면 캐시가 영원히 못 산다.
return /(^|[\\/])(\.astra|\.git)([\\/]|$)/.test(relPath);
}
function bump(st: WatchState, brainPath: string): void {
if (st.bumpTimer) return; // 이미 예약됨 — 몰아서 1회
st.bumpTimer = setTimeout(() => {
st.bumpTimer = null;
st.generation++;
st.list = null;
invalidateBrainFilesCache(brainPath);
}, 300);
if (typeof (st.bumpTimer as any).unref === 'function') (st.bumpTimer as any).unref();
}
/**
* (idempotent). throw
* getBrainGeneration() -1 stat .
*/
export function ensureBrainWatcher(brainPath: string): void {
if (!brainPath) return;
if (_watches.has(brainPath)) return;
const st: WatchState = { watcher: null, generation: 0, failed: false, listGeneration: -1, list: null, bumpTimer: null };
_watches.set(brainPath, st);
try {
// recursive fs.watch 는 Windows/macOS 에서 네이티브 지원. (Linux 는 Node 20+.)
st.watcher = fs.watch(brainPath, { recursive: true }, (_event, filename) => {
const rel = filename ? String(filename) : '';
if (rel && isSelfNoise(rel)) return;
bump(st, brainPath);
});
st.watcher.on('error', () => {
st.failed = true;
try { st.watcher?.close(); } catch { /* already closed */ }
st.watcher = null;
logInfo('Brain watcher failed — falling back to per-query stat.', { brainPath });
});
logInfo('Brain watcher started — file list & stat checks now cached until change.', { brainPath });
} catch (e: any) {
st.failed = true;
st.watcher = null;
logInfo('Brain watcher unavailable — falling back to per-query stat.', { brainPath, error: e?.message ?? String(e) });
}
}
/**
* . -1 .
*/
export function getBrainGeneration(brainPath: string): number {
const st = _watches.get(brainPath);
if (!st || st.failed || !st.watcher) return -1;
return st.generation;
}
/**
* .
* findBrainFiles(5 TTL) .
*/
export function listBrainFilesCached(brainPath: string): string[] {
if (!brainPath) return [];
ensureBrainWatcher(brainPath);
const st = _watches.get(brainPath)!;
if (st.failed || !st.watcher) return findBrainFiles(brainPath);
if (st.list !== null && st.listGeneration === st.generation) return st.list.slice();
const files = findBrainFiles(brainPath);
st.list = files.slice();
st.listGeneration = st.generation;
return files;
}
/**
* 78MB +
* . ( ).
*/
export function warmBrainCache(brainPath: string, warmTokenIndex: (files: string[]) => void): void {
if (!brainPath) return;
setTimeout(() => {
try {
const t0 = Date.now();
const files = listBrainFilesCached(brainPath);
warmTokenIndex(files);
logInfo('Brain cache warmed.', { brainPath, files: files.length, ms: Date.now() - t0 });
} catch (e: any) {
logInfo('Brain cache warm failed (non-fatal).', { brainPath, error: e?.message ?? String(e) });
}
}, 2500);
}
/** 테스트/브레인 전환용 — 워처 해제. */
export function disposeBrainWatcher(brainPath?: string): void {
const targets = brainPath ? [brainPath] : Array.from(_watches.keys());
for (const key of targets) {
const st = _watches.get(key);
if (!st) continue;
if (st.bumpTimer) { clearTimeout(st.bumpTimer); st.bumpTimer = null; }
try { st.watcher?.close(); } catch { /* noop */ }
_watches.delete(key);
}
}
+4 -2
View File
@@ -15,7 +15,8 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { BrainProfile } from '../config'; import { BrainProfile } from '../config';
import { findBrainFiles, summarizeText } from '../utils'; import { summarizeText } from '../utils';
import { listBrainFilesCached } from './brainWatch';
import { isInside } from '../lib/paths'; import { isInside } from '../lib/paths';
import { MemoryManager } from '../memory'; import { MemoryManager } from '../memory';
import { RetrievalChunk, RetrievalResult, ContextBudgetConfig } from './types'; import { RetrievalChunk, RetrievalResult, ContextBudgetConfig } from './types';
@@ -329,7 +330,8 @@ export class RetrievalOrchestrator {
try { try {
const scoped = (file: string) => scopeFolders.length === 0 const scoped = (file: string) => scopeFolders.length === 0
|| scopeFolders.some((folder) => isInside(folder, file)); || scopeFolders.some((folder) => isInside(folder, file));
const allFiles = findBrainFiles(brain.localBrainPath) // [v2.2.311] 워처 세대 기반 상주 목록 — 변경 없으면 디렉터리 재귀 워크 생략.
const allFiles = listBrainFilesCached(brain.localBrainPath)
.filter(scoped) .filter(scoped)
.filter((file) => { .filter((file) => {
const rel = path.relative(brain.localBrainPath, file); const rel = path.relative(brain.localBrainPath, file);
+27 -5
View File
@@ -494,14 +494,19 @@ function getEnvironmentBlock(): string {
- Canonical <run_command> example for this OS: ${example}`; - Canonical <run_command> example for this OS: ${example}`;
} }
export function getSystemPrompt(): string { /**
const now = new Date(); * [KV v2.2.311] * * .
const dateTimeStr = now.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long', hour: '2-digit', minute: '2-digit' }); * LM Studio(llama.cpp) prompt cache
const isoDate = now.toISOString().split('T')[0]; * 1+ 94/
* . / `getDateTimeContextBlock()` (
* ) .
*/
export function getStaticSystemPrompt(): string {
const envBlock = getEnvironmentBlock(); const envBlock = getEnvironmentBlock();
const base = `${BASE_SYSTEM_PROMPT}\n\n[CURRENT DATE/TIME]\nToday: ${isoDate} (${dateTimeStr})\nUse this date as the absolute reference for any date-related calculations (e.g., "this week", "today", "yesterday").\n\n[출력 위생 규칙 — 반드시 준수]\n- 자연스러운 한국어로 작성하고, 한 단어 안에 한글과 영문 알파벳을 섞지 마시오 ("결ently", "인orp" 같은 깨진 합성 표기 절대 금지).\n- 외래어·기술 용어는 완전한 한글 표기 또는 완전한 영문 단어 중 하나로 일관되게 쓰시오.\n- 내부 검증·체크 로그(Consistency/Completeness/Accuracy 등) 블록을 사용자 출력에 포함하지 마시오.${envBlock}`; const base = `${BASE_SYSTEM_PROMPT}\n\n[출력 위생 규칙 — 반드시 준수]\n- 자연스러운 한국어로 작성하고, 한 단어 안에 한글과 영문 알파벳을 섞지 마시오 ("결ently", "인orp" 같은 깨진 합성 표기 절대 금지).\n- 외래어·기술 용어는 완전한 한글 표기 또는 완전한 영문 단어 중 하나로 일관되게 쓰시오.\n- 내부 검증·체크 로그(Consistency/Completeness/Accuracy 등) 블록을 사용자 출력에 포함하지 마시오.${envBlock}`;
// Self-Reflector Phase A — 사용자 설정이 켜져 있으면 답변 끝에 자기검증 // Self-Reflector Phase A — 사용자 설정이 켜져 있으면 답변 끝에 자기검증
// 블록을 강제하는 룰을 prepend. require로 동적 로드해 순환 import 회피. // 블록을 강제하는 룰을 prepend. require로 동적 로드해 순환 import 회피.
// (설정은 세션 중 바뀔 수 있지만 드물다 — 바뀌면 그 턴만 캐시 미스, 문제 없음.)
try { try {
const { getConfig } = require('./config') as typeof import('./config'); const { getConfig } = require('./config') as typeof import('./config');
const { appendSelfReflectorRule } = require('./features/selfReflector/selfReflectorPrompt') as typeof import('./features/selfReflector/selfReflectorPrompt'); const { appendSelfReflectorRule } = require('./features/selfReflector/selfReflectorPrompt') as typeof import('./features/selfReflector/selfReflectorPrompt');
@@ -513,4 +518,21 @@ export function getSystemPrompt(): string {
} }
} }
/** [CURRENT DATE/TIME] 블록 — 가변이므로 정적 프롬프트가 아니라 동적 컨텍스트에 주입. */
export function getDateTimeContextBlock(): string {
const now = new Date();
const dateTimeStr = now.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long', hour: '2-digit', minute: '2-digit' });
const isoDate = now.toISOString().split('T')[0];
return `[CURRENT DATE/TIME]\nToday: ${isoDate} (${dateTimeStr})\nUse this date as the absolute reference for any date-related calculations (e.g., "this week", "today", "yesterday").`;
}
export function getSystemPrompt(): string {
// 기존 호출부 호환 — 정적 본문 + 날짜 블록을 합친 종전 형태 그대로.
const staticPart = getStaticSystemPrompt();
const dateBlock = getDateTimeContextBlock();
// BASE 직후·위생 규칙 앞이던 종전 위치와 달리 끝에 붙지만, 단독 사용처(브리핑·평가
// 등 1회성 호출)에는 위치가 의미 없다. 채팅 본류는 split 경로를 쓴다.
return `${staticPart}\n\n${dateBlock}`;
}
export const SYSTEM_PROMPT = BASE_SYSTEM_PROMPT; export const SYSTEM_PROMPT = BASE_SYSTEM_PROMPT;
+98
View File
@@ -0,0 +1,98 @@
/**
* [v2.2.311] 2 stat + .
*
* :
* 1. getBrainTokenIndex statSync .
* 2. ( ) stat .
* 3. () ,
* ( ).
* 4. (gen === -1) stat.
*/
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
// brainWatch 를 결정적으로 제어 — 실제 fs.watch 이벤트 타이밍에 의존하지 않는다.
let mockGen = 0;
jest.mock('../src/retrieval/brainWatch', () => ({
ensureBrainWatcher: jest.fn(),
getBrainGeneration: jest.fn(() => mockGen),
listBrainFilesCached: jest.fn(),
warmBrainCache: jest.fn(),
disposeBrainWatcher: jest.fn(),
}));
import { getBrainTokenIndex } from '../src/retrieval/brainIndex';
function makeBrain(files: Record<string, string>): { root: string; paths: string[] } {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'astra-brain-'));
const paths: string[] = [];
for (const [name, content] of Object.entries(files)) {
const p = path.join(root, name);
fs.writeFileSync(p, content, 'utf8');
// RECENT_MTIME_GUARD(3초): 갓 수정된 파일은 신뢰 명부에 오르지 않으므로,
// "변경 없음" 시나리오를 테스트하려면 mtime 을 과거로 되돌린다.
const past = new Date(Date.now() - 60_000);
fs.utimesSync(p, past, past);
paths.push(p);
}
return { root, paths };
}
function countStatsFor(paths: string[], spy: jest.SpyInstance): number {
return spy.mock.calls.filter((c) => paths.includes(String(c[0]))).length;
}
describe('getBrainTokenIndex — 워처 세대 기반 stat 생략', () => {
afterEach(() => jest.restoreAllMocks());
test('같은 세대의 2번째 호출은 statSync 0회 (캐시 신뢰)', () => {
mockGen = 0;
const { root, paths } = makeBrain({ 'a.md': '알파 내용', 'b.md': '베타 내용' });
expect(getBrainTokenIndex(root, paths)).toHaveLength(2);
const spy = jest.spyOn(fs, 'statSync');
const r2 = getBrainTokenIndex(root, paths);
expect(r2).toHaveLength(2);
expect(countStatsFor(paths, spy)).toBe(0);
expect(r2.map((d) => d.title).sort()).toEqual(['a', 'b']);
});
test('세대가 바뀌면 재-stat 후 변경 내용을 재색인한다', () => {
mockGen = 0;
const { root, paths } = makeBrain({ 'a.md': '원본 토큰들' });
getBrainTokenIndex(root, paths);
// 파일 변경 + 세대 bump (mtime 차이를 보장하기 위해 utimesSync 로 강제)
fs.writeFileSync(paths[0], '완전히 새로운 내용입니다', 'utf8');
const future = new Date(Date.now() + 5000);
fs.utimesSync(paths[0], future, future);
mockGen = 1;
const spy = jest.spyOn(fs, 'statSync');
const r = getBrainTokenIndex(root, paths);
expect(countStatsFor(paths, spy)).toBeGreaterThan(0);
expect(r[0].tokens).toEqual(expect.arrayContaining(['새로운']));
});
test('스코프 검증 후 전체 질의 — 미검증 파일은 신뢰 경로로 새지 않는다', () => {
mockGen = 0;
const { root, paths } = makeBrain({ 'in-scope.md': '스코프 안', 'out-scope.md': '스코프 밖' });
const [inScope, outScope] = paths;
getBrainTokenIndex(root, [inScope]); // 부분 목록만 검증됨
const spy = jest.spyOn(fs, 'statSync');
getBrainTokenIndex(root, paths); // 전체 질의
expect(countStatsFor([inScope], spy)).toBe(0); // 검증된 파일은 생략
expect(countStatsFor([outScope], spy)).toBeGreaterThan(0); // 미검증 파일은 stat
});
test('워처 불가(gen=-1) 환경은 매번 stat (종전 동작)', () => {
mockGen = -1;
const { root, paths } = makeBrain({ 'a.md': '내용' });
getBrainTokenIndex(root, paths);
const spy = jest.spyOn(fs, 'statSync');
getBrainTokenIndex(root, paths);
expect(countStatsFor(paths, spy)).toBeGreaterThan(0);
});
});
+89
View File
@@ -56,3 +56,92 @@ describe('computeBudgetedRequest — real-window alignment', () => {
expect(run({ actualContextLength: NaN }).effectiveContextLength).toBe(32768); expect(run({ actualContextLength: NaN }).effectiveContextLength).toBe(32768);
}); });
}); });
// ────────────────────────────────────────────────────────────────────────────
// [v2.2.311] KV 캐시 분리 — dynamicContextTail 삽입 계약.
// message[0] 은 불변 정적 프롬프트로 남고, 동적 컨텍스트는 마지막 user 메시지
// 직전의 internal system 메시지로 들어가야 프롬프트 프리픽스 캐시가 산다.
// ────────────────────────────────────────────────────────────────────────────
describe('computeBudgetedRequest — dynamicContextTail (KV cache split)', () => {
const HEAD = 'STATIC HEAD PROMPT';
const TAIL = '[CURRENT DATE/TIME]\nToday: 2026-07-20\n\n[CONTEXT]\nRAG chunk A\n[/CONTEXT]';
// 주의: default parameter 는 명시적 undefined 에도 적용되므로 쓰지 않는다 —
// "tail 없음" 케이스를 정확히 표현하기 위해 항상 명시적으로 넘긴다.
function runSplit(reqMessages: ChatMessage[], tail: string | undefined) {
return computeBudgetedRequest({
fullSystemPrompt: HEAD,
dynamicContextTail: tail,
reqMessages,
actualModel: 'some-13b-model',
config: baseConfig,
imageCount: 0,
});
}
test('tail 은 마지막 user 메시지 직전에 internal system 으로 삽입된다', () => {
const msgs: ChatMessage[] = [
{ role: 'user', content: '첫 질문' },
{ role: 'assistant', content: '첫 답' },
{ role: 'user', content: '두번째 질문' },
];
const r = runSplit(msgs, TAIL);
expect(r.messagesForRequest[0]).toMatchObject({ role: 'system', content: HEAD });
const idx = r.messagesForRequest.findIndex((m) => m.content === TAIL);
expect(idx).toBeGreaterThan(0);
expect(r.messagesForRequest[idx].role).toBe('system');
expect(r.messagesForRequest[idx + 1]).toMatchObject({ role: 'user', content: '두번째 질문' });
});
test('continuation(마지막 user 뒤 액션 결과 system)이 있어도 tail 은 user 앞', () => {
const msgs: ChatMessage[] = [
{ role: 'user', content: '조사해줘' },
{ role: 'system', content: '[Result of read_file] ...', internal: true },
];
const r = runSplit(msgs, TAIL);
const tailIdx = r.messagesForRequest.findIndex((m) => m.content === TAIL);
const userIdx = r.messagesForRequest.findIndex((m) => m.role === 'user');
const actionIdx = r.messagesForRequest.findIndex((m) => String(m.content).startsWith('[Result of'));
expect(tailIdx).toBeLessThan(userIdx);
expect(actionIdx).toBeGreaterThan(userIdx);
});
test('user 메시지가 없으면 tail 을 히스토리 끝에 붙인다', () => {
const msgs: ChatMessage[] = [{ role: 'system', content: 'internal only', internal: true }];
const r = runSplit(msgs, TAIL);
const last = r.messagesForRequest[r.messagesForRequest.length - 1];
expect(last.content).toBe(TAIL);
});
test('tail 미지정이면 종전 단일-시스템 동작 그대로', () => {
const msgs: ChatMessage[] = [{ role: 'user', content: 'hello' }];
const r = runSplit(msgs, undefined);
expect(r.messagesForRequest).toHaveLength(2);
expect(r.messagesForRequest[0].content).toBe(HEAD);
});
test('예산 초과 시 [CONTEXT] truncation 은 tail 에 적용되고 head 는 보존', () => {
const hugeTail = `[CONTEXT]\n${'긴 배경 지식 청크. '.repeat(20000)}\n[/CONTEXT]`;
const msgs: ChatMessage[] = [{ role: 'user', content: 'hello' }];
const r = computeBudgetedRequest({
fullSystemPrompt: HEAD,
dynamicContextTail: hugeTail,
reqMessages: msgs,
actualModel: 'some-13b-model',
config: { ...baseConfig, contextLength: 8192 },
imageCount: 0,
});
expect(r.systemTruncated).toBe(true);
expect(r.messagesForRequest[0].content).toBe(HEAD);
const tailMsg = r.messagesForRequest.find((m) => String(m.content).includes('[CONTEXT]'));
expect(tailMsg).toBeDefined();
expect(String(tailMsg!.content).length).toBeLessThan(hugeTail.length);
});
test('systemTokens 는 head+tail 합산으로 보고된다', () => {
const msgs: ChatMessage[] = [{ role: 'user', content: 'hello' }];
const withTail = runSplit(msgs, TAIL);
const withoutTail = runSplit(msgs, undefined);
expect(withTail.systemTokens).toBeGreaterThan(withoutTail.systemTokens);
});
});
+26
View File
@@ -165,3 +165,29 @@ describe('detectHollowInvestigation — 헛조사 감지', () => {
expect(footer).toContain('investigate_files'); expect(footer).toContain('investigate_files');
}); });
}); });
// [v2.2.312] 근거 없는 분석 감지 — 파일을 읽고도 인용이 없는 일반론 보고.
import { detectUngroundedAnalysis, formatUngroundedAnalysisFooter } from '../src/intelligence/investigationPipeline';
describe('detectUngroundedAnalysis — 일반론 분석 감지', () => {
const generic = '이 프로젝트는 로컬 LLM과 RAG 기술의 결합 시도가 매우 인상적입니다. '.repeat(10);
test('read 를 하고도 파일 인용 없는 장문 = ungrounded', () => {
const r = detectUngroundedAnalysis(generic, { reads: 3, lists: 1, investigates: 0 });
expect(r.ungrounded).toBe(true);
expect(r.readCount).toBe(3);
});
test('파일 근거를 2개 이상 인용하면 정상', () => {
const grounded = `${generic} 근거: agent.ts 의 handlePrompt 와 memoryContext.ts 의 buildMemoryContext 를 확인함.`;
expect(detectUngroundedAnalysis(grounded, { reads: 3, lists: 1, investigates: 0 }).ungrounded).toBe(false);
});
test('read 가 없으면 판정 대상 아님 (hollow 감지의 영역)', () => {
expect(detectUngroundedAnalysis(generic, { reads: 0, lists: 1, investigates: 0 }).ungrounded).toBe(false);
});
test('짧은 확인형 응답은 오탐 방지로 제외', () => {
expect(detectUngroundedAnalysis('네, 서버 정상입니다.', { reads: 1, lists: 0, investigates: 0 }).ungrounded).toBe(false);
});
test('footer 에 읽은 횟수 포함', () => {
expect(formatUngroundedAnalysisFooter(3)).toContain('3회');
});
});
+60
View File
@@ -0,0 +1,60 @@
/**
* [v2.2.311] KV / .
*
* (getStaticSystemPrompt)
* llama.cpp prompt cache (실측: 13k
* 90/). .
*/
import { getStaticSystemPrompt, getDateTimeContextBlock, getSystemPrompt } from '../src/utils';
import { buildAstraModeSystemPrompt } from '../src/agent/handlePrompt/buildAstraModeSystemPrompt';
describe('정적/동적 시스템 프롬프트 분리', () => {
test('정적 프롬프트에는 날짜/시각이 없다 (캐시 무효화 가드)', () => {
const s = getStaticSystemPrompt();
expect(s).not.toContain('[CURRENT DATE/TIME]');
// 오늘 날짜 문자열이 어떤 형태로도 들어있으면 안 된다.
const iso = new Date().toISOString().split('T')[0];
expect(s).not.toContain(iso);
});
test('정적 프롬프트는 연속 호출에서 동일하다 (프리픽스 불변)', () => {
expect(getStaticSystemPrompt()).toBe(getStaticSystemPrompt());
});
test('날짜 블록은 오늘 날짜를 포함한다', () => {
const block = getDateTimeContextBlock();
expect(block).toContain('[CURRENT DATE/TIME]');
expect(block).toContain(new Date().toISOString().split('T')[0]);
});
test('레거시 getSystemPrompt 는 정적 본문 + 날짜 블록을 모두 포함 (호환)', () => {
const s = getSystemPrompt();
expect(s).toContain('[CURRENT DATE/TIME]');
expect(s).toContain('[출력 위생 규칙 — 반드시 준수]');
});
test('빌더에 systemPrompt="" 를 넘겨도 [CONTEXT] 골격은 유지된다 (tail 생성 경로)', () => {
const tail = buildAstraModeSystemPrompt({
prompt: '질문',
systemPrompt: '',
modeBridgeCtx: '',
priorConclusionCtx: '',
designerCtx: '',
projectArchitectureCtx: '',
secondBrainTraceCtx: '',
memoryCtx: 'RAG 본문',
knowledgeContextForPrompt: '',
contextBlock: '열린 파일 내용',
negativeCtx: '',
isCasualConversation: false,
localPathContext: '',
knowledgeMix: null,
dynamicBlocks: new Map([['behavior-constraints', '[GROUNDING] 근거 강도: 보통']]),
});
expect(tail).toContain('[CONTEXT]');
expect(tail).toContain('RAG 본문');
expect(tail).toContain('열린 파일 내용');
expect(tail).toContain('[GROUNDING]');
expect(tail.startsWith('You are Astra')).toBe(false); // 정적 본문이 섞이면 안 됨
});
});
+20
View File
@@ -124,3 +124,23 @@ describe('DEFAULT_TASK_REQUIREMENTS 무결성', () => {
} }
}); });
}); });
// [v2.2.312] 분석 보고 유형 — "프로젝트 분석하고 보고해줘" 실사례가 유형 미감지로
// 일반론 총평이 되던 갭을 막는다.
describe('detectTaskType — analysis-report (v2.2.312)', () => {
test('실사례 문장을 정확히 감지한다', () => {
expect(detectTaskType('E:\Wiki\connectai 프로젝트를 분석하고 어떻게 되어 있는지 보고해줘')?.id).toBe('analysis-report');
expect(detectTaskType('진행 현황 보고 부탁해')?.id).toBe('analysis-report');
expect(detectTaskType('이 코드 검토해서 보고해줘')?.id).toBe('analysis-report');
});
test('더 구체적인 유형이 우선한다 (회의록·시장조사)', () => {
expect(detectTaskType('회의록으로 정리해서 보고해줘')?.id).toBe('meeting-minutes');
expect(detectTaskType('시장조사 결과 보고해줘')?.id).toBe('market-research');
});
test('블록에 파일 근거 요소와 섹션 규칙 오버라이드가 포함된다', () => {
const block = buildRequirementGraphBlock('프로젝트 분석하고 보고해줘');
expect(block).toContain('파일 근거');
expect(block).toContain('보고 개요');
expect(block).toContain('최대 3섹션');
});
});
+35
View File
@@ -0,0 +1,35 @@
/**
* [v2.2.312] "## 4.부터 시작하는 보고서"
* . .
*/
import { stripActionTagsForDisplay } from '../src/agent/actions/stripForDisplay';
describe('stripActionTagsForDisplay', () => {
test('자기닫힘 태그 제거 + 본문 보존', () => {
const input = '## 1. 프로젝트 개요\n구조를 확인한다.\n<read_file path="src/agent.ts"/>\n<list_files path="src"/>';
const out = stripActionTagsForDisplay(input);
expect(out).toContain('## 1. 프로젝트 개요');
expect(out).not.toContain('<read_file');
expect(out).not.toContain('<list_files');
});
test('블록 태그(내용 포함) 제거', () => {
const input = '분석 시작.\n<run_command>cd x; npm test</run_command>\n<create_file path="a.md">본문</create_file>\n끝.';
const out = stripActionTagsForDisplay(input);
expect(out).toContain('분석 시작.');
expect(out).toContain('끝.');
expect(out).not.toContain('npm test');
expect(out).not.toContain('<create_file');
});
test('잔재 여는/닫는 태그도 정리', () => {
const out = stripActionTagsForDisplay('앞 <edit_file path="x.ts"> 뒤');
expect(out).not.toContain('<edit_file');
expect(out).toContain('앞');
expect(out).toContain('뒤');
});
test('태그 없는 본문은 그대로', () => {
expect(stripActionTagsForDisplay('## 2. 구조\n- src/agent.ts')).toBe('## 2. 구조\n- src/agent.ts');
});
test('빈 입력 안전', () => {
expect(stripActionTagsForDisplay('')).toBe('');
});
});