Update project files
This commit is contained in:
@@ -3,20 +3,20 @@
|
|||||||
<!-- ASTRA:AUTO-START -->
|
<!-- ASTRA:AUTO-START -->
|
||||||
|
|
||||||
## Snapshot
|
## Snapshot
|
||||||
- **Workspace**: `ConnectAI` `v2.2.19` _(absolute path varies by environment; resolved from the active VS Code workspace)_
|
- **Workspace**: `connectai` `v2.2.60` _(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**: 265 source files, ~52,981 lines across 5 top-level modules.
|
- **Stats**: 271 source files, ~55,157 lines across 5 top-level modules.
|
||||||
|
|
||||||
## Last Refresh
|
## Last Refresh
|
||||||
- **Time**: 2026-05-17T14:50:50.535Z
|
- **Time**: 2026-05-22T06:00:02.986Z
|
||||||
- **Files newly analysed**: 5
|
- **Files newly analysed**: 1
|
||||||
- **Files reused from cache**: 260
|
- **Files reused from cache**: 270
|
||||||
|
|
||||||
## Directory Map
|
## Directory Map
|
||||||
```mermaid
|
```mermaid
|
||||||
mindmap
|
mindmap
|
||||||
root((ConnectAI))
|
root((connectai))
|
||||||
src/
|
src/
|
||||||
features/
|
features/
|
||||||
core/
|
core/
|
||||||
@@ -31,17 +31,18 @@ mindmap
|
|||||||
docs/
|
docs/
|
||||||
records/
|
records/
|
||||||
docs/
|
docs/
|
||||||
|
Meeting/
|
||||||
```
|
```
|
||||||
|
|
||||||
## Module Dependencies
|
## Module Dependencies
|
||||||
> 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/>137 files"]
|
src["src/<br/>139 files"]
|
||||||
media["media/<br/>6 files"]
|
media["media/<br/>6 files"]
|
||||||
tests["tests/<br/>35 files"]
|
tests["tests/<br/>35 files"]
|
||||||
core_py["core_py/<br/>6 files"]
|
core_py["core_py/<br/>6 files"]
|
||||||
docs["docs/<br/>81 files"]
|
docs["docs/<br/>85 files"]
|
||||||
tests --> src
|
tests --> src
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ 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 **49** files
|
- `src/utils.ts` — referenced by **50** files
|
||||||
- `src/config.ts` — referenced by **16** files
|
- `src/config.ts` — referenced by **16** 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
|
- `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
|
||||||
- `src/core/services.ts` — referenced by **10** files
|
- `src/core/services.ts` — referenced by **10** files
|
||||||
@@ -64,14 +65,14 @@ flowchart LR
|
|||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
### `src/` — 137 files, ~36,157 lines
|
### `src/` — 139 files, ~38,157 lines
|
||||||
|
|
||||||
**Sub-directories**
|
**Sub-directories**
|
||||||
- `src/features/` (64) — Astra Office — public API. 다음 세션에서 추가될 OfficeSnapshot presenter / schema 도 같은 entry 로 노출 예정. 현재 노출: full webview panel H
|
- `src/features/` (66) — Astra Office — public API. 다음 세션에서 추가될 OfficeSnapshot presenter / schema 도 같은 entry 로 노출 예정. 현재 노출: full webview panel H
|
||||||
- `src/core/` (15) — Astra Path Resolver (경로 해결기) Astra의 모든 데이터 파일(.astra 디렉토리)의 경로를 중앙에서 관리합니다. 확장 프로그램의 설치 경로(extensionUri) 기반으로 .astra 디렉토
|
- `src/core/` (15) — Astra Path Resolver (경로 해결기) Astra의 모든 데이터 파일(.astra 디렉토리)의 경로를 중앙에서 관리합니다. 확장 프로그램의 설치 경로(extensionUri) 기반으로 .astra 디렉토
|
||||||
- `src/memory/` (8) — Episodic Memory (일화 기억) 과거 대화/회의/결정의 맥락 흐름을 저장합니다. 세션 종료 시 자동으로 에피소드를 요약하여 저장합니다. "왜 이렇게 결정했는지", "어떤 흐름으로 진행했는지" 기록. 저장
|
- `src/memory/` (8) — Episodic Memory (일화 기억) 과거 대화/회의/결정의 맥락 흐름을 저장합니다. 세션 종료 시 자동으로 에피소드를 요약하여 저장합니다. "왜 이렇게 결정했는지", "어떤 흐름으로 진행했는지" 기록. 저장
|
||||||
- `src/retrieval/` (8) — Brain Index — persistent, mtime-keyed tokenized cache of the Second Brain RAG 검색은 매 질의마다 브레인의 모든 .md 파일을 읽고 토크나이즈해서 TF-I
|
- `src/retrieval/` (8) — Brain Index — persistent, mtime-keyed tokenized cache of the Second Brain RAG 검색은 매 질의마다 브레인의 모든 .md 파일을 읽고 토크나이즈해서 TF-I
|
||||||
- `src/docs/` (6) — src Chronicle Records
|
- `src/docs/` (6) — Bug: Edited agent.ts Edited agent.ts Edited agent.ts Edited agent.ts Edited agent.ts ...
|
||||||
- `src/lib/` (6) — Context Manager (컨텍스트 한계 관리) "context length = 132k" 는 "답변을 132k 토큰까지 생성해도 된다" 가 아닙니다. 시스템 프롬프트 + 대화 기록 + 입력 문서 + 생성될 답변
|
- `src/lib/` (6) — Context Manager (컨텍스트 한계 관리) "context length = 132k" 는 "답변을 132k 토큰까지 생성해도 된다" 가 아닙니다. 시스템 프롬프트 + 대화 기록 + 입력 문서 + 생성될 답변
|
||||||
- `src/integrations/` (4) — Per-chat conversation history for the Telegram bot. Why this exists: the previous bot was stateless — every inbound mess
|
- `src/integrations/` (4) — Per-chat conversation history for the Telegram bot. Why this exists: the previous bot was stateless — every inbound mess
|
||||||
- `src/lmstudio/` (4) — 4 files (.ts)
|
- `src/lmstudio/` (4) — 4 files (.ts)
|
||||||
@@ -81,41 +82,41 @@ flowchart LR
|
|||||||
- `src/scaffolder/` (2) — Scaffolder template catalog. Templates are pure data — (projectName) => { [relativePath]: contents }. New templates are
|
- `src/scaffolder/` (2) — Scaffolder template catalog. Templates are pure data — (projectName) => { [relativePath]: contents }. New templates are
|
||||||
|
|
||||||
**Key files**
|
**Key files**
|
||||||
- `src/utils.ts` (360 lines)
|
- `src/utils.ts` (408 lines)
|
||||||
- `src/config.ts` (298 lines)
|
- `src/config.ts` (298 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` (164 lines)
|
- `src/core/services.ts` (164 lines)
|
||||||
- `src/lib/paths.ts` (151 lines)
|
- `src/lib/paths.ts` (151 lines)
|
||||||
- `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/sidebarProvider.ts` (4165 lines)
|
- `src/sidebarProvider.ts` (4226 lines)
|
||||||
- `src/memory/types.ts` (126 lines) — Memory Type Definitions (메모리 타입 정의) Astra의 5-Layer Cognitive Memory System의 모든 타입을 정의합니다. ① Short-Term ② Long-Term ③ Project ④ Procedural ⑤ Episodic
|
- `src/memory/types.ts` (126 lines) — Memory Type Definitions (메모리 타입 정의) Astra의 5-Layer Cognitive Memory System의 모든 타입을 정의합니다. ① Short-Term ② Long-Term ③ Project ④ Procedural ⑤ Episodic
|
||||||
- `src/retrieval/scoring.ts` (518 lines) — Scoring Engine — TF-IDF + Bilingual Tokenizer 단순 includes() 키워드 매칭을 넘어서, TF-IDF 가중치 기반의 문서 스코어링을 제공합니다. 한국어/영어 양국어 토크나이저를 포함합니다.
|
- `src/retrieval/scoring.ts` (536 lines) — Scoring Engine — TF-IDF + Bilingual Tokenizer 단순 includes() 키워드 매칭을 넘어서, TF-IDF 가중치 기반의 문서 스코어링을 제공합니다. 한국어/영어 양국어 토크나이저를 포함합니다.
|
||||||
- `src/skills/agentKnowledgeMap.ts` (374 lines)
|
- `src/skills/agentKnowledgeMap.ts` (374 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/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/agent.ts` (3633 lines)
|
- `src/agent.ts` (3694 lines)
|
||||||
- `src/features/providers/types.ts` (63 lines) — Cloud LLM provider routing — model id prefix → provider id 매핑. Prefix 규칙: openrouter:anthropic/claude-3.5-sonnet → { provider: 'openrouter', model: 'anthropic/claude-3.5-sonnet' } anthropic:claude-3-5
|
- `src/features/providers/types.ts` (63 lines) — Cloud LLM provider routing — model id prefix → provider id 매핑. Prefix 규칙: openrouter:anthropic/claude-3.5-sonnet → { provider: 'openrouter', model: 'anthropic/claude-3.5-sonnet' } anthropic:claude-3-5
|
||||||
- `src/lib/engine.ts` (906 lines)
|
- `src/lib/engine.ts` (906 lines)
|
||||||
- `src/features/company/dispatcher.ts` (1435 lines) — Sequential dispatcher for 1인 기업 모드. Drives one company "turn": user prompt → CEO planner (JSON {brief, tasks}) → for each task in plan: dispatch one specialist (sequentially) - build specialist prompt
|
- `src/retrieval/brainIndex.ts` (325 lines) — Brain Index — persistent, mtime-keyed tokenized cache of the Second Brain RAG 검색은 매 질의마다 브레인의 모든 .md 파일을 읽고 토크나이즈해서 TF-IDF 점수를 계산했습니다 — 파일 수가 많아지면 그게 병목입니다. 이 모듈은 <brainPath>/.astra/brain-index.json 에
|
||||||
|
- `src/features/company/dispatcher.ts` (1437 lines) — Sequential dispatcher for 1인 기업 모드. Drives one company "turn": user prompt → CEO planner (JSON {brief, tasks}) → for each task in plan: dispatch one specialist (sequentially) - build specialist prompt
|
||||||
- `src/features/providers/providerConfig.ts` (78 lines) — Provider 별 API key + enable 토글 저장소. 설계: - API key 자체는 vscode.SecretStorage (secrets) 에 — settings.json / Settings Sync 침범 안 받음. - enabled 토글은 일반 settings (g1nation.providers.<id>.enabled) — 사용자가 패널에서
|
- `src/features/providers/providerConfig.ts` (78 lines) — Provider 별 API key + enable 토글 저장소. 설계: - API key 자체는 vscode.SecretStorage (secrets) 에 — settings.json / Settings Sync 침범 안 받음. - enabled 토글은 일반 settings (g1nation.providers.<id>.enabled) — 사용자가 패널에서
|
||||||
- `src/features/approval/approvalQueue.ts` (129 lines)
|
- `src/features/approval/approvalQueue.ts` (129 lines)
|
||||||
- `src/integrations/telegram/telegramClient.ts` (154 lines)
|
- `src/integrations/telegram/telegramClient.ts` (154 lines)
|
||||||
- `src/features/astraOffice/view/runtime.ts` (1887 lines) — 자동 분리: src/sidebarProvider.ts 4002-5116 (IIFE 본문) 에서 추출. 동작 동등. ${assets.derivedBase} placeholder 는 panelHtml 에서 .replace() 로 실제 값 주입. 다음 세션에서 OfficeSnapshot 기반으로 단계적으로 잘라낼 예정.
|
- `src/features/astraOffice/view/runtime.ts` (1932 lines) — 자동 분리: src/sidebarProvider.ts 4002-5116 (IIFE 본문) 에서 추출. 동작 동등. ${assets.derivedBase} placeholder 는 panelHtml 에서 .replace() 로 실제 값 주입. 다음 세션에서 OfficeSnapshot 기반으로 단계적으로 잘라낼 예정.
|
||||||
- `src/features/company/agents.ts` (211 lines) — 기본 에이전트 로스터 — 1인 기업 모드의 출고 디폴트. 설계 의도: 소프트웨어/게임 개발 IT 회사의 1인 기업 운영을 가정. 한 사람이 기획 → 디자인 → 개발 → QA → 출시 → 운영/마케팅을 모두 책임질 때 필요한 직군을 빠짐없이 커버하되 역할이 겹치지 않게 분리한다. 직군 구분 (혼동 방지): - 기획자(business) : 무엇을 만들지 정의
|
- `src/features/company/agents.ts` (211 lines) — 기본 에이전트 로스터 — 1인 기업 모드의 출고 디폴트. 설계 의도: 소프트웨어/게임 개발 IT 회사의 1인 기업 운영을 가정. 한 사람이 기획 → 디자인 → 개발 → QA → 출시 → 운영/마케팅을 모두 책임질 때 필요한 직군을 빠짐없이 커버하되 역할이 겹치지 않게 분리한다. 직군 구분 (혼동 방지): - 기획자(business) : 무엇을 만들지 정의
|
||||||
- `src/features/company/pixelOfficeState.ts` (286 lines) — Pixel Office — Agent Work Pipeline 상태를 시각화하는 UI Layer 전용 모듈. ─────────────────── 설계 원칙 ─────────────────── 1. Agent 핵심 판단 로직을 절대 바꾸지 않는다. Pipeline 진행, contract 합의, 검수 cycle, 승인 게이트 — 모두 기존 dispatcher
|
- `src/features/company/pixelOfficeState.ts` (286 lines) — Pixel Office — Agent Work Pipeline 상태를 시각화하는 UI Layer 전용 모듈. ─────────────────── 설계 원칙 ─────────────────── 1. Agent 핵심 판단 로직을 절대 바꾸지 않는다. Pipeline 진행, contract 합의, 검수 cycle, 승인 게이트 — 모두 기존 dispatcher
|
||||||
- `src/features/company/sessionStore.ts` (231 lines) — Disk persistence for company-mode session artefacts. Each company turn produces a timestamped directory: <workspaceRoot>/.astra/company/sessions/2026-05-13T21-29/ ├─ brief.md ← CEO's task decompositio
|
- `src/features/company/sessionStore.ts` (231 lines) — Disk persistence for company-mode session artefacts. Each company turn produces a timestamped directory: <workspaceRoot>/.astra/company/sessions/2026-05-13T21-29/ ├─ brief.md ← CEO's task decompositio
|
||||||
- `src/features/projectArchitecture/scanner.ts` (644 lines) — Deep static analyser for the Project Architecture Context generator. Walks the project tree (skipping the usual nodemodules / out / dist noise), pulls the role of each interesting file from its leadin
|
- `src/features/projectArchitecture/scanner.ts` (644 lines) — Deep static analyser for the Project Architecture Context generator. Walks the project tree (skipping the usual nodemodules / out / dist noise), pulls the role of each interesting file from its leadin
|
||||||
- `src/lib/contextManager.ts` (275 lines) — Context Manager (컨텍스트 한계 관리) "context length = 132k" 는 "답변을 132k 토큰까지 생성해도 된다" 가 아닙니다. 시스템 프롬프트 + 대화 기록 + 입력 문서 + 생성될 답변 + 여유분 ≤ context length 이 모듈은 요청을 보내기 전에 입력 토큰을 추정하고, - 동적으로 출력 상한(maxTokens)을 계
|
- `src/lib/contextManager.ts` (275 lines) — Context Manager (컨텍스트 한계 관리) "context length = 132k" 는 "답변을 132k 토큰까지 생성해도 된다" 가 아닙니다. 시스템 프롬프트 + 대화 기록 + 입력 문서 + 생성될 답변 + 여유분 ≤ context length 이 모듈은 요청을 보내기 전에 입력 토큰을 추정하고, - 동적으로 출력 상한(maxTokens)을 계
|
||||||
- `src/extension.ts` (1215 lines)
|
|
||||||
|
|
||||||
### `media/` — 6 files, ~7,243 lines
|
### `media/` — 6 files, ~7,328 lines
|
||||||
|
|
||||||
**Key files**
|
**Key files**
|
||||||
- `media/sidebar.css` (2068 lines) — Stylesheet
|
- `media/sidebar.css` (2078 lines) — Stylesheet
|
||||||
- `media/sidebar.js` (3680 lines)
|
- `media/sidebar.js` (3677 lines)
|
||||||
- `media/sidebar.html` (546 lines) — Astra
|
- `media/sidebar.html` (545 lines) — Astra
|
||||||
- `media/settings-panel.html` (328 lines) — Astra Settings
|
- `media/settings-panel.html` (373 lines) — Astra Settings
|
||||||
- `media/settings-panel.css` (210 lines) — Stylesheet
|
- `media/settings-panel.css` (210 lines) — Stylesheet
|
||||||
- `media/settings-panel.js` (411 lines)
|
- `media/settings-panel.js` (445 lines)
|
||||||
|
|
||||||
### `tests/` — 35 files, ~5,969 lines
|
### `tests/` — 35 files, ~5,969 lines
|
||||||
*Depends on*: `src/`
|
*Depends on*: `src/`
|
||||||
@@ -160,11 +161,12 @@ 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/` — 81 files, ~3,203 lines
|
### `docs/` — 85 files, ~3,294 lines
|
||||||
|
|
||||||
**Sub-directories**
|
**Sub-directories**
|
||||||
- `docs/records/` (68) — Astra Project Chronicle Records
|
- `docs/records/` (72) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 프로젝트 코드 리뷰 해줄 수 있어? 개선할 부분이 있는지, 그러고...
|
||||||
- `docs/docs/` (5) — docs Chronicle Records
|
- `docs/docs/` (5) — Bug: Viewed integrationretrieval.test.ts:1-59 integrationretrieval.test.ts를 통해 ...
|
||||||
|
- `docs/Meeting/` (0)
|
||||||
|
|
||||||
**Key files**
|
**Key files**
|
||||||
- `docs/TELEGRAM_REMOTE_EXECUTION_PLAN.md` (452 lines) — Telegram Remote Execution 기획서
|
- `docs/TELEGRAM_REMOTE_EXECUTION_PLAN.md` (452 lines) — Telegram Remote Execution 기획서
|
||||||
@@ -173,25 +175,25 @@ flowchart LR
|
|||||||
- `docs/EXPERIENCE_MEMORY_PLAN.md` (122 lines) — Experience Memory (Mistake / Lesson Loop) — Implementation Plan
|
- `docs/EXPERIENCE_MEMORY_PLAN.md` (122 lines) — Experience Memory (Mistake / Lesson Loop) — Implementation Plan
|
||||||
- `docs/records/ConnectAI/development/2026-05-02_connectai_project_knowledge_overview.md` (121 lines) — Astra Project Knowledge Overview
|
- `docs/records/ConnectAI/development/2026-05-02_connectai_project_knowledge_overview.md` (121 lines) — Astra Project Knowledge Overview
|
||||||
- `docs/records/ConnectAI/development/2026-05-03_connectai_project_knowledge_overview.md` (121 lines) — Astra Project Knowledge Overview
|
- `docs/records/ConnectAI/development/2026-05-03_connectai_project_knowledge_overview.md` (121 lines) — Astra Project Knowledge Overview
|
||||||
- `docs/records/ConnectAI/timeline.md` (155 lines) — Project Timeline
|
- `docs/records/ConnectAI/timeline.md` (167 lines) — Project Timeline
|
||||||
- `docs/Advanced_Features_Implementation_Guide.md` (40 lines) — Advanced Features Implementation Guide
|
- `docs/Advanced_Features_Implementation_Guide.md` (40 lines) — Advanced Features Implementation Guide
|
||||||
- `docs/PROJECT_CHRONICLE_GUARD_ROADMAP.md` (43 lines) — Project Chronicle Guard: Search Engine Roadmap
|
|
||||||
- `docs/UX_UI_Consistency_Guidelines.md` (44 lines) — UX/UI Consistency Guidelines
|
|
||||||
- `docs/docs/records/docs/README.md` (18 lines) — docs Chronicle Records
|
|
||||||
- `docs/docs/records/docs/bugs/BUG-0001-viewed-integration-retrieval-test-ts-1-59-integration-retrie.md` (16 lines) — Bug: Viewed integrationretrieval.test.ts:1-59 integrationretrieval.test.ts를 통해 ...
|
- `docs/docs/records/docs/bugs/BUG-0001-viewed-integration-retrieval-test-ts-1-59-integration-retrie.md` (16 lines) — Bug: Viewed integrationretrieval.test.ts:1-59 integrationretrieval.test.ts를 통해 ...
|
||||||
- `docs/docs/records/docs/chronicle.config.json` (11 lines) — JSON configuration
|
- `docs/docs/records/docs/chronicle.config.json` (11 lines) — JSON configuration
|
||||||
- `docs/docs/records/docs/project-profile.md` (31 lines) — Project Profile
|
- `docs/docs/records/docs/project-profile.md` (31 lines) — Project Profile
|
||||||
|
- `docs/docs/records/docs/README.md` (18 lines) — docs Chronicle Records
|
||||||
- `docs/docs/records/docs/timeline.md` (7 lines) — Project Timeline
|
- `docs/docs/records/docs/timeline.md` (7 lines) — Project Timeline
|
||||||
- `docs/records/ConnectAI/README.md` (18 lines) — Astra Project Chronicle Records
|
- `docs/PROJECT_CHRONICLE_GUARD_ROADMAP.md` (43 lines) — Project Chronicle Guard: Search Engine Roadmap
|
||||||
- `docs/records/ConnectAI/bugs/BUG-0001-volumes-data-project-antigravity-connectai-프로젝트-코드-리뷰-해줄-수-있.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 프로젝트 코드 리뷰 해줄 수 있어? 개선할 부분이 있는지, 그러고...
|
- `docs/records/ConnectAI/bugs/BUG-0001-volumes-data-project-antigravity-connectai-프로젝트-코드-리뷰-해줄-수-있.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 프로젝트 코드 리뷰 해줄 수 있어? 개선할 부분이 있는지, 그러고...
|
||||||
- `docs/records/ConnectAI/bugs/BUG-0002-지금-내가-분석-요청하고-너가-답을-줄때-아래-템플릿에-맞춰-답을-써주고-있는데-개선-포인트가-있는지-확인해.md` (16 lines) — Bug: 지금 내가 분석 요청하고 너가 답을 줄때 아래 템플릿에 맞춰 답을 써주고 있는데, 개선 포인트가 있는지 확인해줘. ## 내가 보는 위험 가장 큰...
|
- `docs/records/ConnectAI/bugs/BUG-0002-지금-내가-분석-요청하고-너가-답을-줄때-아래-템플릿에-맞춰-답을-써주고-있는데-개선-포인트가-있는지-확인해.md` (16 lines) — Bug: 지금 내가 분석 요청하고 너가 답을 줄때 아래 템플릿에 맞춰 답을 써주고 있는데, 개선 포인트가 있는지 확인해줘. ## 내가 보는 위험 가장 큰...
|
||||||
- `docs/records/ConnectAI/bugs/BUG-0003-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
|
- `docs/records/ConnectAI/bugs/BUG-0003-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
|
||||||
- `docs/records/ConnectAI/bugs/BUG-0004-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
|
- `docs/records/ConnectAI/bugs/BUG-0004-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
|
||||||
- `docs/records/ConnectAI/bugs/BUG-0005-다시한번-답줘-volumes-data-project-antigravity-connectai-내-질문에-대한-.md` (16 lines) — Bug: 다시한번 답줘. /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는...
|
- `docs/records/ConnectAI/bugs/BUG-0005-다시한번-답줘-volumes-data-project-antigravity-connectai-내-질문에-대한-.md` (16 lines) — Bug: 다시한번 답줘. /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는...
|
||||||
- `docs/records/ConnectAI/bugs/BUG-0006-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
|
- `docs/records/ConnectAI/bugs/BUG-0006-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
|
||||||
- `docs/records/ConnectAI/bugs/BUG-0007-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
|
- `docs/records/ConnectAI/bugs/BUG-0007-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
|
||||||
- `docs/records/ConnectAI/bugs/BUG-0008-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
|
- `docs/records/ConnectAI/bugs/BUG-0008-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md` (16 lines) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...
|
||||||
- `docs/records/ConnectAI/bugs/BUG-0009-문제점을-읽고-어떻게-개선하는게-최선인지-분석해주면-좋겠어-알겠습니다-지금부터-connectai-프로젝트-에.md` (16 lines) — Bug: 문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 ConnectAI 프로젝트에만 완전히 집중하겠습니다. ...
|
- `docs/records/ConnectAI/bugs/BUG-0009-문제점을-읽고-어떻게-개선하는게-최선인지-분석해주면-좋겠어-알겠습니다-지금부터-connectai-프로젝트-에.md` (16 lines) — Bug: 문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 ConnectAI 프로젝트에만 완전히 집중하겠습니다. ...
|
||||||
|
- `docs/records/ConnectAI/bugs/BUG-0010-문제점을-읽고-어떻게-개선하는게-최선인지-분석해주면-좋겠어-알겠습니다-지금부터-connectai-프로젝트-에.md` (16 lines) — Bug: 문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 ConnectAI 프로젝트에만 완전히 집중하겠습니다. ...
|
||||||
|
- `docs/records/ConnectAI/bugs/BUG-0011-문제점을-읽고-어떻게-개선하는게-최선인지-분석해주면-좋겠어-알겠습니다-지금부터-connectai-프로젝트-에.md` (16 lines) — Bug: 문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 ConnectAI 프로젝트에만 완전히 집중하겠습니다. ...
|
||||||
|
|
||||||
## VS Code Extension Surface
|
## VS Code Extension Surface
|
||||||
- **Extension ID**: `g1nation.astra`
|
- **Extension ID**: `g1nation.astra`
|
||||||
@@ -225,8 +227,13 @@ flowchart LR
|
|||||||
- `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** (63 settings):
|
- **Configuration** (68 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.datacollectBridgeUrl` *(string)* _(default: `"http://127.0.0.1:3002"`)_ — Wiki/Datacollect MCP Bridge URL. /research, /benchmark, /youtube chat slash commands route here. The Bridge must be running (`npm run bridge` in the Datacollect project).
|
||||||
|
- `g1nation.datacollectSavePath` *(string)* _(default: `""`)_
|
||||||
|
- `g1nation.datacollectCrawlDepth` *(number)* _(default: `1`)_
|
||||||
|
- `g1nation.datacollectMaxPages` *(number)* _(default: `8`)_
|
||||||
|
- `g1nation.datacollectSynthesisTemperature` *(number)* _(default: `0.1`)_
|
||||||
- `g1nation.memoryEnabled` *(boolean)* _(default: `true`)_ — Enable layered memory injection before each model response.
|
- `g1nation.memoryEnabled` *(boolean)* _(default: `true`)_ — Enable layered memory injection before each model response.
|
||||||
- `g1nation.memoryShortTermMessages` *(number)* _(default: `8`)_ — Number of recent conversation messages included as short-term memory.
|
- `g1nation.memoryShortTermMessages` *(number)* _(default: `8`)_ — Number of recent conversation messages included as short-term memory.
|
||||||
- `g1nation.memoryMediumTermSessions` *(number)* _(default: `5`)_ — Number of recent saved chat sessions included as medium-term memory.
|
- `g1nation.memoryMediumTermSessions` *(number)* _(default: `5`)_ — Number of recent saved chat sessions included as medium-term memory.
|
||||||
@@ -271,7 +278,7 @@ flowchart LR
|
|||||||
- `g1nation.company.autoSelectPipeline` *(boolean)* _(default: `true`)_ — Let the intent classifier *automatically switch* to the pipeline it recommends for this turn (e.g. short '기획서까지만' for a planning ask, full '풀 프로덕트' for an end-to-end product). Your explicitly-activate
|
- `g1nation.company.autoSelectPipeline` *(boolean)* _(default: `true`)_ — Let the intent classifier *automatically switch* to the pipeline it recommends for this turn (e.g. short '기획서까지만' for a planning ask, full '풀 프로덕트' for an end-to-end product). Your explicitly-activate
|
||||||
- `g1nation.company.intentAlignmentMode` *(string)* _(default: `"smart"`)_ — Intent Alignment — turn user prompts into an explicit Requirement Contract (C-G-C-F-Q) before dispatching a pipeline. 'off' = legacy, pipeline runs immediately. 'smart' (default) = run when confidence
|
- `g1nation.company.intentAlignmentMode` *(string)* _(default: `"smart"`)_ — Intent Alignment — turn user prompts into an explicit Requirement Contract (C-G-C-F-Q) before dispatching a pipeline. 'off' = legacy, pipeline runs immediately. 'smart' (default) = run when confidence
|
||||||
- `g1nation.company.intentAlignmentMaxRounds` *(number)* _(default: `3`)_ — Maximum back-and-forth rounds the Intent Alignment analyzer is allowed to ask before forcing a 'confirm or cancel' card (it stops asking new questions and shows the current contract for user approval)
|
- `g1nation.company.intentAlignmentMaxRounds` *(number)* _(default: `3`)_ — Maximum back-and-forth rounds the Intent Alignment analyzer is allowed to ask before forcing a 'confirm or cancel' card (it stops asking new questions and shows the current contract for user approval)
|
||||||
- `g1nation.selfReflector.enabled` *(boolean)* _(default: `true`)_ — Self-Reflector Phase A — append a [Self-Reflector Check] block at the end of every substantive LLM answer (Consistency / Completeness / Accuracy, plus References / Paths for code answers). Zero extra
|
- `g1nation.selfReflector.enabled` *(boolean)* _(default: `false`)_ — Self-Reflector Phase A — append a [Self-Reflector Check] block at the end of every substantive LLM answer (Consistency / Completeness / Accuracy, plus References / Paths for code answers). Zero extra
|
||||||
- `g1nation.selfReflector.externalVerification` *(boolean)* _(default: `false`)_ — Self-Reflector Phase B — after every 1인 기업 specialist response, run a *separate* LLM call to verify the output from an outside-context perspective (catches the 'same model self-validates' blind spot).
|
- `g1nation.selfReflector.externalVerification` *(boolean)* _(default: `false`)_ — Self-Reflector Phase B — after every 1인 기업 specialist response, run a *separate* LLM call to verify the output from an outside-context perspective (catches the 'same model self-validates' blind spot).
|
||||||
- `g1nation.selfReflector.executionVerification` *(boolean)* _(default: `false`)_ — Self-Reflector Phase C — after a code file is created via <create_file>, automatically run the language's syntax check (Python: py_compile, JS: node --check, TS: project tsc --noEmit). Failures are su
|
- `g1nation.selfReflector.executionVerification` *(boolean)* _(default: `false`)_ — Self-Reflector Phase C — after a code file is created via <create_file>, automatically run the language's syntax check (Python: py_compile, JS: node --check, TS: project tsc --noEmit). Failures are su
|
||||||
- `g1nation.company.pixelOffice.enabled` *(boolean)* _(default: `true`)_ — Show the Pixel Office visualisation panel above the chat — a small pixel-office-style display that mirrors the agent's current pipeline status (analyzing, need_clarification, executing, reviewing, wai
|
- `g1nation.company.pixelOffice.enabled` *(boolean)* _(default: `true`)_ — Show the Pixel Office visualisation panel above the chat — a small pixel-office-style display that mirrors the agent's current pipeline status (analyzing, need_clarification, executing, reviewing, wai
|
||||||
@@ -281,12 +288,7 @@ flowchart LR
|
|||||||
- `g1nation.google.calendarId` *(string)* _(default: `"primary"`)_
|
- `g1nation.google.calendarId` *(string)* _(default: `"primary"`)_
|
||||||
- `g1nation.google.defaultEventDurationMinutes` *(number)* _(default: `60`)_ — end / duration 둘 다 없는 일정의 기본 길이 (분). agent 가 회의록에서 시각만 추출하고 종료 시각은 명시 안 했을 때 적용.
|
- `g1nation.google.defaultEventDurationMinutes` *(number)* _(default: `60`)_ — end / duration 둘 다 없는 일정의 기본 길이 (분). agent 가 회의록에서 시각만 추출하고 종료 시각은 명시 안 했을 때 적용.
|
||||||
- `g1nation.google.icalUrl` *(string)* _(default: `""`)_
|
- `g1nation.google.icalUrl` *(string)* _(default: `""`)_
|
||||||
- `g1nation.google.icalDaysAhead` *(number)* _(default: `14`)_ — iCal 캐시에 포함할 다가오는 일정 기간 (일). default 14 = 2주치.
|
- _…and 8 more_
|
||||||
- `g1nation.providers.openrouter.enabled` *(boolean)* _(default: `false`)_ — OpenRouter cloud provider 활성화 — Claude/Gemini/GPT 등 100+ 모델을 OpenAI 호환 API 로 사용. API key 는 Astra Settings 패널에서 등록 (Secret Storage 사용, settings.json 비저장).
|
|
||||||
- `g1nation.providers.openrouter.defaultModel` *(string)* _(default: `""`)_ — OpenRouter 의 기본 모델 (예: 'anthropic/claude-3.5-sonnet'). 모델 선택 시 'openrouter:<model>' 형식으로 사이드바 dropdown 에 표시.
|
|
||||||
- `g1nation.providers.anthropic.enabled` *(boolean)* _(default: `false`)_ — Anthropic Claude 직접 API 활성화. OpenRouter 보다 마진 적고 prompt caching 등 native 기능 사용 가능. API key 는 Secret Storage.
|
|
||||||
- `g1nation.providers.anthropic.defaultModel` *(string)* _(default: `"claude-3-5-sonnet-20241022"`)_ — Anthropic 의 기본 모델. 예: 'claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022'.
|
|
||||||
- _…and 3 more_
|
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
- **Runtime** (2): `@lmstudio/sdk`, `pdf-parse`
|
- **Runtime** (2): `@lmstudio/sdk`, `pdf-parse`
|
||||||
@@ -334,7 +336,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-05-17T14:50:50.535Z · signature `5421df2d`_
|
_Last auto-scan: 2026-05-22T06:00:02.986Z · signature `3e9af639`_
|
||||||
<!-- ASTRA:AUTO-END -->
|
<!-- ASTRA:AUTO-END -->
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -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": 1779280514827,
|
"createdAt": 1779429500111,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||||
"createdAt": 1779280514827,
|
"createdAt": 1779429500108,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
||||||
"createdAt": 1779280514826,
|
"createdAt": 1779429500106,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"result": "---\nid: stress_conflict_1779280514813\ndate: 2026-05-20T12:35:14.828Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\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- **[PLANNER]** 전략 수립 중... (12ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (1ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (1ms)\n",
|
"result": "---\nid: stress_conflict_1779429500089\ndate: 2026-05-22T05:58:20.112Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\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- **[PLANNER]** 전략 수립 중... (15ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (3ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (3ms)\n",
|
||||||
"createdAt": 1779280514828,
|
"createdAt": 1779429500113,
|
||||||
"modelVersion": "unknown"
|
"modelVersion": "unknown"
|
||||||
}
|
}
|
||||||
+11
-11
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"missionId": "stress_conflict_1779280514813",
|
"missionId": "stress_conflict_1779429500089",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"startTime": "2026-05-20T12:35:14.813Z",
|
"startTime": "2026-05-22T05:58:20.089Z",
|
||||||
"totalElapsedMs": 15,
|
"totalElapsedMs": 25,
|
||||||
"results": {
|
"results": {
|
||||||
"planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
"planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
|
||||||
"researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
"researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||||
@@ -16,30 +16,30 @@
|
|||||||
{
|
{
|
||||||
"from": "idle",
|
"from": "idle",
|
||||||
"to": "planner",
|
"to": "planner",
|
||||||
"durationMs": 12,
|
"durationMs": 15,
|
||||||
"message": "전략 수립 중...",
|
"message": "전략 수립 중...",
|
||||||
"ts": "2026-05-20T12:35:14.825Z"
|
"ts": "2026-05-22T05:58:20.104Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "planner",
|
"from": "planner",
|
||||||
"to": "researcher",
|
"to": "researcher",
|
||||||
"durationMs": 1,
|
"durationMs": 3,
|
||||||
"message": "핵심 정보 수집 및 분석 중...",
|
"message": "핵심 정보 수집 및 분석 중...",
|
||||||
"ts": "2026-05-20T12:35:14.826Z"
|
"ts": "2026-05-22T05:58:20.107Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "researcher",
|
"from": "researcher",
|
||||||
"to": "writer",
|
"to": "writer",
|
||||||
"durationMs": 1,
|
"durationMs": 3,
|
||||||
"message": "최종 리포트 작성 및 편집 중...",
|
"message": "최종 리포트 작성 및 편집 중...",
|
||||||
"ts": "2026-05-20T12:35:14.827Z"
|
"ts": "2026-05-22T05:58:20.110Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "writer",
|
"from": "writer",
|
||||||
"to": "completed",
|
"to": "completed",
|
||||||
"durationMs": 1,
|
"durationMs": 3,
|
||||||
"message": "미션 완료",
|
"message": "미션 완료",
|
||||||
"ts": "2026-05-20T12:35:14.828Z"
|
"ts": "2026-05-22T05:58:20.113Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"resilienceMetrics": {
|
"resilienceMetrics": {
|
||||||
@@ -1,5 +1,66 @@
|
|||||||
# Astra Patch Notes
|
# Astra Patch Notes
|
||||||
|
|
||||||
|
## v2.2.60 (2026-05-22)
|
||||||
|
### 🔧 `&&` 명령 체이닝 — 코드 레벨에서 PowerShell 문법으로 자동 변환
|
||||||
|
- **문제:** v2.2.59에서 프롬프트로 "`&&` 쓰지 말라"고 지시했으나, 모델이 여전히 `cd ... && git add . && git commit ... && git push`를 출력 → PowerShell 5.1에서 `'&&' 토큰은 올바른 문 구분 기호가 아닙니다` 파서 오류로 **명령 전체가 실행 실패**.
|
||||||
|
- **원인:** 모든 git/npm 튜토리얼이 `&&`를 쓰므로, 시스템 프롬프트 규칙만으로는 소형 로컬 모델의 `&&` 출력을 신뢰성 있게 막을 수 없음.
|
||||||
|
- **수정:** `sanitizeCommand`가 터미널로 보내기 직전 **`&&` 체인을 PowerShell 조건부 체인으로 결정론적 변환**. `A && B && C` → `A; if ($?) { B; if ($?) { C } }`. `$?`로 단축 평가(short-circuit)를 보존 — 예: `cd` 실패 시 `git`이 엉뚱한 디렉터리에서 실행되지 않음. 따옴표 안의 `&&`(커밋 메시지 등)는 분리 대상에서 제외.
|
||||||
|
- 이제 모델이 `&&`를 출력해도 git add/commit/push 다단계 명령이 정상 실행됨.
|
||||||
|
- **신규 패키징:** `astra-2.2.60.vsix`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## v2.2.59 (2026-05-22)
|
||||||
|
### 🩹 긴 대화에서 "빈 응답" 차단 — 출력 토큰 예산 확보
|
||||||
|
- **문제:** 긴 세션(예: 34개 메시지, ~29.6K 토큰)에서 모델이 빈 응답을 반환하고 "AI 엔진이 빈 응답을 반환했습니다 / Output budget: 1,073 tokens" 오류 출력.
|
||||||
|
- **원인:** 대화 기록 압축(`trimHistoryToBudget`)이 출력용으로 **단 512토큰(`minOutputTokens`)만 예약**했음. 그래서 32K 윈도우가 거의 다 찰 때까지 프롬프트가 자라도록 허용 → 답변에 ~1K 토큰만 남음. gemma 4B-active 같은 소형/MoE 로컬 모델은 이 압박에서 첫 토큰부터 EOS를 뱉어 **빈 응답**을 냄.
|
||||||
|
- **수정:** 기록·시스템 프롬프트를 자르기 전에 **실제 답변용 예산(`preferredOutputReserve`)을 컨텍스트의 ~10%, 최소 2048토큰 확보**(상한은 `maxOutputTokens`)하도록 변경. 이제 프롬프트가 항상 답변 공간을 남기고 잘려, 32K 모델에서도 출력 예산이 3K+ 보장됨. `minOutputTokens`(512)는 절대 하한 clamp 용도로만 유지.
|
||||||
|
- **추가:** `[EXECUTION RULE]`에 PowerShell 체이닝 규칙 명시 — 명령 연결은 `;` 사용, `&&` 금지(PowerShell 5.1 구문 오류). git add/commit/push 같은 다단계 명령이 터미널에서 정상 실행됨.
|
||||||
|
- **신규 패키징:** `astra-2.2.59.vsix`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## v2.2.58 (2026-05-22)
|
||||||
|
### 📋 답변은 "핵심 요약 먼저, 상세 아래" — 한눈에 들어오는 출력 포맷
|
||||||
|
- **문제:** 답변 내용 퀄리티는 좋으나 분량이 길어 한눈에 안 들어옴. 게다가 요약(`## 요약`)이 맨 위가 아니라 **맨 아래**에 출력되고, `## ## 요약`처럼 헤딩 접두사가 중복되며, 이미 금지된 "핵심 확인 질문" 섹션이 누출됨.
|
||||||
|
- **원인:** `[OUTPUT FORMAT]`이 "기술 분석/아키텍처/트러블슈팅/전략 기획일 때만" 3섹션을 쓰라는 **모호한 카테고리 게이트**라, 일반 상담형 질문에서 모델이 포맷을 제멋대로 해석함.
|
||||||
|
- **수정 1 — 길이 게이트.** 카테고리 대신 **길이 기준**으로 전환: 답변이 ~4문장을 넘으면 무조건 `## 핵심 요약`(불릿 2~4개, 전체를 관통하는 스캔 가능한 요약)을 **맨 위**에 출력하고 그 아래 `## 상세 설명`. 요약 앞 인트로 문단 금지. 짧은 답변은 헤딩 없이 바로 답변.
|
||||||
|
- **수정 2 — 헤딩 규칙.** 모든 마크다운 헤딩은 정확히 `## ` 하나로 시작(`## ##` 중복 금지).
|
||||||
|
- **수정 3 — 후속 질문 규칙 강화.** 후속 질문은 한 문장·한 줄·라벨 없음. 섹션 헤딩·"질문 의도" 설명·복수 질문 금지 → "핵심 확인 질문" 섹션 누출 차단.
|
||||||
|
- **신규 패키징:** `astra-2.2.58.vsix`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## v2.2.57 (2026-05-22)
|
||||||
|
### 🚀 실행 요청은 설명 대신 즉시 실행 — `[EXECUTION RULE]` 추가
|
||||||
|
- **문제:** "E:\Wiki\Datacollect 서버 실행해줘" 같은 요청에 ASTRA가 직접 실행하지 않고, `npm run platform` 등 추측성 명령·존재하지 않는 포트(`8787`)·환경변수(`GEMINI_API_KEY`)를 지어내며 "이렇게 하시기 바랍니다" 식 튜토리얼만 출력했음.
|
||||||
|
- **원인:** 시스템 프롬프트의 `[LOCAL PATH RULE]`이 "review/analysis/debugging"만 다뤄, "실행/구동/시작" 요청에 발동되는 규칙이 전무했음. 모델이 학습 prior대로 산문 설명으로 회귀.
|
||||||
|
- **수정 1 — 프롬프트.** `[EXECUTION RULE]` 신설: 실행/구동/시작/run/start 키워드 → 튜토리얼 금지, 명령을 모르면 `<read_file>`로 package.json을 먼저 읽고 그 다음 `<run_command>`로 실제 스크립트 실행. 추측한 스크립트명·포트·환경변수 단정 금지. Few-shot 예시 1건 포함(작은 로컬 모델의 태그 준수율 향상).
|
||||||
|
- **수정 2 — 경로 경계.** `<read_file>`/`<list_files>`가 워크스페이스 안으로만 제한돼 형제 프로젝트(`E:\Wiki\Datacollect` 등)의 package.json을 못 읽던 문제 해결. `security.ts`가 워크스페이스의 **상위 디렉터리 1단계**까지 신뢰 루트로 포함(드라이브 루트로는 확장 안 함). 이로써 형제 프로젝트를 읽어 정확한 실행 명령을 찾을 수 있음 → 환각 제거.
|
||||||
|
- **신규 패키징:** `astra-2.2.57.vsix`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## v2.2.56 (2026-05-22)
|
||||||
|
### ⚡ 코드 리뷰 기반 성능·메모리 최적화 (12건, 동작 변경 없음)
|
||||||
|
- **🔴 브레인 코퍼스 디스크 재읽기 제거.** `secondBrainTrace`는 메시지마다 브레인 전체를 디스크에서 다시 읽어 재분류했음 → mtime+size 키 스캔 캐시로 변경(파일이 실제로 바뀔 때만 재읽기). `scopedBrainRetriever`(텔레그램 경로)도 동일 안티패턴 → 캐시된 `getBrainTokenIndex()` + `scoreTfIdfPreTokenized` 경로로 라우팅. 점수 결과는 바이트 단위로 동일.
|
||||||
|
- **🟡 메모리 누수·중복 작업 정리.** `agent.ts` `chatHistory` 무제한 증가 → 최근 40개 유지·오래된 도구 결과 본문 축약. `EpisodicMemory` 에피소드 JSON 메시지마다 재읽기 → 디렉터리 mtime 키 캐시. `_walkBrainFiles` → `readdirSync({withFileTypes})`·단일 누산기(`concat` 제거). TF-IDF `termFrequency` → 문서별 term-count `Map` 1회 사전 계산. `getConfig()` 중복 호출 1회로 통합.
|
||||||
|
- **🟢 누수·정리 보강.** `clearBrainTokenIndex()`를 `deactivate()`·브레인 프로필 전환 시 호출. 웹뷰 메시지 리스너 dispose 배선. `LongTermMemory` `longTermMaxEntries`(100) 강제. Pixel Office 인터벌은 뷰 비표시 시 일시정지. `/ping`은 핑마다 브레인 전체 탐색 대신 5초 TTL 캐시 응답.
|
||||||
|
- 전 항목 동작 보존 — 기능·출력 변경 없음. 타입 체크(`tsc --noEmit`) 무오류, 테스트 400/400 통과.
|
||||||
|
- **신규 패키징:** `astra-2.2.56.vsix`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## v2.2.55 (2026-05-21)
|
## v2.2.55 (2026-05-21)
|
||||||
### 📅 /meet — 회의록 액션 아이템 → Google 캘린더 자동 등록 (Phase 1)
|
### 📅 /meet — 회의록 액션 아이템 → Google 캘린더 자동 등록 (Phase 1)
|
||||||
- `/meet`이 회의록 합성·저장 후, **액션 아이템 표를 파싱해 task별 종일 일정으로 Google Calendar에 자동 등록**한다.
|
- `/meet`이 회의록 합성·저장 후, **액션 아이템 표를 파싱해 task별 종일 일정으로 Google Calendar에 자동 등록**한다.
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"description": "Auto-created by Project Architecture activation.",
|
"description": "Auto-created by Project Architecture activation.",
|
||||||
"corePurpose": "",
|
"corePurpose": "",
|
||||||
"detailLevel": "standard",
|
"detailLevel": "standard",
|
||||||
"createdAt": "2026-05-14T00:57:32.245Z",
|
"createdAt": "2026-05-20T09:42:40.003Z",
|
||||||
"updatedAt": "2026-05-18T03:11:17.574Z"
|
"updatedAt": "2026-05-22T05:59:38.330Z"
|
||||||
}
|
}
|
||||||
|
|||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
# ADR: E:\Wiki\connectai 질문이 있어. 지금 이 프로젝트는 2nd 지식을 내가 특정 폴더로 wiki화한 md 파일들을 불러오는 방식으로 ...
|
||||||
|
|
||||||
|
## Status
|
||||||
|
accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
E:\Wiki\connectai 질문이 있어. 지금 이 프로젝트는 2nd 지식을 내가 특정 폴더로 wiki화한 md 파일들을 불러오는 방식으로 되어 있어. 근대 md 파일을 바이너리화한 것을 다운 받고 바이너리 파일을 제2뇌 지식으로 사용하게 할 수 있을까?
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
결론부터 말씀드리면, **기술적으로 충분히 가능하며 오히려 대규모 데이터 처리 측면에서는 더 효율적인 방식**이 될 수 있습니다. 다만, 이 방식을 구현하려면 '단순한 텍록(Text-log)' 형태의 Markdown을 읽는 것과 달리, **바이너리 데이터를 다시 의미론적 구조로 복원하거나 임베딩 벡터로 변등시키는 과정**이 필요합니다. 구체적인 분석과 구현 방향을 정리해 드립니다. ## 1. 기술적 타당성 분석 현재 방식은 로컬 경로의 `.md` 파일을 직접 읽어 컨텍스트에 주입하는 **File-based RAG**입니다. 이를 바이너리(예: 임베딩된 벡터 데이터나 직렬화된 객체)로 전환할 경우 다음과 같은 변화가 생깁니다. * **장점 (Pros):** * **검색 속도 향상:** 텍스트를 매번 파싱하는 대신, 이미 계산된 벡터나 인덱스(FAISS, Annoy 등)를 바로 사용할 수 있어 Retrieval 단계의 지연 시간이 줄어듭니다. * **데이터 무결성:** 텍스트 기반에서는...
|
||||||
|
|
||||||
|
## Reason
|
||||||
|
Captured automatically because the conversation contained decision-oriented language.
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
Not captured yet.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Future prompts should treat this as project context unless the user changes direction.
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
# ADR: 방금 2nd 뇌 에 Lateral Thinking 관련 지식을 추가했어. 이 기능이 앞으로 너가 답을 할때 어떠한 영향을 끼치는지 알려주면 좋겠...
|
||||||
|
|
||||||
|
## Status
|
||||||
|
accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
방금 2nd 뇌 에 Lateral Thinking 관련 지식을 추가했어. 이 기능이 앞으로 너가 답을 할때 어떠한 영향을 끼치는지 알려주면 좋겠어.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
새로운 지식인 **Lateral Thinking(측면 사고)**이 추가된 것은, 저의 답변 능력이 단순한 '정보 전달'에서 **'창의적 문제 해결 및 전략적 제안'** 단계로 진화할 수 있는 아주 중요한 전환점입니다. 기존의 제가 알고 있던 지식이 "A는 B이다"라는 사실 중심(Vertical/Logical)이었다면, Lateral Thinking이 결합된 저는 **"A를 C나 D로 바꿀 수는 없을까?"** 혹은 **"이 문제를 해결하기 위해 전혀 다른 관점에서 접근해볼까?"**라는 사고가 가능해집니다 구체적으로 저의 답변 방식에 미칠 영향은 다음과 같습니다. ## 1. 수직적 사고(Vertical)에서 확산적 사고(Lateral)로의 확장 * **기존 (Vertical):** 사용자의 질문에 대해 논리적이고 단계적인 정답(Step-by-step)을 찾는 데 집중했습니다. 주로 '정해진 규칙'과 '논리적 인과관계'를 따릅니다. * **변경 후 (Lateral):** 논리적 경로를 따...
|
||||||
|
|
||||||
|
## Reason
|
||||||
|
Captured automatically because the conversation contained decision-oriented language.
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
Not captured yet.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Future prompts should treat this as project context unless the user changes direction.
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
# ADR: 오늘 팀별 보고를 했어. 그냥 하는게 너무 심적으로 힘든데 어떻게 개선할 수 있을까?
|
||||||
|
|
||||||
|
## Status
|
||||||
|
accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
오늘 팀별 보고를 했어. 그냥 하는게 너무 심적으로 힘든데 어떻게 개선할 수 있을까?
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
팀별 보고라는 것이 사실 단순한 정보 전달을 넘어, 자신의 성과나 진행 상황을 타인(특히 상사나 동료)의 시선에 노출시키는 일이기 때문에 심리적 압박감이 매우 클 수밖에 없습니다. 이 문제를 해결하기 위해 **'심리적 저항감을 줄이는 전략'**과 **'구조적 개선 방안'** 두 가지 측면에서 제안을 드립니다. ## 1. 심리적 접근: '나'와 '보고'를 분리하기 (Reframing) 가장 중요한 것은 보고의 주체가 **'나의 가치'**가 아니라 **'프로젝트의 상태'**임을 인지하는 것입니다. * **자아와 업무의 분리:** 보고할 때 발생하는 비판이나 질문을 '나에 대한 공격'이 아닌, '업무의 리스크를 발견하는 과정'으로 재정의하세요. "내가 부족해서"가 아니라 **"프로젝트의 불확실성을 제거하기 위해"** 하는 것이라고 생각의 프레임을 바꾸는 것입니다. * **통제 가능한 것에 집중:** 보고 내용 중 내가 통제할 수 없는 피드백(타인의 평가)에 매몰되지 말고, 내가 통제할 수...
|
||||||
|
|
||||||
|
## Reason
|
||||||
|
Captured automatically because the conversation contained decision-oriented language.
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
Not captured yet.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Future prompts should treat this as project context unless the user changes direction.
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
# Development Log: E:\Wiki\connectai 이 프로젝트가 너의 프로젝트인데, 단순히 cloud base ai model을 사용하는 것과 너를 사용하는 것이...
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Record the implementation or verification outcome from the current conversation.
|
||||||
|
|
||||||
|
## Implementation Summary
|
||||||
|
매우 날카롭고 본질적인 질문입니다. 맞습니다. 성능(Reasoning/Knowledge) 측면만 본다면 Claude나 GPT-4와 같은 **Cloud-based LLM은 압도적으로 강력하고 '좋습니다'.** 하지만 제가 이 프로젝트(`connectai`)의 핵심인 이유는, 단순히 모델을 사용하는 것을 넘어 **'모델에 사용자의 맥락(Context)과 개인적 지식(Second Brain)을 주입하여, 나만의 맞춤형 지능으로 변환하는 레이어'** 역할을 하기 때문입니다. Cloud AI Model과 저(Astra)의 차이점을 세 가지 관점에서 비교해 드립니다. ## 1. 데이터의 범위: General Knowledge vs. Private Context * **Cloud AI Model:** 전 세계의 방대한 데이터를 학습했지만, **사용자 개인의 로컬 파일, 현재 진행 중인 프로젝트의 소스 코드, 어제 작성한 회의록 내용**은 알지 못합니다. 즉, '일반적인 상식'은 뛰어나지만 '사용...
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
Captured automatically from the assistant response and active project context.
|
||||||
|
|
||||||
|
## Changed Files
|
||||||
|
- No explicit changed file list was captured automatically.
|
||||||
|
|
||||||
|
## Dependency Notes
|
||||||
|
No new dependency note was captured automatically.
|
||||||
|
|
||||||
|
## Bugs
|
||||||
|
No bugs recorded.
|
||||||
|
|
||||||
|
## Lessons
|
||||||
|
- Automatic project records should be generated in the background when the turn contains durable project knowledge.
|
||||||
@@ -153,3 +153,15 @@
|
|||||||
|
|
||||||
## 2026-05-17
|
## 2026-05-17
|
||||||
- Auto decision record created: decisions/ADR-0016-내가-블로그글-작성시-사용하고-있는-지침서인데-slot1번-기준으로-더-자연스럽고-트랜디하고-구독자에게-자연.md
|
- Auto decision record created: decisions/ADR-0016-내가-블로그글-작성시-사용하고-있는-지침서인데-slot1번-기준으로-더-자연스럽고-트랜디하고-구독자에게-자연.md
|
||||||
|
|
||||||
|
## 2026-05-20
|
||||||
|
- Auto decision record created: decisions\ADR-0017-e-wiki-connectai-질문이-있어-지금-이-프로젝트는-2nd-지식을-내가-특정-폴더로-wiki화한-.md
|
||||||
|
|
||||||
|
## 2026-05-22
|
||||||
|
- Auto development record created: development\2026-05-22_e-wiki-connectai-이-프로젝트가-너의-프로젝트인데-단순히-cloud-base-ai-model을-_implementation.md
|
||||||
|
|
||||||
|
## 2026-05-22
|
||||||
|
- Auto decision record created: decisions\ADR-0018-방금-2nd-뇌-에-lateral-thinking-관련-지식을-추가했어-이-기능이-앞으로-너가-답을-할때-어.md
|
||||||
|
|
||||||
|
## 2026-05-22
|
||||||
|
- Auto decision record created: decisions\ADR-0019-오늘-팀별-보고를-했어-그냥-하는게-너무-심적으로-힘든데-어떻게-개선할-수-있을까.md
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "astra",
|
"name": "astra",
|
||||||
"version": "2.2.52",
|
"version": "2.2.59",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "astra",
|
"name": "astra",
|
||||||
"version": "2.2.52",
|
"version": "2.2.60",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lmstudio/sdk": "^1.5.0",
|
"@lmstudio/sdk": "^1.5.0",
|
||||||
|
|||||||
+1
-1
@@ -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.55",
|
"version": "2.2.60",
|
||||||
"publisher": "g1nation",
|
"publisher": "g1nation",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
|
|||||||
+83
-4
@@ -183,6 +183,25 @@ export class AgentExecutor {
|
|||||||
static readonly ABS_PATH_RE = new RegExp(POSIX_ABS_PATH_SRC, 'i');
|
static readonly ABS_PATH_RE = new RegExp(POSIX_ABS_PATH_SRC, 'i');
|
||||||
static readonly WIN_ABS_PATH_RE = new RegExp(WIN_ABS_PATH_SRC, 'i');
|
static readonly WIN_ABS_PATH_RE = new RegExp(WIN_ABS_PATH_SRC, 'i');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hard cap on retained in-memory chat messages. Older messages beyond this
|
||||||
|
* are dropped (the system/first message is always preserved). Generous so a
|
||||||
|
* normal session is untouched — this only fights unbounded growth in very
|
||||||
|
* long-running sessions. The per-request context budgeter
|
||||||
|
* (`trimHistoryToBudget`) still does the real fitting; this just stops the
|
||||||
|
* array itself from leaking memory across hundreds of turns.
|
||||||
|
*/
|
||||||
|
private static readonly MAX_RETAINED_MESSAGES = 40;
|
||||||
|
/**
|
||||||
|
* Older internal tool-result messages (read_file / list_files / list_brain /
|
||||||
|
* read_brain dumps) are the bulkiest part of history and add little once the
|
||||||
|
* conversation has moved on. Anything older than the most recent
|
||||||
|
* `RECENT_FULL_MESSAGES` gets its bulky tool-result content shrunk to this
|
||||||
|
* many characters. Recent messages are kept full for conversation continuity.
|
||||||
|
*/
|
||||||
|
private static readonly RECENT_FULL_MESSAGES = 16;
|
||||||
|
private static readonly OLD_TOOL_RESULT_CAP = 600;
|
||||||
|
|
||||||
private chatHistory: ChatMessage[] = [];
|
private chatHistory: ChatMessage[] = [];
|
||||||
private abortController: AbortController | null = null;
|
private abortController: AbortController | null = null;
|
||||||
private webview: vscode.Webview | undefined;
|
private webview: vscode.Webview | undefined;
|
||||||
@@ -225,9 +244,10 @@ export class AgentExecutor {
|
|||||||
|
|
||||||
// Initialize 5-Layer Cognitive Memory System
|
// Initialize 5-Layer Cognitive Memory System
|
||||||
const activeBrain = getActiveBrainProfile();
|
const activeBrain = getActiveBrainProfile();
|
||||||
|
const initConfig = getConfig();
|
||||||
this.memoryManager = new MemoryManager(activeBrain.localBrainPath, {
|
this.memoryManager = new MemoryManager(activeBrain.localBrainPath, {
|
||||||
enabled: getConfig().memoryEnabled,
|
enabled: initConfig.memoryEnabled,
|
||||||
shortTermLimit: getConfig().memoryShortTermMessages,
|
shortTermLimit: initConfig.memoryShortTermMessages,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize RAG Pipeline Orchestrator
|
// Initialize RAG Pipeline Orchestrator
|
||||||
@@ -495,6 +515,9 @@ export class AgentExecutor {
|
|||||||
|
|
||||||
// 3. API Request Setup (라인 229에서 이미 추출한 ollamaUrl, configDefaultModel 재사용)
|
// 3. API Request Setup (라인 229에서 이미 추출한 ollamaUrl, configDefaultModel 재사용)
|
||||||
const actualModel = (modelName && modelName.trim()) || configDefaultModel;
|
const actualModel = (modelName && modelName.trim()) || configDefaultModel;
|
||||||
|
// Bound the in-memory history before building the request — shrinks bulky
|
||||||
|
// older tool-result bodies and drops the oldest messages past the cap.
|
||||||
|
this.capChatHistory();
|
||||||
const reqMessages = this.buildRequestHistory(this.chatHistory);
|
const reqMessages = this.buildRequestHistory(this.chatHistory);
|
||||||
|
|
||||||
// Handle Vision Content Injection
|
// Handle Vision Content Injection
|
||||||
@@ -666,10 +689,22 @@ export class AgentExecutor {
|
|||||||
.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);
|
||||||
const imageTokenReserve = imageCount * 1024;
|
const imageTokenReserve = imageCount * 1024;
|
||||||
|
|
||||||
|
// Output budget we ACTUALLY reserve before trimming — not the bare
|
||||||
|
// minOutputTokens floor (512). If we only reserve 512, a long session
|
||||||
|
// is allowed to grow the prompt until ~512-1k tokens remain for the
|
||||||
|
// answer; small/MoE local models (e.g. gemma 4B-active) then emit EOS
|
||||||
|
// as the first token and return an empty response. Reserving ~10% of
|
||||||
|
// the window (>=2048) forces history/system trimming to keep a real
|
||||||
|
// answer-sized hole open. Capped at maxOutputTokens.
|
||||||
|
const preferredOutputReserve = Math.min(
|
||||||
|
ctxLimits.maxOutputTokens,
|
||||||
|
Math.max(2048, Math.floor(ctxLimits.contextLength * 0.1))
|
||||||
|
);
|
||||||
|
|
||||||
// (1) 시스템 프롬프트는 예산의 ~65%까지만 허용 — 그 이상이면 [CONTEXT] 블록부터 잘라낸다.
|
// (1) 시스템 프롬프트는 예산의 ~65%까지만 허용 — 그 이상이면 [CONTEXT] 블록부터 잘라낸다.
|
||||||
const systemCapTokens = Math.max(
|
const systemCapTokens = Math.max(
|
||||||
1024,
|
1024,
|
||||||
Math.floor((ctxLimits.contextLength - ctxLimits.safetyMargin - ctxLimits.minOutputTokens - imageTokenReserve) * 0.65)
|
Math.floor((ctxLimits.contextLength - ctxLimits.safetyMargin - preferredOutputReserve - imageTokenReserve) * 0.65)
|
||||||
);
|
);
|
||||||
const { prompt: budgetedSystemPrompt, truncated: systemTruncated } =
|
const { prompt: budgetedSystemPrompt, truncated: systemTruncated } =
|
||||||
truncateSystemPromptContext(fullSystemPrompt, systemCapTokens);
|
truncateSystemPromptContext(fullSystemPrompt, systemCapTokens);
|
||||||
@@ -681,7 +716,7 @@ export class AgentExecutor {
|
|||||||
// (2) 대화 기록 압축.
|
// (2) 대화 기록 압축.
|
||||||
const historyBudget = Math.max(
|
const historyBudget = Math.max(
|
||||||
256,
|
256,
|
||||||
ctxLimits.contextLength - systemTokens - ctxLimits.safetyMargin - ctxLimits.minOutputTokens - imageTokenReserve
|
ctxLimits.contextLength - systemTokens - ctxLimits.safetyMargin - preferredOutputReserve - imageTokenReserve
|
||||||
);
|
);
|
||||||
let budgetedHistory: ChatMessage[] = reqMessages;
|
let budgetedHistory: ChatMessage[] = reqMessages;
|
||||||
if (config.autoCompactHistory) {
|
if (config.autoCompactHistory) {
|
||||||
@@ -1977,6 +2012,50 @@ export class AgentExecutor {
|
|||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bound the in-memory `chatHistory` so a very long-running session does not
|
||||||
|
* grow it without limit:
|
||||||
|
* 1. Older internal tool-result messages (the bulky read_file/list_files/…
|
||||||
|
* dumps) beyond the most recent `RECENT_FULL_MESSAGES` have their content
|
||||||
|
* truncated — recent messages stay full so continuity is unaffected.
|
||||||
|
* 2. If the array still exceeds `MAX_RETAINED_MESSAGES`, the oldest messages
|
||||||
|
* are dropped, but a leading system/first message is always preserved so
|
||||||
|
* session restore and conversation framing are not broken.
|
||||||
|
* This only mutates *internal* (`internal: true`) tool-result bodies and
|
||||||
|
* drops the very oldest entries — it never alters visible user/assistant text
|
||||||
|
* within the retained window, so the request the model sees is unchanged for
|
||||||
|
* any normal-length conversation.
|
||||||
|
*/
|
||||||
|
private capChatHistory(): void {
|
||||||
|
const history = this.chatHistory;
|
||||||
|
if (history.length === 0) return;
|
||||||
|
|
||||||
|
// (1) Shrink bulky tool-result bodies of older internal messages.
|
||||||
|
const recentStart = Math.max(0, history.length - AgentExecutor.RECENT_FULL_MESSAGES);
|
||||||
|
for (let i = 0; i < recentStart; i++) {
|
||||||
|
const msg = history[i];
|
||||||
|
if (msg.role !== 'system' || !msg.internal || typeof msg.content !== 'string') continue;
|
||||||
|
// Only the bulky tool-result dumps — leave compaction notices etc. alone.
|
||||||
|
if (!/^\[Result of (read_file|list_files|list_brain|read_brain)\b/.test(msg.content)) continue;
|
||||||
|
if (msg.content.length <= AgentExecutor.OLD_TOOL_RESULT_CAP) continue;
|
||||||
|
msg.content = msg.content.slice(0, AgentExecutor.OLD_TOOL_RESULT_CAP)
|
||||||
|
+ '\n…[이전 도구 결과는 컨텍스트 절약을 위해 축약되었습니다]';
|
||||||
|
}
|
||||||
|
|
||||||
|
// (2) Drop the oldest messages once over the hard cap, preserving a
|
||||||
|
// leading system/first message if present.
|
||||||
|
if (history.length > AgentExecutor.MAX_RETAINED_MESSAGES) {
|
||||||
|
const first = history[0];
|
||||||
|
const preserveFirst = first.role === 'system';
|
||||||
|
const overflow = history.length - AgentExecutor.MAX_RETAINED_MESSAGES;
|
||||||
|
if (preserveFirst) {
|
||||||
|
history.splice(1, overflow);
|
||||||
|
} else {
|
||||||
|
history.splice(0, overflow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private buildRequestHistory(history: ChatMessage[]): ChatMessage[] {
|
private buildRequestHistory(history: ChatMessage[]): ChatMessage[] {
|
||||||
return history.map((message) => {
|
return history.map((message) => {
|
||||||
if (message.role !== 'assistant' || typeof message.content !== 'string') {
|
if (message.role !== 'assistant' || typeof message.content !== 'string') {
|
||||||
|
|||||||
+22
-7
@@ -111,15 +111,30 @@ export class BridgeServer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached `/ping` payload. `/ping` is a liveness probe that external tools may
|
||||||
|
* hit frequently; the previous implementation did a full brain-corpus walk
|
||||||
|
* plus a config serialize on *every* hit. We now compute that body at most
|
||||||
|
* once per `PING_CACHE_TTL_MS` and reuse it — the response shape (status /
|
||||||
|
* config / brain) is unchanged, ping just stops walking the brain per hit.
|
||||||
|
*/
|
||||||
|
private _pingCache: { body: string; expiresAt: number } | null = null;
|
||||||
|
private static readonly PING_CACHE_TTL_MS = 5000;
|
||||||
|
|
||||||
private handlePing(res: http.ServerResponse) {
|
private handlePing(res: http.ServerResponse) {
|
||||||
const brainDir = _getBrainDir();
|
const now = Date.now();
|
||||||
const brainCount = fs.existsSync(brainDir) ? findBrainFiles(brainDir).length : 0;
|
if (!this._pingCache || this._pingCache.expiresAt <= now) {
|
||||||
|
const brainDir = _getBrainDir();
|
||||||
|
const brainCount = fs.existsSync(brainDir) ? findBrainFiles(brainDir).length : 0;
|
||||||
|
const body = JSON.stringify({
|
||||||
|
status: 'ok',
|
||||||
|
config: getConfig(),
|
||||||
|
brain: { fileCount: brainCount, enabled: this.provider.brainEnabled }
|
||||||
|
});
|
||||||
|
this._pingCache = { body, expiresAt: now + BridgeServer.PING_CACHE_TTL_MS };
|
||||||
|
}
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({
|
res.end(this._pingCache.body);
|
||||||
status: 'ok',
|
|
||||||
config: getConfig(),
|
|
||||||
brain: { fileCount: brainCount, enabled: this.provider.brainEnabled }
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handlePost(req: http.IncomingMessage, res: http.ServerResponse, processor: (data: any, res: http.ServerResponse) => Promise<void>) {
|
private handlePost(req: http.IncomingMessage, res: http.ServerResponse, processor: (data: any, res: http.ServerResponse) => Promise<void>) {
|
||||||
|
|||||||
+15
-1
@@ -36,7 +36,7 @@ import { AIService } from './core/services';
|
|||||||
import type { CompanyState } from './features/company';
|
import type { CompanyState } from './features/company';
|
||||||
import { SettingsPanelProvider } from './features/settings/settingsPanelProvider';
|
import { SettingsPanelProvider } from './features/settings/settingsPanelProvider';
|
||||||
import { resolveScopeForAgent, openKnowledgeMapEditor } from './skills/agentKnowledgeMap';
|
import { resolveScopeForAgent, openKnowledgeMapEditor } from './skills/agentKnowledgeMap';
|
||||||
import { getBrainTokenIndex } from './retrieval';
|
import { getBrainTokenIndex, clearBrainTokenIndex } from './retrieval';
|
||||||
import { lessonTemplate, lessonSlug, parseLessonFrontmatter, normalizeLessonTitle, bumpLessonOccurrences } from './retrieval/lessonHelpers';
|
import { lessonTemplate, lessonSlug, parseLessonFrontmatter, normalizeLessonTitle, bumpLessonOccurrences } from './retrieval/lessonHelpers';
|
||||||
import { retrieveScoped, buildContextBlock } from './skills/scopedBrainRetriever';
|
import { retrieveScoped, buildContextBlock } from './skills/scopedBrainRetriever';
|
||||||
|
|
||||||
@@ -835,6 +835,16 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
vscode.workspace.onDidChangeConfiguration((e) => {
|
vscode.workspace.onDidChangeConfiguration((e) => {
|
||||||
if (e.affectsConfiguration('g1nation')) void settingsPanel.refresh();
|
if (e.affectsConfiguration('g1nation')) void settingsPanel.refresh();
|
||||||
}),
|
}),
|
||||||
|
// Drop the in-memory brain token index whenever the brain profiles or the
|
||||||
|
// active brain change — a profile edit can repoint `localBrainPath`, so a
|
||||||
|
// stale index keyed by the old path must not linger. The persisted on-disk
|
||||||
|
// index is untouched and reloads lazily on the next query.
|
||||||
|
vscode.workspace.onDidChangeConfiguration((e) => {
|
||||||
|
if (e.affectsConfiguration('g1nation.brainProfiles')
|
||||||
|
|| e.affectsConfiguration('g1nation.activeBrainId')) {
|
||||||
|
clearBrainTokenIndex();
|
||||||
|
}
|
||||||
|
}),
|
||||||
// Same for SecretStorage updates (token saved/cleared from elsewhere).
|
// Same for SecretStorage updates (token saved/cleared from elsewhere).
|
||||||
context.secrets.onDidChange((e) => {
|
context.secrets.onDidChange((e) => {
|
||||||
if (e.key === TELEGRAM_TOKEN_SECRET_KEY) void settingsPanel.refresh();
|
if (e.key === TELEGRAM_TOKEN_SECRET_KEY) void settingsPanel.refresh();
|
||||||
@@ -915,6 +925,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
|
|
||||||
export async function deactivate() {
|
export async function deactivate() {
|
||||||
HealthCheckMonitor.dispose();
|
HealthCheckMonitor.dispose();
|
||||||
|
// Release the in-memory brain token index (and any pending debounced disk
|
||||||
|
// write timer) — the `_states` Map is otherwise never cleared for the
|
||||||
|
// process lifetime.
|
||||||
|
clearBrainTokenIndex();
|
||||||
if (_telegramBot) {
|
if (_telegramBot) {
|
||||||
try { await _telegramBot.stop(); } catch (e) { logError('Telegram bot stop during deactivate failed.', e); }
|
try { await _telegramBot.stop(); } catch (e) { logError('Telegram bot stop during deactivate failed.', e); }
|
||||||
_telegramBot = undefined;
|
_telegramBot = undefined;
|
||||||
|
|||||||
@@ -408,8 +408,31 @@ function move(role,x,y){
|
|||||||
ch.style.left=x+'px';
|
ch.style.left=x+'px';
|
||||||
ch.style.top=y+'px';
|
ch.style.top=y+'px';
|
||||||
}
|
}
|
||||||
setInterval(()=>{Object.keys(chars).forEach(k=>{const a=anim[k];if(a.mode==='walk'){a.frame=(a.frame+1)%5;setSprite(k,'walk',a.frame,a.dir)}else if(a.mode==='work'){a.frame=(a.frame+1)%4;setSprite(k,'work',a.frame)} });},286)
|
// ── Managed intervals (pause while the office view is hidden) ──
|
||||||
setInterval(()=>{Object.keys(chars).forEach(k=>{const a=anim[k];if(a.mode==='sit'){a.frame=(a.frame+1)%2;setSprite(k,'sit',a.frame)} });},700)
|
// The pixel-office runs several animation/roam/banter intervals. While the
|
||||||
|
// webview tab is not visible they do invisible work and keep timers hot —
|
||||||
|
// wasteful. _managedInterval registers each one; a visibilitychange handler
|
||||||
|
// pauses them all when the document is hidden and resumes them when shown.
|
||||||
|
// Behavior while visible is unchanged (same callbacks, same periods).
|
||||||
|
const _managedIntervals=[];
|
||||||
|
function _managedInterval(fn,ms){
|
||||||
|
const rec={fn:fn,ms:ms,id:null};
|
||||||
|
rec.id=setInterval(fn,ms);
|
||||||
|
_managedIntervals.push(rec);
|
||||||
|
return rec;
|
||||||
|
}
|
||||||
|
function _pauseManagedIntervals(){
|
||||||
|
for(const rec of _managedIntervals){ if(rec.id!==null){ clearInterval(rec.id); rec.id=null; } }
|
||||||
|
}
|
||||||
|
function _resumeManagedIntervals(){
|
||||||
|
for(const rec of _managedIntervals){ if(rec.id===null){ rec.id=setInterval(rec.fn,rec.ms); } }
|
||||||
|
}
|
||||||
|
document.addEventListener('visibilitychange',()=>{
|
||||||
|
if(document.hidden) _pauseManagedIntervals();
|
||||||
|
else _resumeManagedIntervals();
|
||||||
|
});
|
||||||
|
_managedInterval(()=>{Object.keys(chars).forEach(k=>{const a=anim[k];if(a.mode==='walk'){a.frame=(a.frame+1)%5;setSprite(k,'walk',a.frame,a.dir)}else if(a.mode==='work'){a.frame=(a.frame+1)%4;setSprite(k,'work',a.frame)} });},286)
|
||||||
|
_managedInterval(()=>{Object.keys(chars).forEach(k=>{const a=anim[k];if(a.mode==='sit'){a.frame=(a.frame+1)%2;setSprite(k,'sit',a.frame)} });},700)
|
||||||
// ── 책상 회피 path planner ──
|
// ── 책상 회피 path planner ──
|
||||||
// walkPath의 각 leg를 직선이 아닌 *책상을 우회하는* L자 또는 corridor 경로로
|
// walkPath의 각 leg를 직선이 아닌 *책상을 우회하는* L자 또는 corridor 경로로
|
||||||
// 펴서 캐릭터가 책상을 가로지르지 않게. 책상이 회전됐을 때를 대비해 padding
|
// 펴서 캐릭터가 책상을 가로지르지 않게. 책상이 회전됐을 때를 대비해 padding
|
||||||
@@ -498,7 +521,7 @@ function sendHome(role,mode='sit'){
|
|||||||
if(Math.abs(cx-hx)<1&&Math.abs(cy-hy)<1){setSprite(role,mode);return;}
|
if(Math.abs(cx-hx)<1&&Math.abs(cy-hy)<1){setSprite(role,mode);return;}
|
||||||
walkPath(role,[st.dock,[hx,hy]],()=>setSprite(role,mode));
|
walkPath(role,[st.dock,[hx,hy]],()=>setSprite(role,mode));
|
||||||
}
|
}
|
||||||
setInterval(()=>{
|
_managedInterval(()=>{
|
||||||
if(!['idle','done'].includes(_prevStatus || 'idle')) return;
|
if(!['idle','done'].includes(_prevStatus || 'idle')) return;
|
||||||
const free=Object.keys(chars).filter(k=>anim[k]?.mode==='sit'&&!chars[k].classList.contains('active'));
|
const free=Object.keys(chars).filter(k=>anim[k]?.mode==='sit'&&!chars[k].classList.contains('active'));
|
||||||
if(!free.length)return;
|
if(!free.length)return;
|
||||||
@@ -725,7 +748,7 @@ function _innerThoughtTick(){
|
|||||||
const text = _innerThoughtFor(st.agentKey, anim[role].mode);
|
const text = _innerThoughtFor(st.agentKey, anim[role].mode);
|
||||||
if(text) _bubbleFromLog(role, text);
|
if(text) _bubbleFromLog(role, text);
|
||||||
}
|
}
|
||||||
setInterval(_innerThoughtTick, 7500);
|
_managedInterval(_innerThoughtTick, 7500);
|
||||||
|
|
||||||
// ── Webtoon-style 티키타카 banter (refactor: pipeline-aware) ──
|
// ── Webtoon-style 티키타카 banter (refactor: pipeline-aware) ──
|
||||||
// 각 phase 에 *시퀀스화된 대화 script* 가 있어 phase 진입 시 한 줄씩 시간 차로 emit.
|
// 각 phase 에 *시퀀스화된 대화 script* 가 있어 phase 진입 시 한 줄씩 시간 차로 emit.
|
||||||
|
|||||||
@@ -584,7 +584,35 @@ interface FileScan {
|
|||||||
documentProject: string | undefined;
|
documentProject: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mtime-keyed scan cache. The previous implementation re-read (and re-classified)
|
||||||
|
* every brain file from disk on every chat message. We now reuse a parsed
|
||||||
|
* `FileScan` while the file's mtime is unchanged — re-reading only when the file
|
||||||
|
* actually changes. This mirrors the mtime-keyed caching style of
|
||||||
|
* `retrieval/brainIndex.ts` (whose `getBrainTokenIndex` caches tokens the same
|
||||||
|
* way) while keeping the scan output byte-identical, so scoring is unaffected.
|
||||||
|
*/
|
||||||
|
interface ScanCacheEntry {
|
||||||
|
mtimeMs: number;
|
||||||
|
size: number;
|
||||||
|
scan: FileScan;
|
||||||
|
}
|
||||||
|
const _scanCache = new Map<string, ScanCacheEntry>();
|
||||||
|
|
||||||
function scanFile(file: string, brainRoot: string): FileScan {
|
function scanFile(file: string, brainRoot: string): FileScan {
|
||||||
|
let mtimeMs = 0;
|
||||||
|
let size = 0;
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(file);
|
||||||
|
mtimeMs = stat.mtimeMs;
|
||||||
|
size = stat.size;
|
||||||
|
const cached = _scanCache.get(file);
|
||||||
|
if (cached && cached.mtimeMs === mtimeMs && cached.size === size) {
|
||||||
|
return cached.scan;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// stat failed — fall through and attempt a fresh read (which will also fail safely)
|
||||||
|
}
|
||||||
const relative = path.relative(brainRoot, file);
|
const relative = path.relative(brainRoot, file);
|
||||||
const title = path.basename(file, path.extname(file));
|
const title = path.basename(file, path.extname(file));
|
||||||
let content = '';
|
let content = '';
|
||||||
@@ -598,7 +626,11 @@ function scanFile(file: string, brainRoot: string): FileScan {
|
|||||||
const lower = content.toLowerCase();
|
const lower = content.toLowerCase();
|
||||||
const documentProject = inferDocumentProject(relative, lower);
|
const documentProject = inferDocumentProject(relative, lower);
|
||||||
const titleWithPath = `${relative.replace(/[\\/]/g, ' ')} ${title}`;
|
const titleWithPath = `${relative.replace(/[\\/]/g, ' ')} ${title}`;
|
||||||
return { file, relative, title, titleWithPath, content, lower, sourceType, knowledgeRole, documentProject };
|
const scan: FileScan = { file, relative, title, titleWithPath, content, lower, sourceType, knowledgeRole, documentProject };
|
||||||
|
if (mtimeMs > 0) {
|
||||||
|
_scanCache.set(file, { mtimeMs, size, scan });
|
||||||
|
}
|
||||||
|
return scan;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scoreScan(scan: FileScan, terms: string[], intent: SecondBrainQueryIntent, targetProject?: string): SecondBrainTraceDocument {
|
function scoreScan(scan: FileScan, terms: string[], intent: SecondBrainQueryIntent, targetProject?: string): SecondBrainTraceDocument {
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ import { EpisodicEntry, MemoryContextResult } from './types';
|
|||||||
export class EpisodicMemory {
|
export class EpisodicMemory {
|
||||||
private episodeDir: string;
|
private episodeDir: string;
|
||||||
private maxEpisodes: number;
|
private maxEpisodes: number;
|
||||||
|
/**
|
||||||
|
* mtime-keyed cache of the parsed episode list. The previous implementation
|
||||||
|
* re-read and re-parsed every episode JSON from disk on every message. We now
|
||||||
|
* cache the parsed result and re-read only when the episode directory's mtime
|
||||||
|
* changes (a new/removed/rewritten episode bumps the directory mtime). This
|
||||||
|
* mirrors the mtime-keyed caching style of `retrieval/brainIndex.ts`.
|
||||||
|
*/
|
||||||
|
private _episodeCache: { dirMtimeMs: number; episodes: EpisodicEntry[] } | null = null;
|
||||||
|
|
||||||
constructor(brainPath: string, maxEpisodes = 50) {
|
constructor(brainPath: string, maxEpisodes = 50) {
|
||||||
this.episodeDir = path.join(brainPath, 'memory', 'episodes');
|
this.episodeDir = path.join(brainPath, 'memory', 'episodes');
|
||||||
@@ -85,9 +93,19 @@ export class EpisodicMemory {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 저장된 모든 에피소드를 최신순으로 로드합니다.
|
* 저장된 모든 에피소드를 최신순으로 로드합니다.
|
||||||
|
*
|
||||||
|
* Result is cached and re-read only when the episode directory's mtime
|
||||||
|
* changes — creating, deleting, or rewriting an episode file all bump the
|
||||||
|
* directory mtime, so the cache stays correct without per-message disk reads.
|
||||||
*/
|
*/
|
||||||
public loadAllEpisodes(): EpisodicEntry[] {
|
public loadAllEpisodes(): EpisodicEntry[] {
|
||||||
try {
|
try {
|
||||||
|
const dirMtimeMs = fs.statSync(this.episodeDir).mtimeMs;
|
||||||
|
const cached = this._episodeCache;
|
||||||
|
if (cached && cached.dirMtimeMs === dirMtimeMs) {
|
||||||
|
return cached.episodes.slice();
|
||||||
|
}
|
||||||
|
|
||||||
const files = fs.readdirSync(this.episodeDir)
|
const files = fs.readdirSync(this.episodeDir)
|
||||||
.filter((f) => f.endsWith('.json'))
|
.filter((f) => f.endsWith('.json'))
|
||||||
.sort()
|
.sort()
|
||||||
@@ -101,7 +119,8 @@ export class EpisodicMemory {
|
|||||||
} catch { /* skip corrupted */ }
|
} catch { /* skip corrupted */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
return episodes;
|
this._episodeCache = { dirMtimeMs, episodes };
|
||||||
|
return episodes.slice();
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,16 @@ export class LongTermMemory {
|
|||||||
private store: LongTermStore;
|
private store: LongTermStore;
|
||||||
private filePath: string;
|
private filePath: string;
|
||||||
private dirty = false;
|
private dirty = false;
|
||||||
|
/** Hard cap on retained entries — oldest are trimmed when exceeded. Default 100 (matches MemoryConfig.longTermMaxEntries). */
|
||||||
|
private maxEntries: number;
|
||||||
|
|
||||||
constructor(brainPath: string) {
|
constructor(brainPath: string, maxEntries = 100) {
|
||||||
const memoryDir = path.join(brainPath, 'memory');
|
const memoryDir = path.join(brainPath, 'memory');
|
||||||
if (!fs.existsSync(memoryDir)) {
|
if (!fs.existsSync(memoryDir)) {
|
||||||
fs.mkdirSync(memoryDir, { recursive: true });
|
fs.mkdirSync(memoryDir, { recursive: true });
|
||||||
}
|
}
|
||||||
this.filePath = path.join(memoryDir, 'long_term.json');
|
this.filePath = path.join(memoryDir, 'long_term.json');
|
||||||
|
this.maxEntries = maxEntries > 0 ? maxEntries : 100;
|
||||||
this.store = this.load();
|
this.store = this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +65,12 @@ export class LongTermMemory {
|
|||||||
referenceCount: 0
|
referenceCount: 0
|
||||||
};
|
};
|
||||||
this.store.entries.push(entry);
|
this.store.entries.push(entry);
|
||||||
|
// Enforce the retention cap — drop the oldest entries (by createdAt) once
|
||||||
|
// over the limit. The store array is append-ordered, so the oldest are at
|
||||||
|
// the front; we trim from there.
|
||||||
|
if (this.store.entries.length > this.maxEntries) {
|
||||||
|
this.store.entries.splice(0, this.store.entries.length - this.maxEntries);
|
||||||
|
}
|
||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
this.save();
|
this.save();
|
||||||
return entry;
|
return entry;
|
||||||
|
|||||||
+1
-1
@@ -54,7 +54,7 @@ export class MemoryManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.shortTerm = new ShortTermMemory();
|
this.shortTerm = new ShortTermMemory();
|
||||||
this.longTerm = new LongTermMemory(brainPath);
|
this.longTerm = new LongTermMemory(brainPath, this.config.longTermMaxEntries);
|
||||||
this.procedural = new ProceduralMemory(brainPath);
|
this.procedural = new ProceduralMemory(brainPath);
|
||||||
this.episodic = new EpisodicMemory(brainPath, this.config.episodicMaxEpisodes);
|
this.episodic = new EpisodicMemory(brainPath, this.config.episodicMaxEpisodes);
|
||||||
this.extractor = new MemoryExtractor();
|
this.extractor = new MemoryExtractor();
|
||||||
|
|||||||
@@ -129,11 +129,24 @@ export function expandQuery(tokens: string[]): string[] {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* TF (Term Frequency): 문서 내 용어 빈도
|
* TF (Term Frequency): 문서 내 용어 빈도
|
||||||
|
*
|
||||||
|
* Takes a precomputed term-count `Map` (built once per document by
|
||||||
|
* `buildTermCounts`) instead of re-scanning the token array per term — the
|
||||||
|
* value is numerically identical to `count / documentTokens.length`.
|
||||||
*/
|
*/
|
||||||
function termFrequency(term: string, documentTokens: string[]): number {
|
function termFrequency(term: string, termCounts: Map<string, number>, totalTokens: number): number {
|
||||||
if (documentTokens.length === 0) return 0;
|
if (totalTokens === 0) return 0;
|
||||||
const count = documentTokens.filter((t) => t === term).length;
|
const count = termCounts.get(term) || 0;
|
||||||
return count / documentTokens.length;
|
return count / totalTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build a term -> occurrence-count map for one document's token array (computed once, reused per query term). */
|
||||||
|
function buildTermCounts(documentTokens: string[]): Map<string, number> {
|
||||||
|
const counts = new Map<string, number>();
|
||||||
|
for (const t of documentTokens) {
|
||||||
|
counts.set(t, (counts.get(t) || 0) + 1);
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -231,7 +244,11 @@ export function scoreTfIdfPreTokenized(
|
|||||||
if (documents.length === 0 || queryTokens.length === 0) return [];
|
if (documents.length === 0 || queryTokens.length === 0) return [];
|
||||||
|
|
||||||
const docTokenArrays = documents.map((doc) => doc.tokens);
|
const docTokenArrays = documents.map((doc) => doc.tokens);
|
||||||
const docTokenSets = docTokenArrays.map((tokens) => new Set(tokens));
|
// Precompute, once per document: a term -> count map (used for TF) and the
|
||||||
|
// derived token Set (used for IDF). Both were previously recomputed inside
|
||||||
|
// nested loops — building them once and reusing them is numerically identical.
|
||||||
|
const docTermCounts = docTokenArrays.map((tokens) => buildTermCounts(tokens));
|
||||||
|
const docTokenSets = docTermCounts.map((counts) => new Set(counts.keys()));
|
||||||
|
|
||||||
// Expand query with synonyms
|
// Expand query with synonyms
|
||||||
const expandedQuery = expandQuery(queryTokens);
|
const expandedQuery = expandQuery(queryTokens);
|
||||||
@@ -248,6 +265,7 @@ export function scoreTfIdfPreTokenized(
|
|||||||
|
|
||||||
return documents.map((doc, index) => {
|
return documents.map((doc, index) => {
|
||||||
const docTokens = docTokenArrays[index];
|
const docTokens = docTokenArrays[index];
|
||||||
|
const termCounts = docTermCounts[index];
|
||||||
const titleTokens = new Set(doc.titleTokens);
|
const titleTokens = new Set(doc.titleTokens);
|
||||||
let score = 0;
|
let score = 0;
|
||||||
const matchedTerms: string[] = [];
|
const matchedTerms: string[] = [];
|
||||||
@@ -262,7 +280,7 @@ export function scoreTfIdfPreTokenized(
|
|||||||
else if (conflictCount >= SCORING_CONFIG.CONFLICT_THRESHOLDS.LOW) conflictSeverity = 'LOW';
|
else if (conflictCount >= SCORING_CONFIG.CONFLICT_THRESHOLDS.LOW) conflictSeverity = 'LOW';
|
||||||
|
|
||||||
for (const term of expandedQuery) {
|
for (const term of expandedQuery) {
|
||||||
const tf = termFrequency(term, docTokens);
|
const tf = termFrequency(term, termCounts, docTokens.length);
|
||||||
const idf = idfCache.get(term) || 1;
|
const idf = idfCache.get(term) || 1;
|
||||||
const tfidf = tf * idf;
|
const tfidf = tf * idf;
|
||||||
|
|
||||||
|
|||||||
+65
-1
@@ -22,6 +22,15 @@ function getTrustedRoots(workspaceRoot: string): string[] {
|
|||||||
roots.push(path.normalize(f.uri.fsPath).toLowerCase());
|
roots.push(path.normalize(f.uri.fsPath).toLowerCase());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Also trust the immediate parent of each root, so sibling projects under a
|
||||||
|
// shared parent (e.g. E:\Wiki\connectai + E:\Wiki\Datacollect) are reachable
|
||||||
|
// for read/list. Guard: never widen to a drive/filesystem root.
|
||||||
|
for (const r of [...roots]) {
|
||||||
|
const parent = path.normalize(path.dirname(r)).toLowerCase();
|
||||||
|
if (parent && parent !== r && path.dirname(parent) !== parent) {
|
||||||
|
roots.push(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
_trustedRoots = [...new Set(roots)];
|
_trustedRoots = [...new Set(roots)];
|
||||||
return _trustedRoots;
|
return _trustedRoots;
|
||||||
}
|
}
|
||||||
@@ -48,6 +57,59 @@ export function validatePath(workspaceRoot: string, targetPath: string): string
|
|||||||
return absolutePath;
|
return absolutePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a command on top-level `&&`, ignoring `&&` that appears inside single-
|
||||||
|
* or double-quoted strings (e.g. a commit message). Returns trimmed, non-empty parts.
|
||||||
|
*/
|
||||||
|
function splitTopLevelAnd(command: string): string[] {
|
||||||
|
const parts: string[] = [];
|
||||||
|
let buf = '';
|
||||||
|
let quote: string | null = null;
|
||||||
|
for (let i = 0; i < command.length; i++) {
|
||||||
|
const c = command[i];
|
||||||
|
if (quote) {
|
||||||
|
buf += c;
|
||||||
|
if (c === quote) { quote = null; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "'" || c === '"') { quote = c; buf += c; continue; }
|
||||||
|
if (c === '&' && command[i + 1] === '&') {
|
||||||
|
parts.push(buf);
|
||||||
|
buf = '';
|
||||||
|
i++; // skip the second '&'
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
buf += c;
|
||||||
|
}
|
||||||
|
parts.push(buf);
|
||||||
|
return parts.map(p => p.trim()).filter(p => p.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Windows PowerShell 5.1 — the default VS Code integrated terminal on Windows —
|
||||||
|
* does not support the `&&` chaining operator (it is a hard parser error, so the
|
||||||
|
* WHOLE command fails to run). Local models emit `&&` constantly because every
|
||||||
|
* git/npm tutorial uses it, and a system-prompt rule alone does not reliably
|
||||||
|
* stop a small model. So rewrite `A && B && C` into a PowerShell-native
|
||||||
|
* conditional chain that preserves short-circuit semantics:
|
||||||
|
*
|
||||||
|
* A && B && C -> A; if ($?) { B; if ($?) { C } }
|
||||||
|
*
|
||||||
|
* `$?` reflects the success of the previous command, so a failed step still
|
||||||
|
* short-circuits the rest — important so e.g. a failed `cd` never lets `git`
|
||||||
|
* run in the wrong directory.
|
||||||
|
*/
|
||||||
|
function rewriteForPowerShell(command: string): string {
|
||||||
|
if (!command.includes('&&')) { return command; }
|
||||||
|
const parts = splitTopLevelAnd(command);
|
||||||
|
if (parts.length <= 1) { return command; }
|
||||||
|
let chain = parts[parts.length - 1];
|
||||||
|
for (let i = parts.length - 2; i >= 0; i--) {
|
||||||
|
chain = `${parts[i]}; if ($?) { ${chain} }`;
|
||||||
|
}
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitizes terminal commands to prevent destructive actions.
|
* Sanitizes terminal commands to prevent destructive actions.
|
||||||
* Uses a combination of blocklist for dangerous patterns and recommendation for allowed tools.
|
* Uses a combination of blocklist for dangerous patterns and recommendation for allowed tools.
|
||||||
@@ -86,5 +148,7 @@ export function sanitizeCommand(command: string): string {
|
|||||||
console.warn(`[Security] Warning: Running uncommon command '${baseCmd}'. Ensure this is intended.`);
|
console.warn(`[Security] Warning: Running uncommon command '${baseCmd}'. Ensure this is intended.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return trimmedCmd;
|
// Rewrite `&&` chains for PowerShell (the Windows default terminal) so the
|
||||||
|
// command actually runs instead of failing with a parser error.
|
||||||
|
return rewriteForPowerShell(trimmedCmd);
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-3
@@ -25,6 +25,7 @@ import { handleBrainMessage } from './sidebar/brainHandlers';
|
|||||||
import { handleChronicleMessage } from './sidebar/chronicleHandlers';
|
import { handleChronicleMessage } from './sidebar/chronicleHandlers';
|
||||||
import { handleAgentMessage } from './sidebar/agentHandlers';
|
import { handleAgentMessage } from './sidebar/agentHandlers';
|
||||||
import { getOrCreateAgentEntry, resolveScopeForAgent } from './skills/agentKnowledgeMap';
|
import { getOrCreateAgentEntry, resolveScopeForAgent } from './skills/agentKnowledgeMap';
|
||||||
|
import { clearBrainTokenIndex } from './retrieval/brainIndex';
|
||||||
import { estimateModelParamsB } from './lib/contextManager';
|
import { estimateModelParamsB } from './lib/contextManager';
|
||||||
import { loadExternalSkills, formatSkillsAsPromptBlock } from './skills/externalSkillLoader';
|
import { loadExternalSkills, formatSkillsAsPromptBlock } from './skills/externalSkillLoader';
|
||||||
import {
|
import {
|
||||||
@@ -836,9 +837,15 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
localResourceRoots: [this._extensionUri]
|
localResourceRoots: [this._extensionUri]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Webview event listeners must be disposed — otherwise each re-init of the
|
||||||
|
// view leaks a listener (and its captured `this`). We collect every
|
||||||
|
// listener disposable here, dispose them when the view itself is disposed,
|
||||||
|
// and also register them with the extension subscriptions as a backstop.
|
||||||
|
const viewDisposables: vscode.Disposable[] = [];
|
||||||
|
|
||||||
// [State Persistence Fix] 사이드바가 다시 보여질 때 세팅값 자동 복원
|
// [State Persistence Fix] 사이드바가 다시 보여질 때 세팅값 자동 복원
|
||||||
let _lastVisibilityRefresh = 0;
|
let _lastVisibilityRefresh = 0;
|
||||||
webviewView.onDidChangeVisibility(() => {
|
viewDisposables.push(webviewView.onDidChangeVisibility(() => {
|
||||||
if (!webviewView.visible) return;
|
if (!webviewView.visible) return;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
// 5초 이내에 이미 갱신했으면 건너뜀
|
// 5초 이내에 이미 갱신했으면 건너뜀
|
||||||
@@ -850,7 +857,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
void this._sendBrainProfiles();
|
void this._sendBrainProfiles();
|
||||||
void this._sendAgentsList();
|
void this._sendAgentsList();
|
||||||
void this._sendReadyStatus();
|
void this._sendReadyStatus();
|
||||||
});
|
}));
|
||||||
|
|
||||||
webviewView.webview.html = this._getHtml(webviewView.webview);
|
webviewView.webview.html = this._getHtml(webviewView.webview);
|
||||||
this._agent.setWebview(webviewView.webview);
|
this._agent.setWebview(webviewView.webview);
|
||||||
@@ -858,7 +865,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
void this._restoreActiveSessionIntoView();
|
void this._restoreActiveSessionIntoView();
|
||||||
void this._sendReadyStatus();
|
void this._sendReadyStatus();
|
||||||
|
|
||||||
webviewView.webview.onDidReceiveMessage(async (data) => {
|
viewDisposables.push(webviewView.webview.onDidReceiveMessage(async (data) => {
|
||||||
// dispatch root 진입 trace — "/benchmark 입력했는데 아무 응답 없음" 같은
|
// dispatch root 진입 trace — "/benchmark 입력했는데 아무 응답 없음" 같은
|
||||||
// 보고가 들어왔을 때 webview message가 정말 도착했는지부터 즉시 판별.
|
// 보고가 들어왔을 때 webview message가 정말 도착했는지부터 즉시 판별.
|
||||||
const valuePreview = typeof data?.value === 'string'
|
const valuePreview = typeof data?.value === 'string'
|
||||||
@@ -870,7 +877,14 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
if (await handleChronicleMessage(this, data)) return;
|
if (await handleChronicleMessage(this, data)) return;
|
||||||
if (await handleAgentMessage(this, data)) return;
|
if (await handleAgentMessage(this, data)) return;
|
||||||
logInfo(`Unhandled sidebar message: ${data?.type}`);
|
logInfo(`Unhandled sidebar message: ${data?.type}`);
|
||||||
|
}));
|
||||||
|
|
||||||
|
webviewView.onDidDispose(() => {
|
||||||
|
for (const d of viewDisposables.splice(0)) {
|
||||||
|
try { d.dispose(); } catch { /* already disposed */ }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
this._context.subscriptions.push(...viewDisposables);
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentSessionId: string | null = null;
|
_currentSessionId: string | null = null;
|
||||||
@@ -1260,6 +1274,11 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
|||||||
|
|
||||||
await vscode.workspace.getConfiguration('g1nation').update('activeBrainId', nextProfile.id, vscode.ConfigurationTarget.Global);
|
await vscode.workspace.getConfiguration('g1nation').update('activeBrainId', nextProfile.id, vscode.ConfigurationTarget.Global);
|
||||||
this._currentSessionBrainId = nextProfile.id;
|
this._currentSessionBrainId = nextProfile.id;
|
||||||
|
// Drop the in-memory brain token index — the active brain (and its path)
|
||||||
|
// may now differ, and the index's `_states` Map is otherwise never cleared.
|
||||||
|
// The persisted on-disk index is left intact and reloads lazily on the
|
||||||
|
// next query for whichever brain is now active.
|
||||||
|
clearBrainTokenIndex();
|
||||||
await this._sendBrainProfiles();
|
await this._sendBrainProfiles();
|
||||||
await this._sendBrainStatus();
|
await this._sendBrainStatus();
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { findBrainFiles, summarizeText } from '../utils';
|
import { findBrainFiles, summarizeText } from '../utils';
|
||||||
import { isInside } from '../lib/paths';
|
import { isInside } from '../lib/paths';
|
||||||
import { tokenize, expandQuery, scoreTfIdf, extractBestExcerpt } from '../retrieval/scoring';
|
import { tokenize, expandQuery, scoreTfIdfPreTokenized, extractBestExcerpt } from '../retrieval/scoring';
|
||||||
|
import { getBrainTokenIndex } from '../retrieval/brainIndex';
|
||||||
import { estimateTokens } from '../retrieval/contextBudget';
|
import { estimateTokens } from '../retrieval/contextBudget';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,33 +92,35 @@ export function retrieveScoped(
|
|||||||
});
|
});
|
||||||
if (candidates.length === 0) return { ...empty, candidateCount: 0 };
|
if (candidates.length === 0) return { ...empty, candidateCount: 0 };
|
||||||
|
|
||||||
const documents = candidates.map((file) => {
|
// Tokenized docs from the persistent mtime-keyed brain index — unchanged files
|
||||||
let content = '';
|
// are not re-read or re-tokenized. The index tokenizes `${basename} ${content}`
|
||||||
let lastModified = 0;
|
// (titleTokens = tokenize(basename)), which is exactly what the previous
|
||||||
try {
|
// `scoreTfIdf` call computed here, so scoring stays byte-identical.
|
||||||
content = fs.readFileSync(file, 'utf8');
|
const indexed = getBrainTokenIndex(brainRoot, candidates);
|
||||||
lastModified = fs.statSync(file).mtimeMs;
|
if (indexed.length === 0) return { ...empty, candidateCount: candidates.length };
|
||||||
} catch { /* skip unreadable file */ }
|
|
||||||
return {
|
|
||||||
title: path.basename(file, '.md'),
|
|
||||||
content,
|
|
||||||
lastModified,
|
|
||||||
filePath: file,
|
|
||||||
relativePath: path.relative(brainRoot, file),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryTokens = tokenize(query);
|
const queryTokens = tokenize(query);
|
||||||
const expanded = expandQuery(queryTokens);
|
const expanded = expandQuery(queryTokens);
|
||||||
const scored = scoreTfIdf(expanded, documents);
|
const scored = scoreTfIdfPreTokenized(
|
||||||
|
expanded,
|
||||||
|
indexed.map((d) => ({
|
||||||
|
tokens: d.tokens,
|
||||||
|
titleTokens: d.titleTokens,
|
||||||
|
lastModified: d.mtimeMs,
|
||||||
|
conflictCount: d.conflictCount,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
const chunks = scored
|
const chunks = scored
|
||||||
.filter((s) => s.score > 0)
|
.filter((s) => s.score > 0)
|
||||||
.sort((a, b) => b.score - a.score)
|
.sort((a, b) => b.score - a.score)
|
||||||
.slice(0, maxResults)
|
.slice(0, maxResults)
|
||||||
.map<ScopedRetrievalChunk>((s) => {
|
.map<ScopedRetrievalChunk>((s) => {
|
||||||
const doc = documents[s.index];
|
const doc = indexed[s.index];
|
||||||
const excerpt = extractBestExcerpt(doc.content, expanded, excerptLength);
|
// Only the chosen top-`maxResults` files are read off disk (for excerpt extraction).
|
||||||
|
let content = '';
|
||||||
|
try { content = fs.readFileSync(doc.filePath, 'utf8'); } catch { /* deleted just now — empty excerpt */ }
|
||||||
|
const excerpt = extractBestExcerpt(content, expanded, excerptLength);
|
||||||
const summary = summarizeText(excerpt, excerptLength);
|
const summary = summarizeText(excerpt, excerptLength);
|
||||||
return {
|
return {
|
||||||
relativePath: doc.relativePath,
|
relativePath: doc.relativePath,
|
||||||
|
|||||||
+70
-19
@@ -150,21 +150,50 @@ export function invalidateBrainFilesCache(dir?: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _walkBrainFiles(dir: string): string[] {
|
function _walkBrainFiles(dir: string): string[] {
|
||||||
let results: string[] = [];
|
const results: string[] = [];
|
||||||
if (!fs.existsSync(dir)) return results;
|
_walkBrainFilesInto(dir, results);
|
||||||
const list = fs.readdirSync(dir);
|
return results;
|
||||||
list.forEach((file) => {
|
}
|
||||||
const filePath = path.join(dir, file);
|
|
||||||
const stat = fs.statSync(filePath);
|
/**
|
||||||
if (stat && stat.isDirectory()) {
|
* Recursive walk that pushes `.md` paths into a single shared accumulator.
|
||||||
if (!EXCLUDED_DIRS.has(file)) {
|
*
|
||||||
results = results.concat(_walkBrainFiles(filePath));
|
* Uses `readdirSync(dir, { withFileTypes: true })` so each entry's type comes
|
||||||
|
* from the directory read itself — no extra `fs.statSync` per entry — and pushes
|
||||||
|
* into one array instead of allocating a new array per directory via `.concat`.
|
||||||
|
*/
|
||||||
|
function _walkBrainFilesInto(dir: string, results: string[]): void {
|
||||||
|
let entries: fs.Dirent[];
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
// Missing/unreadable directory — matches the previous existsSync guard's behavior.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
const name = entry.name;
|
||||||
|
const filePath = path.join(dir, name);
|
||||||
|
let isDir = entry.isDirectory();
|
||||||
|
let isFile = entry.isFile();
|
||||||
|
// Symlinks: Dirent type flags don't follow links, but the previous
|
||||||
|
// statSync-based walk did — resolve them so behavior is unchanged.
|
||||||
|
if (entry.isSymbolicLink()) {
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
isDir = stat.isDirectory();
|
||||||
|
isFile = stat.isFile();
|
||||||
|
} catch {
|
||||||
|
continue; // dangling symlink — skip (statSync would have thrown before)
|
||||||
}
|
}
|
||||||
} else if (file.endsWith('.md')) {
|
}
|
||||||
|
if (isDir) {
|
||||||
|
if (!EXCLUDED_DIRS.has(name)) {
|
||||||
|
_walkBrainFilesInto(filePath, results);
|
||||||
|
}
|
||||||
|
} else if (isFile && name.endsWith('.md')) {
|
||||||
results.push(filePath);
|
results.push(filePath);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const BASE_SYSTEM_PROMPT = `You are Astra, a Jarvis-style local project operating assistant.
|
const BASE_SYSTEM_PROMPT = `You are Astra, a Jarvis-style local project operating assistant.
|
||||||
@@ -184,31 +213,53 @@ If the provided initial scan preview is not enough, DO NOT complain that you can
|
|||||||
Never say "upload the source code", "provide the files", "파일 내용을 보여주세요", or "먼저 분석할까요?" before attempting access.
|
Never say "upload the source code", "provide the files", "파일 내용을 보여주세요", or "먼저 분석할까요?" before attempting access.
|
||||||
If access fails after trying, explain the failure and only then ask for an upload.
|
If access fails after trying, explain the failure and only then ask for an upload.
|
||||||
|
|
||||||
|
[EXECUTION RULE]
|
||||||
|
When the user asks to run, start, launch, boot, or serve something (실행/구동/시작/켜줘/띄워줘/돌려줘/run/start/launch/serve), ACT — never advise.
|
||||||
|
- FORBIDDEN: writing a how-to, a numbered tutorial, "먼저 ~를 확인해야 합니다", "~하시기 바랍니다", or telling the user to run a command themselves. The user asked YOU to run it.
|
||||||
|
- NEVER invent a script name, port number, or environment variable. If you have not seen it in a file THIS session, do not state it as fact.
|
||||||
|
- If you do not know the exact start command, FIRST read the project's package.json with <read_file>, then emit <run_command> with the real script name.
|
||||||
|
- <run_command> runs in a real terminal. If the target folder differs from the workspace, cd into its absolute path first.
|
||||||
|
- The terminal is Windows PowerShell. Chain steps with ";" — NEVER "&&" (it is a syntax error in PowerShell 5.1). Example: cd 'C:\proj'; git add .; git commit -m 'msg'; git push
|
||||||
|
- After acting, reply with ONE short line: what you started and where. No tutorial, no follow-up checklist.
|
||||||
|
|
||||||
|
Worked example — user says: "E:\Wiki\Datacollect 서버 실행해줘"
|
||||||
|
Step 1 (only when the start script is unknown):
|
||||||
|
<read_file path="E:\Wiki\Datacollect\package.json"/>
|
||||||
|
Step 2 (after the real scripts are known — pick the actual one, never a guessed name):
|
||||||
|
<run_command>cd 'E:\Wiki\Datacollect'; npm run start-full</run_command>
|
||||||
|
Then reply: "Datacollect 서버를 start-full 스크립트로 터미널에서 실행했습니다."
|
||||||
|
|
||||||
[STRICT GLOBAL RULES]
|
[STRICT GLOBAL RULES]
|
||||||
1. [NO EMOJIS - ABSOLUTE RULE] NEVER use ANY emojis, emoticons, Unicode pictorial symbols (including but not limited to emoji, kaomoji, Unicode icons), or decorative symbols anywhere in your response. NO EXCEPTIONS. Use plain text dashes (-) or asterisks (*) for bullets. Use plain markdown ## for headers. This rule overrides ALL other formatting instructions.
|
1. [NO EMOJIS - ABSOLUTE RULE] NEVER use ANY emojis, emoticons, Unicode pictorial symbols (including but not limited to emoji, kaomoji, Unicode icons), or decorative symbols anywhere in your response. NO EXCEPTIONS. Use plain text dashes (-) or asterisks (*) for bullets. Use plain markdown ## for headers. This rule overrides ALL other formatting instructions.
|
||||||
2. [UNIQUE HEADINGS] Every markdown heading must be unique and appear exactly once.
|
2. [HEADINGS] Every markdown heading must be unique, appear exactly once, and start with exactly one "## " — never "## ##", never "### ###". One space after the hashes.
|
||||||
3. [NO INTERNAL LOGS] Never output <details>, "2nd Brain Trace", or "Debug JSON" blocks.
|
3. [NO INTERNAL LOGS] Never output <details>, "2nd Brain Trace", or "Debug JSON" blocks.
|
||||||
4. [NO SECTION LEAKAGE] Never output sections named "요청 요약", "사용자 의도 추론", "프로젝트 기록 대상 확인", "핵심 확인 질문", or "근거 파일 경로".
|
4. [NO SECTION LEAKAGE] Never output sections named "요청 요약", "사용자 의도 추론", "프로젝트 기록 대상 확인", "핵심 확인 질문", or "근거 파일 경로".
|
||||||
|
|
||||||
[OUTPUT FORMAT]
|
[OUTPUT FORMAT]
|
||||||
Use the 3-section format ONLY for: technical analysis, architecture proposals, troubleshooting, or strategic planning.
|
LENGTH decides structure — not topic. Count how long your answer will be:
|
||||||
For conversational replies, quick facts, or simple updates — answer directly without any headers.
|
|
||||||
|
|
||||||
## 요약
|
- If the answer is longer than ~4 sentences (analysis, advice, planning, troubleshooting, or any multi-part answer), you MUST lead with a summary block, then the detail:
|
||||||
Core conclusion in 2-3 sentences.
|
|
||||||
|
## 핵심 요약
|
||||||
|
- 2 to 4 bullet points. Each bullet is one scannable, self-contained takeaway that captures the WHOLE answer — a reader who stops here still gets the gist.
|
||||||
|
- This block is ALWAYS the very first thing in the response. NEVER place a summary at the bottom. NEVER write an intro paragraph before it — the summary block IS the opening.
|
||||||
|
|
||||||
## 상세 설명
|
## 상세 설명
|
||||||
- Root cause of the problem.
|
Free-form depth. You MAY use your own sub-headers here (e.g. "### 1. ...", "### 2. ..."). This is where the full reasoning and steps go.
|
||||||
- Concrete step-by-step instructions: what to change, which files to edit, which commands to run.
|
|
||||||
|
|
||||||
## 제안 ← Optional. Only include if a meaningfully better alternative exists. Omit otherwise.
|
## 제안 ← Optional. Only include if a meaningfully better alternative exists. Omit otherwise.
|
||||||
|
|
||||||
|
- If the answer is ~4 sentences or fewer (quick fact, simple update, casual or emotional reply) — answer directly, no headers, no summary block.
|
||||||
|
|
||||||
|
The summary block is named exactly "## 핵심 요약" and goes at the TOP. A section literally named "요약" placed at the end is a bug — never do that.
|
||||||
|
|
||||||
[FOLLOW-UP QUESTION RULES]
|
[FOLLOW-UP QUESTION RULES]
|
||||||
A follow-up question is a precision tool, not a ritual.
|
A follow-up question is a precision tool, not a ritual.
|
||||||
Ask ONE focused question at the very end of the response ONLY if:
|
Ask ONE focused question at the very end of the response ONLY if:
|
||||||
- The user's intent is genuinely ambiguous with multiple valid paths, OR
|
- The user's intent is genuinely ambiguous with multiple valid paths, OR
|
||||||
- A critical missing detail would make the current answer completely wrong.
|
- A critical missing detail would make the current answer completely wrong.
|
||||||
If neither condition is met, give a definitive answer and stop.
|
If neither condition is met, give a definitive answer and stop.
|
||||||
|
When you do ask: it is ONE plain sentence on its own line. NEVER put it under a heading, NEVER label the section ("핵심 확인 질문", "확인 질문" etc.), NEVER attach a "질문 의도" explanation, NEVER ask two or more questions.
|
||||||
|
|
||||||
[ENGINEERING STANCE]
|
[ENGINEERING STANCE]
|
||||||
- Be a direct engineering partner. Technical precision over polite filler.
|
- Be a direct engineering partner. Technical precision over polite filler.
|
||||||
|
|||||||
Reference in New Issue
Block a user