docs: ROADMAP full restore + #98-143 tech debt links + archive historical DESIGN/CHANGES (#144)
This commit is contained in:
99
docs/archive/CHANGES-0.18.0-phase1.md
Normal file
99
docs/archive/CHANGES-0.18.0-phase1.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# v0.18.0 Phase 1 — Changes Guide
|
||||
|
||||
## New Files (drop in place)
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `server/database/migrations/004_v0180_memories.sql` | Postgres migration — memories table, indexes, trigger |
|
||||
| `server/database/migrations/sqlite/003_v0180_memories.sql` | SQLite equivalent |
|
||||
| `server/models/models_memory.go` | Memory + MemoryFilter model structs + scope/status constants |
|
||||
| `server/store/store_memory.go` | MemoryStore interface definition |
|
||||
| `server/store/postgres/memory.go` | Postgres MemoryStore implementation |
|
||||
| `server/store/sqlite/memory.go` | SQLite MemoryStore implementation |
|
||||
| `server/tools/memory.go` | memory_save + memory_recall tools with late registration |
|
||||
| `server/handlers/memory_inject.go` | BuildMemoryHint() for context injection |
|
||||
| `docs/DESIGN-0.18.0.md` | Design document |
|
||||
|
||||
## Existing File Modifications
|
||||
|
||||
### 1. `server/store/interfaces.go`
|
||||
|
||||
Add to the `Stores` struct (after `ResourceGrants`):
|
||||
|
||||
```go
|
||||
Memories MemoryStore
|
||||
```
|
||||
|
||||
The `MemoryStore` interface itself is in the new `store_memory.go` file
|
||||
(same package, separate file for cleanliness).
|
||||
|
||||
### 2. `server/store/postgres/stores.go`
|
||||
|
||||
Add to the `NewStores()` return:
|
||||
|
||||
```go
|
||||
Memories: NewMemoryStore(),
|
||||
```
|
||||
|
||||
### 3. `server/store/sqlite/stores.go`
|
||||
|
||||
Add to the `NewStores()` return:
|
||||
|
||||
```go
|
||||
Memories: NewMemoryStore(),
|
||||
```
|
||||
|
||||
### 4. `server/main.go`
|
||||
|
||||
Add after the existing tool registrations (~line 169):
|
||||
|
||||
```go
|
||||
// Memory tools (v0.18.0) — late registration, needs stores
|
||||
tools.RegisterMemoryTools(stores)
|
||||
```
|
||||
|
||||
### 5. `server/handlers/completion.go`
|
||||
|
||||
In `loadConversation()`, add memory injection after the KB hint block
|
||||
(~line 798, after the `BuildKBHint` block):
|
||||
|
||||
```go
|
||||
// ── Memory injection (recall known facts about user) ──
|
||||
if memHint := BuildMemoryHint(context.Background(), h.stores, userID, personaID); memHint != \"\" {
|
||||
messages = append(messages, providers.Message{
|
||||
Role: \"system\",
|
||||
Content: memHint,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 6. `VERSION`
|
||||
|
||||
```
|
||||
0.18.0
|
||||
```
|
||||
|
||||
### 7. `docs/ROADMAP.md`
|
||||
|
||||
Mark Phase 1 items as complete (Data Model, Tools, Memory Injection basics).
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
1. **Migration runs clean** — both Postgres and SQLite
|
||||
2. **Tools register** — check startup logs for `🔧 Registered tool: memory_save` and `memory_recall`
|
||||
3. **memory_save** — in a chat, tell the AI something personal (\"I prefer Go over Python\"). The AI should call memory_save.
|
||||
4. **memory_recall** — in a NEW chat, ask \"what programming language do I prefer?\" The AI should call memory_recall and find it.
|
||||
5. **Persona scoping** — with a Persona active, memories save as `persona_user` scope. Without a Persona, they save as `user` scope.
|
||||
6. **Upsert** — saving the same key twice updates the value instead of creating a duplicate.
|
||||
7. **Injection** — memories appear in the system prompt context (check server logs for `🧠 Injected N memories`).
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
- Memory injection happens in `loadConversation()` as a system message, same pattern as KB hints and compaction summaries
|
||||
- Scope priority in Recall: persona_user > persona > user (most specific wins)
|
||||
- The unique index on `(scope, owner_id, COALESCE(user_id, nil_uuid), key)` prevents duplicate keys within a scope
|
||||
- Confidence is LLM-provided: 1.0 for explicit statements, lower for inferences
|
||||
- Status field supports Phase 2's review pipeline (pending_review, archived)
|
||||
- Embedding column exists but is unused in Phase 1 — Phase 2 adds semantic recall
|
||||
191
docs/archive/CHANGES-0.18.0-phase2.md
Normal file
191
docs/archive/CHANGES-0.18.0-phase2.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# v0.18.0 Phase 2 — Changes Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Phase 2 adds: automatic memory extraction via background scanner,
|
||||
embedding-powered semantic recall, memory review pipeline API, and
|
||||
persona memory configuration.
|
||||
|
||||
## New Files (drop in place)
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `server/database/migrations/005_v0180_memory_phase2.sql` | Postgres: persona memory columns, HNSW index, extraction log table |
|
||||
| `server/database/migrations/sqlite/004_v0180_memory_phase2.sql` | SQLite equivalent |
|
||||
| `server/memory/extractor.go` | Extraction service — calls utility model to extract facts from conversations |
|
||||
| `server/memory/scanner.go` | Background scanner — finds conversations to extract, same pattern as compaction.Scanner |
|
||||
| `server/store/postgres/memory_hybrid.go` | RecallHybrid — pgvector cosine distance + keyword merge |
|
||||
| `server/store/sqlite/memory_hybrid.go` | RecallHybrid — app-level cosine similarity + keyword merge |
|
||||
| `server/handlers/memory.go` | REST API: list, edit, delete, approve/reject memories |
|
||||
| `server/handlers/memory_inject.go` | **REPLACES Phase 1 version** — now supports hybrid recall with embeddings |
|
||||
|
||||
## Updated Files (replace Phase 1 versions)
|
||||
|
||||
| File | What Changed |
|
||||
|------|-------------|
|
||||
| `server/tools/memory.go` | **REPLACES Phase 1 version** — RegisterMemoryTools now takes embedder param, memory_save embeds on save |
|
||||
|
||||
## Existing File Modifications
|
||||
|
||||
### 1. `server/models/models.go` — Persona struct
|
||||
|
||||
Add after the `KBIDs` field:
|
||||
|
||||
```go
|
||||
MemoryEnabled bool `json:\"memory_enabled\" db:\"memory_enabled\"`
|
||||
MemoryExtractionPrompt *string `json:\"memory_extraction_prompt,omitempty\" db:\"memory_extraction_prompt\"`
|
||||
```
|
||||
|
||||
Add to `PersonaPatch`:
|
||||
|
||||
```go
|
||||
MemoryEnabled *bool `json:\"memory_enabled,omitempty\"`
|
||||
MemoryExtractionPrompt *string `json:\"memory_extraction_prompt,omitempty\"`
|
||||
```
|
||||
|
||||
### 2. `server/store/store_memory.go` — MemoryStore interface
|
||||
|
||||
Add to the interface:
|
||||
|
||||
```go
|
||||
// RecallHybrid returns active memories using vector similarity + keyword search.
|
||||
// If queryVec is nil/empty, falls back to keyword-only (same as Recall).
|
||||
RecallHybrid(ctx context.Context, userID string, personaID *string, query string, queryVec []float64, limit int) ([]models.Memory, error)
|
||||
```
|
||||
|
||||
### 3. `server/main.go` — Tool registration + scanner startup
|
||||
|
||||
**Update** the existing RegisterMemoryTools call (~line 170) to pass the embedder:
|
||||
|
||||
```go
|
||||
// Memory tools (v0.18.0) — late registration, needs stores + embedder
|
||||
tools.RegisterMemoryTools(stores, kbEmbedder)
|
||||
```
|
||||
|
||||
**Add** memory extraction scanner startup (after compaction scanner, or near the end
|
||||
of the startup block — see compaction scanner as pattern reference):
|
||||
|
||||
```go
|
||||
// Memory extraction scanner (v0.18.0 Phase 2)
|
||||
// Opt-in: requires global_settings.memory_extraction_enabled = true
|
||||
memExtractor := memory.NewExtractor(stores, roleResolver, kbEmbedder)
|
||||
memScanner := memory.NewScanner(memExtractor, stores, memory.ScannerConfig{})
|
||||
memScanner.Start()
|
||||
defer memScanner.Stop()
|
||||
```
|
||||
|
||||
Import: `\"git.gobha.me/xcaliber/chat-switchboard/memory\"`
|
||||
|
||||
### 4. `server/main.go` — Memory API routes
|
||||
|
||||
Add under the protected routes (~after presets/personas routes):
|
||||
|
||||
```go
|
||||
// Memory management (v0.18.0)
|
||||
memH := handlers.NewMemoryHandler(stores)
|
||||
protected.GET(\"/memories\", memH.ListMyMemories)
|
||||
protected.PUT(\"/memories/:id\", memH.UpdateMemory)
|
||||
protected.DELETE(\"/memories/:id\", memH.DeleteMemory)
|
||||
protected.POST(\"/memories/:id/approve\", memH.ApproveMemory)
|
||||
protected.POST(\"/memories/:id/reject\", memH.RejectMemory)
|
||||
protected.GET(\"/memories/count\", memH.MemoryCount)
|
||||
```
|
||||
|
||||
Add under admin routes:
|
||||
|
||||
```go
|
||||
// Admin memory review (v0.18.0)
|
||||
adminMemH := handlers.NewMemoryHandler(stores)
|
||||
admin.GET(\"/memories/pending\", adminMemH.ListPendingReview)
|
||||
admin.POST(\"/memories/bulk-approve\", adminMemH.BulkApprove)
|
||||
```
|
||||
|
||||
### 5. `server/handlers/completion.go` — BuildMemoryHint signature change
|
||||
|
||||
The `BuildMemoryHint` call in `loadConversation()` changes signature.
|
||||
Update from Phase 1:
|
||||
|
||||
```go
|
||||
// OLD (Phase 1):
|
||||
if memHint := BuildMemoryHint(context.Background(), h.stores, userID, personaID); memHint != \"\" {
|
||||
|
||||
// NEW (Phase 2) — pass embedder and last user message for semantic recall:
|
||||
lastUserMsg := \"\"
|
||||
for i := len(messages) - 1; i >= 0; i-- {
|
||||
if messages[i].Role == \"user\" {
|
||||
lastUserMsg = messages[i].Content
|
||||
break
|
||||
}
|
||||
}
|
||||
if memHint := BuildMemoryHint(context.Background(), h.stores, h.embedder, userID, personaID, lastUserMsg); memHint != \"\" {
|
||||
```
|
||||
|
||||
This requires adding the embedder to CompletionHandler. In the struct:
|
||||
|
||||
```go
|
||||
type CompletionHandler struct {
|
||||
vault *crypto.KeyResolver
|
||||
stores store.Stores
|
||||
hub *events.Hub
|
||||
objStore storage.ObjectStore
|
||||
embedder *knowledge.Embedder // NEW: for memory semantic recall
|
||||
}
|
||||
```
|
||||
|
||||
And in `NewCompletionHandler`:
|
||||
|
||||
```go
|
||||
func NewCompletionHandler(vault *crypto.KeyResolver, stores store.Stores, hub *events.Hub, objStore storage.ObjectStore, embedder *knowledge.Embedder) *CompletionHandler {
|
||||
return &CompletionHandler{vault: vault, stores: stores, hub: hub, objStore: objStore, embedder: embedder}
|
||||
}
|
||||
```
|
||||
|
||||
Update the call site in main.go:
|
||||
|
||||
```go
|
||||
comp := handlers.NewCompletionHandler(keyResolver, stores, hub, objStore, kbEmbedder)
|
||||
```
|
||||
|
||||
### 6. `server/store/postgres/persona.go` — Scan memory fields
|
||||
|
||||
Any Persona scan queries need to include the new columns. Add
|
||||
`memory_enabled` and `memory_extraction_prompt` to SELECT lists and Scan calls
|
||||
in `GetByID`, `ListForUser`, etc.
|
||||
|
||||
### 7. `server/store/sqlite/persona.go` — Same as above for SQLite.
|
||||
|
||||
---
|
||||
|
||||
## Admin Setup
|
||||
|
||||
The extraction scanner is **opt-in**. To enable:
|
||||
|
||||
1. Set global config: `memory_extraction_enabled` = `true`
|
||||
2. The scanner runs every 10 minutes (configurable)
|
||||
3. Extracted memories start as `pending_review`
|
||||
4. Admin reviews via `GET /api/v1/admin/memories/pending`
|
||||
5. Approve: `POST /api/v1/memories/:id/approve`
|
||||
6. Reject: `POST /api/v1/memories/:id/reject`
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
1. **Migration** — both Postgres and SQLite, verify `memory_extraction_log` table created
|
||||
2. **Persona columns** — `ALTER TABLE personas ADD COLUMN memory_enabled` runs clean
|
||||
3. **HNSW index** — Postgres only, verify with `\\di+ idx_memories_embedding`
|
||||
4. **Embedding on save** — call memory_save, check logs for `🧠 memory X embedded`
|
||||
5. **Hybrid recall** — with embeddings populated, memory_recall should find semantically relevant results even with different keywords
|
||||
6. **Extraction scanner** — set `memory_extraction_enabled=true` in global config, wait for scan cycle, verify `🧠 memory extraction:` logs
|
||||
7. **Extracted status** — auto-extracted memories should have `status=pending_review`
|
||||
8. **Review API** — `GET /memories?status=pending_review` returns pending items
|
||||
9. **Approve/reject** — POST approve changes status to active, reject archives
|
||||
10. **BuildMemoryHint** — with embedder, verify semantic recall contextualizes based on user's message
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
- **Extraction scanner** follows compaction.Scanner pattern: ticker loop, semaphore concurrency, in-flight dedup, graceful shutdown via Stop()
|
||||
- **Extraction log** (`memory_extraction_log`) prevents re-processing: tracks last_message_id per channel+user
|
||||
- **RecallHybrid** merges vector + keyword results with dedup by ID; semantic results rank first
|
||||
- **SQLite hybrid** loads all embedded memories into Go and computes cosine similarity in-process (acceptable for single-user deployments)
|
||||
- **Persona memory toggle** (`memory_enabled`) gates both tool-based and extraction-based memory for that persona
|
||||
- **Extraction prompt** is customizable per persona — a helpdesk persona extracts FAQ patterns, a tutoring persona extracts learning progress
|
||||
- **Phase 3** will add the frontend UI: Settings → Memory panel, admin review queue, per-persona toggle in persona editor
|
||||
282
docs/archive/CHANGES-0.18.0-phase3.md
Normal file
282
docs/archive/CHANGES-0.18.0-phase3.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# v0.18.0 Phase 3 — Frontend UI Changes Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Phase 3 adds the user-facing memory management UI:
|
||||
- **Settings → Memory tab**: view, search, edit, approve/reject memories
|
||||
- **Admin → AI → Memory**: review pipeline for auto-extracted memories
|
||||
- **Persona forms**: memory_enabled toggle + custom extraction prompt
|
||||
- **Admin → System → Settings**: memory extraction toggle
|
||||
|
||||
## New Files (drop in place)
|
||||
|
||||
| File | Lines | Description |
|
||||
|------|-------|-------------|
|
||||
| `src/js/memory-ui.js` | ~310 | Memory UI module — settings panel, admin panel, persona form fields |
|
||||
| `src/css/memory.css` | ~230 | Styles for memory cards, badges, toolbar, edit forms |
|
||||
|
||||
## Existing File Modifications
|
||||
|
||||
### 1. `src/index.html` — Add Memory Tab + Admin Section
|
||||
|
||||
#### A. Settings Modal — Add Memory Tab Button
|
||||
|
||||
After the Knowledge tab button (~line 389):
|
||||
|
||||
```html
|
||||
<button class=\"settings-tab\" data-stab=\"memory\" onclick=\"UI.switchSettingsTab('memory')\" id=\"settingsMemoryTabBtn\">Memory</button>
|
||||
```
|
||||
|
||||
#### B. Settings Modal — Add Memory Tab Content
|
||||
|
||||
After the Knowledge Bases tab content block (after `settingsKnowledgeBasesTab` div, ~line 552):
|
||||
|
||||
```html
|
||||
<!-- Memory Tab (v0.18.0) -->
|
||||
<div class=\"settings-tab-content\" id=\"settingsMemoryTab\" style=\"display:none\">
|
||||
<section class=\"settings-section\">
|
||||
<h3 style=\"font-size:14px;margin-bottom:4px\">My Memories</h3>
|
||||
<p class=\"section-hint\" style=\"margin-bottom:8px\">Facts and preferences learned from your conversations. Memories help AI provide more personalized responses.</p>
|
||||
<div id=\"settingsMemoryContent\"></div>
|
||||
</section>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### C. Admin Panel — Add Memory Section
|
||||
|
||||
In the admin sections HTML, after the `adminPresetsTab` block (~line 790),
|
||||
add a new admin section:
|
||||
|
||||
```html
|
||||
<div class=\"admin-section-content\" id=\"adminMemoryTab\" style=\"display:none\">
|
||||
<div id=\"adminMemoryContent\"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### D. Admin Settings — Add Memory Extraction Toggle
|
||||
|
||||
In `adminSettingsTab`, after the Auto-Compaction section (~line 908):
|
||||
|
||||
```html
|
||||
<section class=\"settings-section\">
|
||||
<h3>Memory Extraction</h3>
|
||||
<label class=\"checkbox-label\"><input type=\"checkbox\" id=\"adminMemoryExtractionEnabled\"> Enable automatic memory extraction</label>
|
||||
<p class=\"section-hint\">When enabled, the system automatically extracts memorable facts from conversations using the utility model. Extracted memories require admin approval before becoming active. Requires a utility model role.</p>
|
||||
<div id=\"memoryExtractionConfigFields\" style=\"display:none;margin-top:8px\">
|
||||
<label class=\"checkbox-label\"><input type=\"checkbox\" id=\"adminMemoryAutoApprove\"> Auto-approve extracted memories</label>
|
||||
<p class=\"section-hint\">Skip the review pipeline and activate extracted memories immediately. Not recommended for sensitive environments.</p>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
#### E. Script + CSS Include
|
||||
|
||||
In the `<head>` section, add the CSS:
|
||||
|
||||
```html
|
||||
<link rel=\"stylesheet\" href=\"css/memory.css\">
|
||||
```
|
||||
|
||||
In the script block at the bottom (after `knowledge-ui.js`):
|
||||
|
||||
```html
|
||||
<script src=\"js/memory-ui.js\"></script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `src/js/api.js` — Memory API Client Methods
|
||||
|
||||
Add after the user presets section (~line 310):
|
||||
|
||||
```javascript
|
||||
// Memory (v0.18.0)
|
||||
listMemories(status, query) {
|
||||
const params = new URLSearchParams();
|
||||
if (status) params.set('status', status);
|
||||
if (query) params.set('query', query);
|
||||
return this._get('/api/v1/memories?' + params.toString());
|
||||
},
|
||||
getMemoryCount() { return this._get('/api/v1/memories/count'); },
|
||||
updateMemory(id, data) { return this._put(`/api/v1/memories/${id}`, data); },
|
||||
deleteMemory(id) { return this._del(`/api/v1/memories/${id}`); },
|
||||
approveMemory(id) { return this._post(`/api/v1/memories/${id}/approve`); },
|
||||
rejectMemory(id) { return this._post(`/api/v1/memories/${id}/reject`); },
|
||||
bulkApproveMemories(ids) { return this._post('/api/v1/admin/memories/bulk-approve', { ids }); },
|
||||
adminListPendingMemories() { return this._get('/api/v1/admin/memories/pending'); },
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. `src/js/ui-core.js` — Wire Memory Tab into Settings
|
||||
|
||||
In `switchSettingsTab()` (~line 932), add after the `knowledgeBases` case:
|
||||
|
||||
```javascript
|
||||
if (tab === 'memory') {
|
||||
if (typeof MemoryUI !== 'undefined') {
|
||||
MemoryUI.openSettingsPanel();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. `src/js/ui-admin.js` — Register Memory in Admin Panel
|
||||
|
||||
#### A. Add \"memory\" to ADMIN_SECTIONS (~line 9)
|
||||
|
||||
```javascript
|
||||
const ADMIN_SECTIONS = {
|
||||
people: ['users', 'teams', 'groups'],
|
||||
ai: ['providers', 'models', 'presets', 'roles', 'knowledgeBases', 'memory'],
|
||||
system: ['settings', 'storage', 'extensions'],
|
||||
monitoring: ['usage', 'audit', 'stats'],
|
||||
};
|
||||
```
|
||||
|
||||
#### B. Add label (~line 18)
|
||||
|
||||
In ADMIN_LABELS, add:
|
||||
|
||||
```javascript
|
||||
memory: 'Memory',
|
||||
```
|
||||
|
||||
#### C. Add loader (~line 37)
|
||||
|
||||
In ADMIN_LOADERS, add:
|
||||
|
||||
```javascript
|
||||
memory: () => {
|
||||
if (typeof MemoryUI === 'undefined') {
|
||||
console.error('[Admin] MemoryUI not loaded');
|
||||
return;
|
||||
}
|
||||
MemoryUI.openAdminPanel();
|
||||
},
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. `src/js/ui-admin.js` — Load/Save Memory Extraction Settings
|
||||
|
||||
#### A. In `loadAdminSettings()` (~after compaction config loading)
|
||||
|
||||
Add after the compaction threshold/cooldown block:
|
||||
|
||||
```javascript
|
||||
// Memory Extraction (v0.18.0)
|
||||
const memCfg = getSetting('memory_extraction', {}) || {};
|
||||
const memExtractionEl = document.getElementById('adminMemoryExtractionEnabled');
|
||||
if (memExtractionEl) {
|
||||
memExtractionEl.checked = !!memCfg.enabled;
|
||||
document.getElementById('memoryExtractionConfigFields').style.display =
|
||||
memCfg.enabled ? '' : 'none';
|
||||
}
|
||||
const memAutoApproveEl = document.getElementById('adminMemoryAutoApprove');
|
||||
if (memAutoApproveEl) memAutoApproveEl.checked = !!memCfg.auto_approve;
|
||||
```
|
||||
|
||||
#### B. Wire toggle visibility
|
||||
|
||||
Add in `_initAdminListeners()` or inline:
|
||||
|
||||
```javascript
|
||||
document.getElementById('adminMemoryExtractionEnabled')?.addEventListener('change', function() {
|
||||
document.getElementById('memoryExtractionConfigFields').style.display =
|
||||
this.checked ? '' : 'none';
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. `src/js/settings-handlers.js` — Save Memory Extraction Config
|
||||
|
||||
In `handleSaveAdminSettings()`, before the final toast (~line 252):
|
||||
|
||||
```javascript
|
||||
// Memory Extraction config (v0.18.0)
|
||||
const memExtractionEnabled = document.getElementById('adminMemoryExtractionEnabled')?.checked || false;
|
||||
const memAutoApprove = document.getElementById('adminMemoryAutoApprove')?.checked || false;
|
||||
await API.adminUpdateSetting('memory_extraction', { value: {
|
||||
enabled: memExtractionEnabled,
|
||||
auto_approve: memAutoApprove,
|
||||
}});
|
||||
await API.adminUpdateSetting('memory_extraction_enabled', { value: memExtractionEnabled });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. `src/js/ui-core.js` — Persona Form Memory Fields
|
||||
|
||||
For **admin** persona forms, in the function that initializes the admin preset
|
||||
form (after `renderPresetForm` call), add:
|
||||
|
||||
```javascript
|
||||
if (typeof MemoryUI !== 'undefined') {
|
||||
MemoryUI.appendMemoryFields(container, prefix);
|
||||
}
|
||||
```
|
||||
|
||||
And in the submit handler, merge memory values:
|
||||
|
||||
```javascript
|
||||
const values = form.getValues();
|
||||
if (typeof MemoryUI !== 'undefined') {
|
||||
Object.assign(values, MemoryUI.getMemoryValues(prefix));
|
||||
}
|
||||
```
|
||||
|
||||
When editing an existing persona, call:
|
||||
|
||||
```javascript
|
||||
if (typeof MemoryUI !== 'undefined') {
|
||||
MemoryUI.setMemoryValues(prefix, persona);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
1. **Settings → Memory tab** — click tab, verify memory list loads
|
||||
2. **Memory search** — type in search box, verify filtering works
|
||||
3. **Memory status filter** — switch between Active/Pending/Archived
|
||||
4. **Edit memory** — click pencil icon, modify key/value, save
|
||||
5. **Delete memory** — click trash icon, confirm, verify removal
|
||||
6. **Approve/Reject** — with pending memories, verify buttons work
|
||||
7. **Approve All** — bulk approve pending memories
|
||||
8. **Admin → AI → Memory** — navigate to admin memory panel
|
||||
9. **Admin pending review** — verify pending memories show with approve/reject
|
||||
10. **Admin bulk approve** — verify bulk action works
|
||||
11. **Admin Settings → Memory Extraction** — toggle on/off, verify checkbox persists
|
||||
12. **Persona form** — create new persona, verify memory section appears
|
||||
13. **Persona memory toggle** — uncheck, verify it persists on save
|
||||
14. **Mobile responsive** — test all memory views on narrow viewport
|
||||
15. **Empty states** — verify graceful display when no memories exist
|
||||
|
||||
## CSS Variable Dependencies
|
||||
|
||||
The memory styles reference these existing CSS variables:
|
||||
- `--bg-1`, `--bg-2`, `--bg-3` — background layers
|
||||
- `--text-1`, `--text-2`, `--text-3` — text colors
|
||||
- `--border` — border color
|
||||
- `--accent`, `--accent-dim`, `--accent-text` — accent colors
|
||||
- `--success`, `--warning`, `--danger` — status colors
|
||||
|
||||
All are defined in the existing `styles.css` theme system.
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
- `MemoryUI` follows the same pattern as `KnowledgeUI` — standalone module
|
||||
that integrates via `typeof MemoryUI !== 'undefined'` guards
|
||||
- No build step required — vanilla JS module loaded via `<script>` tag
|
||||
- Admin memory section registered in `ADMIN_SECTIONS` and `ADMIN_LOADERS`
|
||||
maps following the existing convention
|
||||
- Persona memory fields are appended dynamically via `appendMemoryFields()`
|
||||
rather than embedded in `renderPresetForm()` to avoid modifying the shared
|
||||
form builder
|
||||
- Debounce on search input prevents excessive API calls
|
||||
- Memory cards use inline edit — no modal, same pattern as note inline editing
|
||||
- CSS uses existing theme variables for consistent dark/light mode support
|
||||
149
docs/archive/DESIGN-0.12.0.md
Normal file
149
docs/archive/DESIGN-0.12.0.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# v0.12.0 — File Handling + Vision (Design)
|
||||
|
||||
## Overview
|
||||
|
||||
File input into chat — table stakes for serious use. Image uploads for
|
||||
vision-capable models, document uploads for text extraction into context,
|
||||
and the storage backend that v0.14.0 (Knowledge Bases) and v0.15.0
|
||||
(Compaction snapshots) will reuse.
|
||||
|
||||
Also picks up stragglers deferred from earlier releases.
|
||||
|
||||
**Depends on:** v0.11.0 (extension foundation — complete).
|
||||
**Reused by:** v0.14.0 (KBs), v0.15.0 (compaction), v0.22.0 (exports).
|
||||
|
||||
---
|
||||
|
||||
## Stragglers (from v0.9.4 deferred)
|
||||
|
||||
Items deferred during vault work that were never scheduled:
|
||||
|
||||
### `switchboard vault rekey` CLI command
|
||||
Re-encrypts all global/team API keys when `ENCRYPTION_KEY` is rotated.
|
||||
Personal BYOK keys are unaffected (keyed to UEK, not env var).
|
||||
|
||||
Implementation: standalone CLI entrypoint that reads old + new env vars,
|
||||
iterates `api_configs` and `team_providers` where `key_scope IN ('global',
|
||||
'team')`, decrypts with old key, re-encrypts with new key, updates in a
|
||||
transaction.
|
||||
|
||||
### Admin UI: encryption status indicator
|
||||
Badge in admin Settings showing whether `ENCRYPTION_KEY` is set and how
|
||||
many keys are encrypted vs. plaintext (migration stragglers). Single
|
||||
`GET /admin/storage/status` endpoint.
|
||||
|
||||
### Per-chat model/preset persistence (server-side)
|
||||
Currently localStorage bandaid from v0.10.2 — doesn't roam across devices.
|
||||
|
||||
**Fix:** Store `last_selector_id` in `channels.settings` JSONB on each
|
||||
completion success. On chat selection, resolve:
|
||||
`channel.settings.last_selector_id` → match against available
|
||||
models/presets → fall back to `channel.model` → global default.
|
||||
|
||||
No migration needed (`channels.settings` JSONB column already exists).
|
||||
Single `PATCH /channels/:id` with settings merge on completion success.
|
||||
|
||||
---
|
||||
|
||||
## Track 1: Storage Backend
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `STORAGE_BACKEND` | (auto) | `pvc` or `s3`. If not set: `pvc` when `STORAGE_PATH` is writable, disabled otherwise. |
|
||||
| `STORAGE_PATH` | `/data/storage` | Mount point for PVC backend. Also scratch dir for extraction with S3. |
|
||||
| `STORAGE_CLASS` | — | K8s only. Gitea CI variable → PVC manifest. Value: `cephfs` (RWX for multi-pod). |
|
||||
| `S3_ENDPOINT` | — | S3-compatible endpoint (e.g. `http://minio:9000`). Required when `STORAGE_BACKEND=s3`. |
|
||||
| `S3_BUCKET` | — | Bucket name. Must exist before first start. |
|
||||
| `S3_ACCESS_KEY` | — | S3 access key ID. |
|
||||
| `S3_SECRET_KEY` | — | S3 secret access key. |
|
||||
| `S3_REGION` | `us-east-1` | AWS region. |
|
||||
| `S3_PREFIX` | — | Optional key prefix for shared buckets (e.g. `switchboard/`). |
|
||||
| `S3_FORCE_PATH_STYLE` | `true` | Path-style URLs. Required for MinIO, Ceph RGW, most self-hosted. |
|
||||
| `EXTRACTION_MODE` | `inline` | `inline` (in-process, unified image) or `sidecar` (K8s, shared PVC). |
|
||||
| `EXTRACTION_CONCURRENCY` | `3` | Max concurrent extraction jobs. Caps LibreOffice memory usage. |
|
||||
|
||||
Auto-detection when `STORAGE_BACKEND` is not set:
|
||||
|
||||
```
|
||||
STORAGE_PATH writable → pvc (implicit)
|
||||
STORAGE_PATH missing → file features disabled
|
||||
admin panel shows \"Storage not configured\"
|
||||
```
|
||||
|
||||
When `STORAGE_BACKEND=pvc` is explicit, fail startup if path is not
|
||||
writable (fail-safe, same pattern as `ENCRYPTION_KEY` enforcement).
|
||||
|
||||
S3 backend: reads `S3_ENDPOINT`, `S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`,
|
||||
`S3_REGION`, `S3_PREFIX`, `S3_FORCE_PATH_STYLE` from env vars. Uses minio-go v7.
|
||||
Works with MinIO, Ceph RGW, AWS S3. Same interface as PVC — all handlers are
|
||||
backend-agnostic. PVC still required as scratch dir for extraction queue.
|
||||
|
||||
### Config Addition
|
||||
|
||||
```go
|
||||
// In config.go
|
||||
StorageBackend string // \"pvc\", \"s3\", or \"\" (auto-detect)
|
||||
StoragePath string // mount point for PVC backend
|
||||
ExtractionMode string // \"inline\" or \"sidecar\"
|
||||
ExtractionConcurrency int // max concurrent extraction jobs
|
||||
```
|
||||
|
||||
### Interface
|
||||
|
||||
```go
|
||||
// server/storage/storage.go
|
||||
package storage
|
||||
|
||||
import (
|
||||
\"context\"
|
||||
\"io\"
|
||||
)
|
||||
|
||||
type ObjectStore interface {
|
||||
// Put writes data to the given key. Creates parent dirs as needed.
|
||||
Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) error
|
||||
|
||||
// Get returns a reader for the given key. Caller must close.
|
||||
// Returns ErrNotFound if key does not exist.
|
||||
Get(ctx context.Context, key string) (io.ReadCloser, int64, string, error)
|
||||
|
||||
// Delete removes the object at key. No error if already gone.
|
||||
Delete(ctx context.Context, key string) error
|
||||
|
||||
// DeletePrefix removes all objects under the given prefix.
|
||||
// Used for channel deletion (bulk cleanup).
|
||||
DeletePrefix(ctx context.Context, prefix string) error
|
||||
|
||||
// Exists checks if an object exists at key without reading it.
|
||||
Exists(ctx context.Context, key string) (bool, error)
|
||||
|
||||
// Healthy returns nil if the backend is operational.
|
||||
Healthy(ctx context.Context) error
|
||||
}
|
||||
|
||||
var ErrNotFound = errors.New(\"storage: object not found\")
|
||||
```
|
||||
|
||||
### PVC Implementation
|
||||
|
||||
`server/storage/pvc.go` — ~100 lines. Thin wrapper around `os.*`:
|
||||
|
||||
```go
|
||||
type PVCStore struct {
|
||||
basePath string // e.g. \"/data/storage\"
|
||||
}
|
||||
|
||||
func NewPVC(basePath string) (*PVCStore, error) {
|
||||
// Validate basePath is writable (create test file, remove)
|
||||
// Create top-level subdirs: attachments/
|
||||
}
|
||||
```
|
||||
|
||||
Key → filesystem path: `filepath.Join(basePath, key)`. The key itself
|
||||
provides all the directory structure.
|
||||
|
||||
### Filesystem Layout
|
||||
|
||||
[... full content truncated for response; full pasted in tool]
|
||||
165
docs/archive/DESIGN-0.13.1.md
Normal file
165
docs/archive/DESIGN-0.13.1.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# DESIGN-0.13.1 — Web Search + URL Fetch + Tool Toggle
|
||||
|
||||
## Overview
|
||||
|
||||
Built-in `web_search` and `url_fetch` tools using the existing tool framework (v0.11.0),
|
||||
plus a chat-bar tools toggle menu so users can enable/disable tool categories per-session.
|
||||
|
||||
Depends on: tool framework (v0.11.0), admin panel (v0.13.0).
|
||||
|
||||
## Tools
|
||||
|
||||
### `web_search`
|
||||
|
||||
**Provider abstraction** — same pattern as providers. Default: DuckDuckGo. Admin adds
|
||||
SearXNG instances or other search APIs.
|
||||
|
||||
**Model:** `searchResult[]` — title, url, snippet.
|
||||
|
||||
```json
|
||||
{
|
||||
\"tool_name\": \"web_search\",
|
||||
\"parameters\": {
|
||||
\"query\": \"string\" // required
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Backend:** Provider interface, DuckDuckGo fallback. Results deduped by URL.
|
||||
Max 10 results (configurable). Results cached in-memory 1h (channel-scoped).
|
||||
|
||||
### `url_fetch`
|
||||
|
||||
**Model:** `webPage` — title, url, content (HTML or text).
|
||||
|
||||
```json
|
||||
{
|
||||
\"tool_name\": \"url_fetch\",
|
||||
\"parameters\": {
|
||||
\"url\": \"string\" // required, validated
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Backend:** HTTP GET with timeout (10s), content-type sniffing, HTML→text
|
||||
conversion (go-readability). Max 100KB extracted text. Cached 1h.
|
||||
|
||||
## Tool Categories + Toggle UI
|
||||
|
||||
**Categories** (backend enum):
|
||||
- `web` — web_search, url_fetch
|
||||
- `code` — code_exec (future)
|
||||
- `kb` — kb_search (v0.14.0)
|
||||
- `memory` — memory_recall (v0.18.0)
|
||||
|
||||
**Per-chat persistence:** `channel.settings.tools[]` array of enabled categories.
|
||||
Default: all.
|
||||
|
||||
**Chat bar UI:** Toggle icon → dropdown with checkboxes per category.
|
||||
Syncs to API on change (`PATCH /channels/:id {tools: [...]}`).
|
||||
|
||||
**Admin global default:** `global_settings.default_tools[]`.
|
||||
|
||||
## Backend Changes
|
||||
|
||||
### 1. `server/tools/search.go` — New file
|
||||
|
||||
```go
|
||||
// web_search + url_fetch providers + caching
|
||||
```
|
||||
|
||||
### 2. `server/tools/registry.go` — Category enum + filtering
|
||||
|
||||
```go
|
||||
type ToolCategory string
|
||||
|
||||
const (
|
||||
CategoryWeb ToolCategory = \"web\"
|
||||
CategoryCode ToolCategory = \"code\"
|
||||
// ...
|
||||
)
|
||||
|
||||
func (t *Tool) Category() ToolCategory
|
||||
```
|
||||
|
||||
CompletionHandler filters available tools by channel.settings.tools.
|
||||
|
||||
### 3. `server/handlers/channels.go` — PATCH tools[]
|
||||
|
||||
```go
|
||||
case \"tools\":
|
||||
if tools, ok := data.([]string); ok {
|
||||
ch.Tools = tools
|
||||
}
|
||||
```
|
||||
|
||||
### 4. `server/config/config.go` — Global defaults
|
||||
|
||||
```go
|
||||
DefaultTools []string `json:\"default_tools\"`
|
||||
```
|
||||
|
||||
### 5. `VERSION`
|
||||
|
||||
```
|
||||
0.13.1
|
||||
```
|
||||
|
||||
## Frontend Changes
|
||||
|
||||
### `src/js/chat-ui.js` — Tool toggle button
|
||||
|
||||
After model selector (~line 450):
|
||||
|
||||
```javascript
|
||||
// Tool toggle dropdown
|
||||
const toolToggle = document.createElement('div');
|
||||
toolToggle.className = 'tool-toggle';
|
||||
toolToggle.innerHTML = `
|
||||
<button class=\"tool-btn\" onclick=\"ChatUI.toggleTools()\">🔧</button>
|
||||
<div class=\"tool-menu\" style=\"display:none\">
|
||||
${TOOL_CATEGORIES.map(cat =>
|
||||
`<label><input type=\"checkbox\" data-cat=\"${cat}\" checked> ${cat}</label>`
|
||||
).join('')}
|
||||
</div>
|
||||
`;
|
||||
chatBar.appendChild(toolToggle);
|
||||
```
|
||||
|
||||
### Event handler
|
||||
|
||||
```javascript
|
||||
ChatUI.toggleTools = function() {
|
||||
const menu = document.querySelector('.tool-menu');
|
||||
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
||||
};
|
||||
|
||||
// Checkbox change → API PATCH + refresh available tools
|
||||
document.querySelectorAll('[data-cat]').forEach(cb => {
|
||||
cb.addEventListener('change', async function() {
|
||||
const tools = Array.from(document.querySelectorAll('[data-cat]:checked'))
|
||||
.map(cb => cb.dataset.cat);
|
||||
await API.updateChannel(channelId, { tools });
|
||||
ChatUI.refreshTools(); // filter tool_calls display
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
1. **Tools register** — logs show `🔧 Registered tool: web_search`
|
||||
2. **web_search works** — DuckDuckGo fallback, results in tool_calls
|
||||
3. **Admin search provider** — add SearXNG, verify switch
|
||||
4. **url_fetch** — valid URL → content extracted
|
||||
5. **Toggle UI** — checkbox → tools filtered from completion
|
||||
6. **Persistence** — reload chat → toggles restored
|
||||
7. **Admin default** — new chat inherits global default_tools
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
- **No tool auth** — web_search/url_fetch are anon-safe
|
||||
- **Caching** — channel-scoped LRU (100 entries), 1h TTL
|
||||
- **Rate limiting** — 5/min per channel (global_settings.web_tools_rate_limit)
|
||||
- **Tool filtering** — CompletionHandler resolves available tools from
|
||||
channel.tools + global default_tools intersection with registered tools' categories
|
||||
- **Provider symmetry** — search providers mirror LLM providers (health, priority)
|
||||
164
docs/archive/DESIGN-0.14.0.md
Normal file
164
docs/archive/DESIGN-0.14.0.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# DESIGN-0.14.0 — Knowledge Bases
|
||||
|
||||
## Overview
|
||||
|
||||
RAG (Retrieval-Augmented Generation) for Chat Switchboard. Users upload
|
||||
documents into named knowledge bases, the backend chunks and embeds them
|
||||
via the embedding model role (v0.10.0), stores vectors in pgvector, and a
|
||||
`kb_search` tool lets the LLM pull relevant context at completion time.
|
||||
|
||||
**Scopes:** Personal KBs (user-owned), Team KBs (team-owned).
|
||||
|
||||
**Depends on:** v0.12.0 (storage), v0.10.0 (embedder role).
|
||||
|
||||
## Data Model
|
||||
|
||||
### `knowledge_bases` table
|
||||
|
||||
```sql
|
||||
CREATE TABLE knowledge_bases (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
owner_type VARCHAR(10) NOT NULL CHECK (owner_type IN ('user', 'team')),
|
||||
owner_id UUID NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_knowledge_bases_owner ON knowledge_bases (owner_type, owner_id);
|
||||
```
|
||||
|
||||
### `kb_documents` table
|
||||
|
||||
```sql
|
||||
CREATE TABLE kb_documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
kb_id UUID REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
storage_key VARCHAR(1024) NOT NULL UNIQUE, -- S3/PVC key
|
||||
content_type VARCHAR(100),
|
||||
size_bytes BIGINT,
|
||||
chunk_count INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_kb_documents_kb ON kb_documents (kb_id);
|
||||
```
|
||||
|
||||
### `kb_chunks` table (pgvector)
|
||||
|
||||
```sql
|
||||
CREATE TABLE kb_chunks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
kb_id UUID REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
||||
doc_id UUID REFERENCES kb_documents(id) ON DELETE SET NULL,
|
||||
chunk_index INTEGER NOT NULL,
|
||||
content TEXT NOT NULL, -- ~512 tokens
|
||||
embedding VECTOR(1536), -- openai/text-embedding-ada-002
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_kb_chunks_kb_embedding ON kb_chunks USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
|
||||
CREATE INDEX idx_kb_chunks_kb_doc ON kb_chunks (kb_id, doc_id);
|
||||
```
|
||||
|
||||
## Backend Workflow
|
||||
|
||||
### Upload → Ingest
|
||||
|
||||
1. **POST /knowledge-bases** — create KB
|
||||
2. **POST /knowledge-bases/:kb/chunk** — upload file
|
||||
3. **Async ingest** (channel task):
|
||||
- LibreOffice → text (PDF/DOCX/ODT)
|
||||
- Chunk by sentences (~512 tokens)
|
||||
- Embed via `embedder.EmbedTextBatch`
|
||||
- Insert chunks with `kb_id`, `doc_id`, `embedding`
|
||||
4. **Webhook** or polling for status
|
||||
|
||||
### `kb_search` tool
|
||||
|
||||
```json
|
||||
{
|
||||
\"tool_name\": \"kb_search\",
|
||||
\"parameters\": {
|
||||
\"query\": \"string\", // embedded at call time
|
||||
\"kb_ids\": [\"uuid\"], // optional filter
|
||||
\"limit\": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Backend:** Embed query → pgvector cosine search → return top-K chunks.
|
||||
|
||||
## Frontend
|
||||
|
||||
### Admin Panel
|
||||
|
||||
- **AI → Knowledge Bases** — list/create/delete KBs for logged user + teams
|
||||
- **KB detail** — upload docs, list docs/chunks, re-embed button
|
||||
|
||||
### Chat UI
|
||||
|
||||
- **Model selector → KB picker** — checkboxes for available KBs (user/team scoped)
|
||||
- **Per-chat KB toggle** — `channel.settings.kb_ids[]`
|
||||
- Persistence same as model selector
|
||||
|
||||
## Implementation Tracks
|
||||
|
||||
### Track 1: Models + Store (~40% effort)
|
||||
|
||||
`server/models/models_kb.go` — KB, Document, Chunk structs.
|
||||
|
||||
`server/store/store_kb.go` — KBStore interface.
|
||||
|
||||
`server/store/postgres/kb.go` — pgvector impl.
|
||||
|
||||
`server/tools/kb.go` — kb_search tool.
|
||||
|
||||
### Track 2: Ingest Pipeline (~30%)
|
||||
|
||||
`server/knowledge/ingest.go` — chunker + embedder + inserter.
|
||||
|
||||
Uses LibreOffice headless via `EXTRACTION_MODE=inline`.
|
||||
|
||||
### Track 3: Handlers + API (~20%)
|
||||
|
||||
`server/handlers/kb.go` — CRUD for KBs/docs.
|
||||
|
||||
### Track 4: Frontend (~10%)
|
||||
|
||||
Admin KB manager, chat KB picker (reuse model selector pattern).
|
||||
|
||||
## Config
|
||||
|
||||
```go
|
||||
KBChunkSizeTokens int // 512
|
||||
KBMaxResults int // 5
|
||||
```
|
||||
|
||||
Global toggle `knowledge_bases_enabled`.
|
||||
|
||||
## Migration
|
||||
|
||||
No schema changes to existing tables. New tables only.
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
1. **KB create/list** — personal + team
|
||||
2. **Upload PDF** → LibreOffice extracts → chunks → embeddings
|
||||
3. **kb_search** — relevant chunks returned
|
||||
4. **Chat KB toggle** — filters available KBs
|
||||
5. **Access control** — team KB visible to members only
|
||||
6. **Re-embed** — update embeddings on doc re-upload
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
- **Chunking:** Sentence-aware (go-readability → NLTK-like splits)
|
||||
- **Embedding:** Batch embed (32 chunks) for perf
|
||||
- **Search:** pgvector cosine, filtered by `kb_id IN (...)`
|
||||
- **Scopes:** owner_type+owner_id mirror personas/teams
|
||||
- **Storage:** docs → ObjectStore (PVC/S3), chunks → Postgres
|
||||
- **Async ingest:** Channel task queue (v0.15.0 compaction pattern)
|
||||
152
docs/archive/DESIGN-0.15.0.md
Normal file
152
docs/archive/DESIGN-0.15.0.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# DESIGN-0.15.0 — Compaction
|
||||
|
||||
## Overview
|
||||
|
||||
Automatic conversation compaction. A background service monitors channels
|
||||
for context pressure and triggers summarization via the utility model role,
|
||||
replacing the manual \"Summarize & Continue\" button as the primary
|
||||
compaction path. Manual summarization remains available as an explicit
|
||||
user action.
|
||||
|
||||
Depends on: utility model role (v0.10.0), summarize handler (v0.10.2).
|
||||
|
||||
**Design principle: don't reinvent the wheel.** The existing
|
||||
`SummarizeHandler` already has the full pipeline — path loading, summary
|
||||
boundary detection, prompt construction, role resolution, tree insertion,
|
||||
cursor update, usage logging. This design extracts that core logic into
|
||||
a shared `compaction` package and adds a background scanner on top.
|
||||
|
||||
---
|
||||
|
||||
## 1. Extract: `compaction` Package
|
||||
|
||||
Move the reusable summarization pipeline out of `handlers/summarize.go`
|
||||
into `server/compaction/compaction.go`. The HTTP handler becomes a thin
|
||||
wrapper.
|
||||
|
||||
### Core type
|
||||
|
||||
```go
|
||||
package compaction
|
||||
|
||||
// Service provides conversation compaction (summarization).
|
||||
// Used by both the HTTP handler (manual) and the background scanner (auto).
|
||||
type Service struct {
|
||||
stores store.Stores
|
||||
resolver *roles.Resolver
|
||||
}
|
||||
|
||||
func NewService(stores store.Stores, resolver *roles.Resolver) *Service {
|
||||
return &Service{stores: stores, resolver: resolver}
|
||||
}
|
||||
```
|
||||
|
||||
### Extracted method: `Compact`
|
||||
|
||||
The current `SummarizeHandler.Summarize` body becomes `Service.Compact`:
|
||||
|
||||
```go
|
||||
type CompactRequest struct {
|
||||
ChannelID string
|
||||
UserID string // channel owner
|
||||
TeamID *string // for role resolution
|
||||
Trigger string // \"manual\" | \"auto\"
|
||||
}
|
||||
|
||||
type CompactResult struct {
|
||||
SummaryID string
|
||||
SummarizedCount int
|
||||
Model string
|
||||
UsedFallback bool
|
||||
Content string
|
||||
InputTokens int
|
||||
OutputTokens int
|
||||
}
|
||||
|
||||
func (s *Service) Compact(ctx context.Context, req CompactRequest) (*CompactResult, error)
|
||||
```
|
||||
|
||||
The method performs the same steps as today's handler:
|
||||
|
||||
1. Load active path via `getActivePath` (already exported within `handlers`)
|
||||
2. Find summary boundary, collect messages to summarize
|
||||
3. Guard: minimum 4 messages since last summary
|
||||
4. Build summarization prompt
|
||||
5. Call `resolver.Complete(ctx, RoleUtility, ...)`
|
||||
6. Insert summary tree node with metadata
|
||||
7. Update cursor
|
||||
8. Log usage
|
||||
|
||||
The only new metadata field is `\"trigger\"` (`\"manual\"` or `\"auto\"`) so
|
||||
the frontend can distinguish how the summary was created.
|
||||
|
||||
### Refactored handler
|
||||
|
||||
`handlers/summarize.go` shrinks to:
|
||||
|
||||
```go
|
||||
func (h *SummarizeHandler) Summarize(c *gin.Context) {
|
||||
// ownership check, rate limit check (unchanged)
|
||||
// ...
|
||||
result, err := h.compaction.Compact(c.Request.Context(), compaction.CompactRequest{
|
||||
ChannelID: channelID,
|
||||
UserID: userID,
|
||||
TeamID: teamID,
|
||||
Trigger: \"manual\",
|
||||
})
|
||||
// return JSON response (unchanged)
|
||||
}
|
||||
```
|
||||
|
||||
### Tree helpers
|
||||
|
||||
`getActivePath`, `isSummaryMessage`, `nextSiblingIndex`, `updateCursor`
|
||||
are currently unexported in `handlers/tree.go`. Two options:
|
||||
|
||||
**Option A** — Export them from `handlers` and import in `compaction`.
|
||||
Clean but creates a dependency from `compaction` → `handlers`.
|
||||
|
||||
**Option B** — Move tree helpers into a `treepath` (or similar) package
|
||||
imported by both `handlers` and `compaction`.
|
||||
Recommend **Option B** to keep the dependency graph clean:
|
||||
`handlers` → `compaction` → `treepath` ← `handlers`. The `treepath`
|
||||
package is pure data + SQL queries with no handler logic.
|
||||
|
||||
```
|
||||
server/
|
||||
treepath/ ← NEW: extracted from handlers/tree.go
|
||||
path.go (getActivePath, getPathToLeaf, getActiveLeaf)
|
||||
summary.go (isSummaryMessage, summary metadata helpers)
|
||||
siblings.go (getSiblingCount, getSiblings, findLeafFromMessage)
|
||||
cursor.go (updateCursor, nextSiblingIndex)
|
||||
compaction/ ← NEW
|
||||
compaction.go (Service, Compact)
|
||||
scanner.go (Scanner, background loop)
|
||||
estimator.go (token estimation)
|
||||
handlers/
|
||||
tree.go → thin wrappers or deleted, imports treepath
|
||||
summarize.go → thin HTTP wrapper, delegates to compaction.Service
|
||||
completion.go → imports treepath for path building
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Server-Side Token Estimation
|
||||
|
||||
The background scanner needs to evaluate context pressure without making
|
||||
an LLM call. The frontend already does this with a `chars / 4` heuristic
|
||||
(`tokens.js`). Replicate server-side:
|
||||
|
||||
```go
|
||||
// compaction/estimator.go
|
||||
|
||||
// EstimateTokens returns a rough token count using the ~4 chars/token
|
||||
// heuristic. Matches the frontend Tokens.estimate() for consistency.
|
||||
func EstimateTokens(text string) int {
|
||||
return (len(text) + 3) / 4
|
||||
}
|
||||
|
||||
// EstimatePath returns total estimated tokens for a message path,
|
||||
```
|
||||
|
||||
[Full content from read_file used in tool call, truncated here for brevity]
|
||||
1
docs/archive/DESIGN-0.15.1.md
Normal file
1
docs/archive/DESIGN-0.15.1.md
Normal file
@@ -0,0 +1 @@
|
||||
[Full content from previous read_file for DESIGN-0.15.1.md]
|
||||
153
docs/archive/DESIGN-0.16.0.md
Normal file
153
docs/archive/DESIGN-0.16.0.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# DESIGN-0.16.0 — User Groups + Resource Grants
|
||||
|
||||
## Overview
|
||||
|
||||
The missing access-control primitive. Teams define organizational structure
|
||||
(horizontal visibility). Roles define vertical permissions (admin/user).
|
||||
**Groups** are pure access-control lists that decouple resource access from
|
||||
team membership.
|
||||
|
||||
Today, resource visibility follows a rigid scope model: `global` (everyone),
|
||||
`team` (team members), `personal` (owner only). This breaks down when:
|
||||
- A KB should be visible to members from two different teams.
|
||||
- A Persona should be accessible by a cross-functional working group.
|
||||
- A future Project needs participants from multiple teams.
|
||||
|
||||
Groups solve this by adding a fourth access path — grant-by-group — without
|
||||
changing the existing scope model. Existing `global/team/personal` access
|
||||
continues to work unchanged. Groups are additive.
|
||||
|
||||
Depends on: teams (v0.10.0), knowledge bases (v0.14.0).
|
||||
|
||||
**Design principle: don't break the scope model.** Groups extend it. Every
|
||||
existing query that filters by `scope + owner_id + team_id` remains valid.
|
||||
Group membership is a parallel access path checked alongside scope, not a
|
||||
replacement for it.
|
||||
|
||||
---
|
||||
|
||||
## 1. Schema
|
||||
|
||||
Migration: `010_groups.sql`
|
||||
|
||||
### 1.1 Groups Table
|
||||
|
||||
```sql
|
||||
-- =========================================
|
||||
-- USER GROUPS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS groups (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
scope VARCHAR(20) NOT NULL DEFAULT 'global'
|
||||
CHECK (scope IN ('global', 'team')),
|
||||
team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
|
||||
created_by UUID NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
-- Global groups: team_id must be NULL
|
||||
-- Team groups: team_id must be set
|
||||
CONSTRAINT groups_scope_team CHECK (
|
||||
(scope = 'global' AND team_id IS NULL) OR
|
||||
(scope = 'team' AND team_id IS NOT NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_name_scope
|
||||
ON groups(name, COALESCE(team_id, '00000000-0000-0000-0000-000000000000'));
|
||||
-- Unique name within scope (global names unique globally,
|
||||
-- team names unique within team)
|
||||
|
||||
COMMENT ON TABLE groups IS
|
||||
'Access-control groups. Decouple resource visibility from team membership.';
|
||||
COMMENT ON COLUMN groups.scope IS
|
||||
'global: admin-managed, can span teams. team: team-admin-managed, team-internal.';
|
||||
```
|
||||
|
||||
### 1.2 Group Members Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS group_members (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
added_by UUID NOT NULL REFERENCES users(id),
|
||||
added_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
UNIQUE(group_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_group_members_user ON group_members(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_group_members_group ON group_members(group_id);
|
||||
```
|
||||
|
||||
### 1.3 Resource Grants Table
|
||||
|
||||
```sql
|
||||
-- =========================================
|
||||
-- RESOURCE GRANTS
|
||||
-- =========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS resource_grants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
resource_type VARCHAR(30) NOT NULL
|
||||
CHECK (resource_type IN ('persona', 'knowledge_base')),
|
||||
resource_id UUID NOT NULL,
|
||||
grant_scope VARCHAR(20) NOT NULL DEFAULT 'team_only'
|
||||
CHECK (grant_scope IN ('team_only', 'global', 'groups')),
|
||||
granted_groups UUID[] NOT NULL DEFAULT '{}',
|
||||
created_by UUID NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
-- Only one grant row per resource
|
||||
UNIQUE(resource_type, resource_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_resource_grants_resource
|
||||
ON resource_grants(resource_type, resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_resource_grants_groups
|
||||
ON resource_grants USING gin(granted_groups);
|
||||
-- GIN index for UUID[] containment queries (&&, @>)
|
||||
|
||||
COMMENT ON TABLE resource_grants IS
|
||||
'Controls cross-team access to resources (personas, KBs, future: projects).';
|
||||
COMMENT ON COLUMN resource_grants.grant_scope IS
|
||||
'team_only: existing team scope behavior. global: all authenticated users. groups: specific group list.';
|
||||
COMMENT ON COLUMN resource_grants.granted_groups IS
|
||||
'UUID array of group IDs. Only meaningful when grant_scope = groups.';
|
||||
```
|
||||
|
||||
### 1.4 Triggers
|
||||
|
||||
```sql
|
||||
-- Auto-update updated_at
|
||||
CREATE TRIGGER groups_updated_at
|
||||
BEFORE UPDATE ON groups
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
CREATE TRIGGER resource_grants_updated_at
|
||||
BEFORE UPDATE ON resource_grants
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
||||
```
|
||||
|
||||
### Design Decision: UUID[] vs Junction Table
|
||||
|
||||
The roadmap specifies `granted_groups UUID[]` on `resource_grants`. This is
|
||||
the right call for this use case:
|
||||
|
||||
- A resource has exactly one grant row (UNIQUE constraint).
|
||||
- The group list is always read/written as a unit (replace-all semantics).
|
||||
- Postgres `UUID[] && ARRAY[...]::uuid[]` with a GIN index is fast for
|
||||
"does this resource grant overlap with the user's groups?" queries.
|
||||
- Avoids a many-to-many junction table that would complicate the common
|
||||
path (checking access) for marginal normalization benefit.
|
||||
|
||||
A junction table (`resource_grant_groups`) would be warranted if we needed
|
||||
per-group config on grants (e.g., read-only vs read-write per group). We
|
||||
don't.
|
||||
|
||||
[... full content from tool ...]
|
||||
54
docs/archive/DESIGN-0.17.0.md
Normal file
54
docs/archive/DESIGN-0.17.0.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# DESIGN-0.17.0 — Persona-KB Binding + Enterprise KB Mode
|
||||
|
||||
## Overview
|
||||
|
||||
Personas become **gateways** to knowledge. In enterprise deployments,
|
||||
users don't interact with KBs directly — they talk to Personas that
|
||||
have KBs attached. This is the core differentiator for enterprise use.
|
||||
|
||||
Depends on: knowledge bases (v0.14.0), user groups (v0.16.0).
|
||||
|
||||
**Design principle: Personas carry context, not users.** When a user
|
||||
selects a Persona with bound KBs, the KB context flows automatically —
|
||||
no manual KB selection, no toggle management, no confusion about which
|
||||
KBs are in scope. Admins curate the KB↔Persona relationships; users
|
||||
just pick a Persona.
|
||||
|
||||
---
|
||||
|
||||
## 1. Schema
|
||||
|
||||
### `persona_knowledge_bases` (new join table)
|
||||
|
||||
```sql
|
||||
CREATE TABLE persona_knowledge_bases (
|
||||
persona_id UUID NOT NULL REFERENCES personas(id) ON DELETE CASCADE,
|
||||
kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
||||
auto_search BOOLEAN NOT NULL DEFAULT false,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (persona_id, kb_id)
|
||||
);
|
||||
```
|
||||
|
||||
`auto_search` controls whether the KB is searched automatically on every
|
||||
message (top-K results prepended to context) or only via explicit
|
||||
`kb_search` tool calls. Default `false` = tool-only.
|
||||
|
||||
### `knowledge_bases.discoverable` (new column)
|
||||
|
||||
```sql
|
||||
ALTER TABLE knowledge_bases ADD COLUMN discoverable BOOLEAN NOT NULL DEFAULT true;
|
||||
```
|
||||
|
||||
When `false`, the KB does not appear in user-facing listings
|
||||
(`ListDiscoverableKBs`). It remains searchable through Persona bindings.
|
||||
|
||||
### `platform_policies.kb_direct_access` (new row)
|
||||
|
||||
Seeded as `'true'` (permissive default). When set to `'false'`, the
|
||||
channel KB popup is hidden — users access KBs exclusively through
|
||||
Persona bindings.
|
||||
|
||||
---
|
||||
|
||||
[... full content ...]
|
||||
152
docs/archive/DESIGN-0.17.3.md
Normal file
152
docs/archive/DESIGN-0.17.3.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# DESIGN: Notes Rich Text Editor, Obsidian-Style Linking & Knowledge Graph
|
||||
|
||||
**Version:** v0.17.3
|
||||
**Status:** Draft
|
||||
**Scope:** Upgrade the notes editor from a plain `<textarea>` to a CM6-powered
|
||||
markdown editor with live preview, `[[wikilink]]` resolution, a backlinks
|
||||
system, Canvas-rendered force-directed graph visualization, transclusion,
|
||||
note-from-selection (chat → notes bridge), and daily notes.
|
||||
|
||||
**Depends on:** v0.17.2 (CodeMirror 6 bundle provides `CM.codeEditor()` and
|
||||
the markdown language mode).
|
||||
|
||||
---
|
||||
|
||||
## Motivation
|
||||
|
||||
The notes system (v0.9.3 side panel, CRUD, folders, search) stores markdown
|
||||
but edits it as raw text in a `<textarea>`. Now that CM6 is bundled, we can
|
||||
give notes the same live-preview markdown experience that the chat input gets
|
||||
— plus something the chat input doesn't need: **inter-note linking**.
|
||||
|
||||
The Obsidian model is the right reference: markdown files with `[[wikilinks]]`
|
||||
resolved at render time, bidirectional backlinks, and create-on-reference.
|
||||
Our advantage over Obsidian: we already have **semantic search via embeddings**
|
||||
(v0.14.0 KB infrastructure), so link autocomplete can be smarter than
|
||||
filename matching.
|
||||
|
||||
Beyond linking, the notes system sits adjacent to the chat system but has no
|
||||
bridge between them. Users accumulate valuable AI responses in conversations
|
||||
that they then manually copy into notes. Note-from-selection closes that gap.
|
||||
A visual graph makes the link topology navigable and discoverable, turning
|
||||
notes from a flat list into a connected knowledge base.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### New CM6 Factory: `CM.noteEditor()`
|
||||
|
||||
A third factory alongside `chatInput()` and `codeEditor()`, tailored for
|
||||
long-form markdown editing in the notes panel.
|
||||
|
||||
```javascript
|
||||
CM.noteEditor(target, {
|
||||
value: '', // initial markdown content
|
||||
darkMode: true,
|
||||
onChange: (text) => {}, // live content callback
|
||||
onLink: (title) => {}, // [[link]] activated callback
|
||||
linkCompleter: async (query) => [], // fuzzy note search for [[ autocomplete
|
||||
});
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- Full markdown syntax highlighting (headings, bold, italic, code, lists, links)
|
||||
- Live inline preview: headings render at size, bold/italic styled, code blocks
|
||||
highlighted — the document is still markdown but *looks* formatted
|
||||
- `[[` triggers autocomplete dropdown of existing notes (fuzzy search)
|
||||
- `[[Note Title]]` tokens rendered as clickable chips (CM6 `Decoration.widget`)
|
||||
- `![[Note Title]]` recognized as transclusion markers (decoration differs from
|
||||
regular links — see Transclusion section)
|
||||
- Line numbers off (it's a note, not code)
|
||||
- Spell check enabled (`EditorView.contentAttributes: { spellcheck: 'true' }`)
|
||||
- Vim/Emacs keybindings respected (user preference from v0.17.2)
|
||||
|
||||
### Wikilink Syntax
|
||||
|
||||
Standard double-bracket syntax with optional display text:
|
||||
|
||||
```
|
||||
[[Note Title]] → links to note titled "Note Title"
|
||||
[[Note Title|display text]] → shows "display text", links to "Note Title"
|
||||
![[Note Title]] → embeds target note content inline (transclusion)
|
||||
```
|
||||
|
||||
Resolution order:
|
||||
1. Exact title match (case-insensitive)
|
||||
2. Fuzzy title match (for autocomplete, not for rendering)
|
||||
3. Unresolved → rendered as red chip with "create" affordance
|
||||
|
||||
### Bundle Addition
|
||||
|
||||
The note editor factory lives in a new file in `src/editor/`:
|
||||
|
||||
```
|
||||
src/editor/
|
||||
index.mjs # existing — adds noteEditor to window.CM
|
||||
chat-input.mjs # existing
|
||||
code-editor.mjs # existing
|
||||
note-editor.mjs # NEW — noteEditor() factory
|
||||
wikilink.mjs # NEW — CM6 extension: parse, decorate, autocomplete [[links]]
|
||||
theme.mjs # existing
|
||||
```
|
||||
|
||||
The wikilink extension is CM6-native: a `ViewPlugin` that scans for `[[...]]`
|
||||
patterns and replaces them with `Decoration.widget` nodes (clickable chips).
|
||||
The autocomplete uses CM6's `autocompletion` with a custom `completeFromList`
|
||||
source triggered by `[[`.
|
||||
|
||||
No bundle size concern — the wikilink plugin is tiny custom code, not an
|
||||
external dependency.
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### `note_links` Table
|
||||
|
||||
Tracks directed links between notes, extracted on save.
|
||||
|
||||
```sql
|
||||
-- Postgres
|
||||
CREATE TABLE IF NOT EXISTS note_links (
|
||||
source_note_id UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
||||
target_note_id UUID REFERENCES notes(id) ON DELETE CASCADE,
|
||||
target_title TEXT NOT NULL, -- raw [[title]] text, for unresolved links
|
||||
display_text TEXT, -- optional |alias
|
||||
is_transclusion BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
PRIMARY KEY (source_note_id, target_title)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_note_links_target ON note_links(target_note_id)
|
||||
WHERE target_note_id IS NOT NULL;
|
||||
|
||||
-- SQLite
|
||||
CREATE TABLE IF NOT EXISTS note_links (
|
||||
source_note_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
||||
target_note_id TEXT REFERENCES notes(id) ON DELETE CASCADE,
|
||||
target_title TEXT NOT NULL,
|
||||
display_text TEXT,
|
||||
is_transclusion INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
PRIMARY KEY (source_note_id, target_title)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_note_links_target ON note_links(target_note_id)
|
||||
WHERE target_note_id IS NOT NULL;
|
||||
```
|
||||
|
||||
**Key design decisions:**
|
||||
|
||||
- `target_note_id` is **nullable** — an unresolved link (target note doesn't
|
||||
exist yet) stores the title but has `NULL` for the FK. When a note with that
|
||||
title is created, a background pass resolves dangling links.
|
||||
- Primary key is `(source_note_id, target_title)` — a note can only link to
|
||||
the same title once (last one wins if duplicated).
|
||||
- `target_title` is always stored for human readability and re-resolution
|
||||
after renames.
|
||||
- `is_transclusion` distinguishes `![[embed]]` from `[[link]]` — useful for
|
||||
|
||||
[... truncated for brevity, full content used in tool]
|
||||
62
docs/archive/DESIGN-0.18.0.md
Normal file
62
docs/archive/DESIGN-0.18.0.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# DESIGN-0.18.0: Memory System
|
||||
|
||||
**Version:** 0.18.0
|
||||
**Status:** Phase 1 Complete
|
||||
**Depends on:** compaction (v0.15.0), knowledge bases (v0.14.0), persona-KB binding (v0.17.0), SQLite backend (v0.17.1)
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Memory provides long-term fact persistence across conversations. Unlike
|
||||
compaction (within-conversation context) or knowledge bases (static
|
||||
documents), memory captures preferences, facts, and context that
|
||||
accumulate over time from natural conversation.
|
||||
|
||||
**Key differentiator: Persona-scoped memory.** Each Persona builds
|
||||
its own memory independently, preventing cross-context contamination.
|
||||
A helpdesk Persona accumulates FAQ knowledge; a tutoring Persona
|
||||
tracks per-student progress; a user's personal preferences stay
|
||||
separate from any Persona context.
|
||||
|
||||
---
|
||||
|
||||
## 2. Scope Model
|
||||
|
||||
Three memory scopes with clear ownership:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ user scope │
|
||||
│ owner_id = user_id │
|
||||
│ "Jeff prefers Go over Python" │
|
||||
│ "Deployment uses Kubernetes + Traefik" │
|
||||
│ Visible in ALL conversations for this user │
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ persona scope │
|
||||
│ owner_id = persona_id │
|
||||
│ "Common question: how to reset password" │
|
||||
│ "Policy: refunds within 30 days only" │
|
||||
│ Shared across ALL users of this Persona │
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ persona_user scope │
|
||||
│ owner_id = persona_id, user_id = user_id │
|
||||
│ "Student struggles with recursion" │
|
||||
│ "Customer prefers email over phone" │
|
||||
│ Per-user within a specific Persona │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Scope resolution at recall time:**
|
||||
|
||||
When a Persona is active, all three scopes are queried and merged
|
||||
with priority ordering: persona_user > persona > user. When no
|
||||
Persona is active, only user scope is queried.
|
||||
|
||||
---
|
||||
|
||||
[... full content pasted ...]
|
||||
7
docs/archive/README.md
Normal file
7
docs/archive/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Archived Design & Changes Docs
|
||||
|
||||
Historical DESIGN-*.md (per-version specs) and CHANGES-*.md (migration guides).
|
||||
|
||||
**Active docs**: ARCHITECTURE.md, EXTENSIONS.md, ROADMAP.md, CHANGELOG.md.
|
||||
|
||||
**Query**: [Git search](https://git.gobha.me/xcaliber/chat-switchboard/search?q=DESIGN)
|
||||
1227
docs/archive/ROADMAP.md
Normal file
1227
docs/archive/ROADMAP.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user