engrm 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp.json +9 -0
- package/AUTH-DESIGN.md +436 -0
- package/BRIEF.md +197 -0
- package/CLAUDE.md +44 -0
- package/COMPETITIVE.md +174 -0
- package/CONTEXT-OPTIMIZATION.md +305 -0
- package/INFRASTRUCTURE.md +252 -0
- package/LICENSE +105 -0
- package/MARKET.md +230 -0
- package/PLAN.md +278 -0
- package/README.md +121 -0
- package/SENTINEL.md +293 -0
- package/SERVER-API-PLAN.md +553 -0
- package/SPEC.md +843 -0
- package/SWOT.md +148 -0
- package/SYNC-ARCHITECTURE.md +294 -0
- package/VIBE-CODER-STRATEGY.md +250 -0
- package/bun.lock +375 -0
- package/hooks/post-tool-use.ts +144 -0
- package/hooks/session-start.ts +64 -0
- package/hooks/stop.ts +131 -0
- package/mem-page.html +1305 -0
- package/package.json +30 -0
- package/src/capture/dedup.test.ts +103 -0
- package/src/capture/dedup.ts +76 -0
- package/src/capture/extractor.test.ts +245 -0
- package/src/capture/extractor.ts +330 -0
- package/src/capture/quality.test.ts +168 -0
- package/src/capture/quality.ts +104 -0
- package/src/capture/retrospective.test.ts +115 -0
- package/src/capture/retrospective.ts +121 -0
- package/src/capture/scanner.test.ts +131 -0
- package/src/capture/scanner.ts +100 -0
- package/src/capture/scrubber.test.ts +144 -0
- package/src/capture/scrubber.ts +181 -0
- package/src/cli.ts +517 -0
- package/src/config.ts +238 -0
- package/src/context/inject.test.ts +940 -0
- package/src/context/inject.ts +382 -0
- package/src/embeddings/backfill.ts +50 -0
- package/src/embeddings/embedder.test.ts +76 -0
- package/src/embeddings/embedder.ts +139 -0
- package/src/lifecycle/aging.test.ts +103 -0
- package/src/lifecycle/aging.ts +36 -0
- package/src/lifecycle/compaction.test.ts +264 -0
- package/src/lifecycle/compaction.ts +190 -0
- package/src/lifecycle/purge.test.ts +100 -0
- package/src/lifecycle/purge.ts +37 -0
- package/src/lifecycle/scheduler.test.ts +120 -0
- package/src/lifecycle/scheduler.ts +101 -0
- package/src/provisioning/browser-auth.ts +172 -0
- package/src/provisioning/provision.test.ts +198 -0
- package/src/provisioning/provision.ts +94 -0
- package/src/register.test.ts +167 -0
- package/src/register.ts +178 -0
- package/src/server.ts +436 -0
- package/src/storage/migrations.test.ts +244 -0
- package/src/storage/migrations.ts +261 -0
- package/src/storage/outbox.test.ts +229 -0
- package/src/storage/outbox.ts +131 -0
- package/src/storage/projects.test.ts +137 -0
- package/src/storage/projects.ts +184 -0
- package/src/storage/sqlite.test.ts +798 -0
- package/src/storage/sqlite.ts +934 -0
- package/src/storage/vec.test.ts +198 -0
- package/src/sync/auth.test.ts +76 -0
- package/src/sync/auth.ts +68 -0
- package/src/sync/client.ts +183 -0
- package/src/sync/engine.test.ts +94 -0
- package/src/sync/engine.ts +127 -0
- package/src/sync/pull.test.ts +279 -0
- package/src/sync/pull.ts +170 -0
- package/src/sync/push.test.ts +117 -0
- package/src/sync/push.ts +230 -0
- package/src/tools/get.ts +34 -0
- package/src/tools/pin.ts +47 -0
- package/src/tools/save.test.ts +301 -0
- package/src/tools/save.ts +231 -0
- package/src/tools/search.test.ts +69 -0
- package/src/tools/search.ts +181 -0
- package/src/tools/timeline.ts +64 -0
- package/tsconfig.json +22 -0
package/SPEC.md
ADDED
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
# Technical Specification — Engrm
|
|
2
|
+
|
|
3
|
+
## 1. Project Identity
|
|
4
|
+
|
|
5
|
+
### The Problem
|
|
6
|
+
|
|
7
|
+
The same project lives at different paths on different machines:
|
|
8
|
+
- `/Users/david/code/aimy-agent` (MacBook)
|
|
9
|
+
- `/home/david/projects/aimy-agent` (desktop)
|
|
10
|
+
- `/Volumes/Data/devs/aimy-agent` (external drive)
|
|
11
|
+
|
|
12
|
+
A string like `"aimy-agent"` is ambiguous — two different repos could share the same directory name. We need a **canonical project ID** that's the same everywhere.
|
|
13
|
+
|
|
14
|
+
### Solution: Git Remote as Canonical ID
|
|
15
|
+
|
|
16
|
+
For git repos (the vast majority of real projects), the remote URL is a globally unique, stable identifier. Normalise it to strip protocol and auth variations:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
git@github.com:unimpossible/aimy-agent.git → github.com/unimpossible/aimy-agent
|
|
20
|
+
https://github.com/unimpossible/aimy-agent.git → github.com/unimpossible/aimy-agent
|
|
21
|
+
https://david@github.com/unimpossible/aimy-agent → github.com/unimpossible/aimy-agent
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Normalisation rules**:
|
|
25
|
+
1. Strip protocol (`https://`, `git@`, `ssh://`)
|
|
26
|
+
2. Replace `:` with `/` (for SSH-style URLs)
|
|
27
|
+
3. Strip `.git` suffix
|
|
28
|
+
4. Strip auth credentials (`user@`)
|
|
29
|
+
5. Lowercase the host
|
|
30
|
+
|
|
31
|
+
**Fallbacks** (in order):
|
|
32
|
+
1. Git remote `origin` URL → normalised (preferred)
|
|
33
|
+
2. Git remote — any remote if `origin` doesn't exist
|
|
34
|
+
3. Manual `project_id` in `.engrm.json` in project root (for non-git projects)
|
|
35
|
+
4. Last resort: directory name (not great, but better than failing)
|
|
36
|
+
|
|
37
|
+
### Local Projects Table
|
|
38
|
+
|
|
39
|
+
```sql
|
|
40
|
+
CREATE TABLE projects (
|
|
41
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
42
|
+
canonical_id TEXT UNIQUE NOT NULL, -- normalised git remote URL
|
|
43
|
+
name TEXT NOT NULL, -- human-readable (repo name portion)
|
|
44
|
+
local_path TEXT, -- path on THIS machine
|
|
45
|
+
remote_url TEXT, -- original git remote URL
|
|
46
|
+
first_seen_epoch INTEGER NOT NULL,
|
|
47
|
+
last_active_epoch INTEGER NOT NULL -- updated on every observation
|
|
48
|
+
);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Auto-detection**: On session start, the plugin runs `git remote get-url origin` in the working directory. If the canonical ID exists in `projects`, update `local_path` and `last_active_epoch`. If not, insert a new row. Zero config for the developer.
|
|
52
|
+
|
|
53
|
+
### Project Config File (Optional)
|
|
54
|
+
|
|
55
|
+
For non-git projects or overrides, drop a `.engrm.json` in the project root:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"project_id": "internal/design-system",
|
|
60
|
+
"name": "Design System"
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This is optional. Git repos need nothing — it's fully automatic.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 2. Observation Schema
|
|
69
|
+
|
|
70
|
+
### Observation Lifecycle
|
|
71
|
+
|
|
72
|
+
Observations are not permanent. They have a lifecycle that manages growth and keeps search results relevant.
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
┌───────────┐ 30 days ┌───────────┐ 90 days ┌───────────┐
|
|
76
|
+
│ active │ ──────────────→ │ aging │ ──────────────→ │ archived │
|
|
77
|
+
│ │ │ │ │ │
|
|
78
|
+
│ Full text │ │ Full text │ │ Summarised │
|
|
79
|
+
│ in FTS5 │ │ in FTS5 │ │ out of FTS │
|
|
80
|
+
│ in Vector │ │ in Vector │ │ out of Vec │
|
|
81
|
+
│ Full score │ │ 0.7x score │ │ Local only │
|
|
82
|
+
└───────────┘ └───────────┘ └───────────┘
|
|
83
|
+
│
|
|
84
|
+
12 months
|
|
85
|
+
│
|
|
86
|
+
▼
|
|
87
|
+
┌───────────┐
|
|
88
|
+
│ purged │
|
|
89
|
+
│ (deleted) │
|
|
90
|
+
└───────────┘
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
| State | Age | Search weight | In FTS5 | In Candengo Vector | Counts toward quota |
|
|
94
|
+
|---|---|---|---|---|---|
|
|
95
|
+
| `active` | 0-30 days | 1.0x | Yes | Yes | Yes |
|
|
96
|
+
| `aging` | 30-90 days | 0.7x | Yes | Yes | Yes |
|
|
97
|
+
| `archived` | 90-365 days | 0.3x (local only) | No | Removed | No |
|
|
98
|
+
| `purged` | >365 days | — | — | — | No |
|
|
99
|
+
|
|
100
|
+
**Key insight**: Archived observations are removed from Candengo Vector (freeing quota and reducing noise in cross-device search) but kept in local SQLite. They're still searchable locally at reduced weight. This means the free tier's 10K limit applies to active+aging observations in the vector store, not total local history.
|
|
101
|
+
|
|
102
|
+
**Exceptions**: Observations can be **pinned** (`lifecycle = 'pinned'`) to prevent aging. Useful for architectural decisions, critical gotchas, and other knowledge that stays relevant indefinitely.
|
|
103
|
+
|
|
104
|
+
### Observation Quality Scoring
|
|
105
|
+
|
|
106
|
+
Not all observations are equal. A quality score (0.0-1.0) is assigned at capture time and influences search ranking and lifecycle.
|
|
107
|
+
|
|
108
|
+
| Signal | Score contribution | Rationale |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| Bug fix with root cause | +0.3 | High-value, prevents repeat debugging |
|
|
111
|
+
| Architectural decision | +0.3 | Long-lived, affects future work |
|
|
112
|
+
| Multiple files modified | +0.2 | Indicates non-trivial change |
|
|
113
|
+
| Error → fix sequence | +0.2 | Problem-solution pair is reusable knowledge |
|
|
114
|
+
| Test failure → fix | +0.2 | Specific, actionable |
|
|
115
|
+
| Pattern/gotcha identified | +0.2 | Transferable to other contexts |
|
|
116
|
+
| Single file read | +0.0 | Low signal, likely navigation |
|
|
117
|
+
| Simple config change | +0.05 | Minor, rarely worth retrieving |
|
|
118
|
+
| Duplicate of recent observation | -0.3 | Redundant |
|
|
119
|
+
|
|
120
|
+
Observations with quality < 0.1 are **not saved**. This is the primary noise filter.
|
|
121
|
+
|
|
122
|
+
**Compaction**: When observations are archived (90 days), related observations from the same project and session are **compacted** — summarised into a single "digest" observation. 20 observations from a debugging session become one concise summary of what was wrong, what was tried, and what fixed it. This preserves the knowledge while dramatically reducing storage.
|
|
123
|
+
|
|
124
|
+
### Local SQLite Schema
|
|
125
|
+
|
|
126
|
+
```sql
|
|
127
|
+
-- Projects (canonical identity across machines)
|
|
128
|
+
CREATE TABLE projects (
|
|
129
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
130
|
+
canonical_id TEXT UNIQUE NOT NULL, -- normalised git remote URL
|
|
131
|
+
name TEXT NOT NULL, -- human-readable
|
|
132
|
+
local_path TEXT, -- path on THIS machine
|
|
133
|
+
remote_url TEXT, -- original git remote URL
|
|
134
|
+
first_seen_epoch INTEGER NOT NULL,
|
|
135
|
+
last_active_epoch INTEGER NOT NULL
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
-- Core observations table
|
|
139
|
+
CREATE TABLE observations (
|
|
140
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
141
|
+
session_id TEXT,
|
|
142
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
143
|
+
type TEXT NOT NULL, -- bugfix | discovery | decision | pattern | change | feature | refactor
|
|
144
|
+
title TEXT NOT NULL,
|
|
145
|
+
narrative TEXT, -- Detailed description
|
|
146
|
+
facts TEXT, -- JSON array of factual assertions
|
|
147
|
+
concepts TEXT, -- JSON array: how-it-works, why-it-exists, etc.
|
|
148
|
+
files_read TEXT, -- JSON array of RELATIVE file paths
|
|
149
|
+
files_modified TEXT, -- JSON array of RELATIVE file paths
|
|
150
|
+
quality REAL DEFAULT 0.5, -- 0.0-1.0, influences search rank and lifecycle
|
|
151
|
+
lifecycle TEXT DEFAULT 'active', -- active | aging | archived | purged | pinned
|
|
152
|
+
sensitivity TEXT DEFAULT 'shared', -- shared | personal | secret
|
|
153
|
+
user_id TEXT NOT NULL,
|
|
154
|
+
device_id TEXT NOT NULL,
|
|
155
|
+
agent TEXT DEFAULT 'claude-code',
|
|
156
|
+
created_at TEXT NOT NULL,
|
|
157
|
+
created_at_epoch INTEGER NOT NULL,
|
|
158
|
+
archived_at_epoch INTEGER, -- when moved to archived state
|
|
159
|
+
compacted_into INTEGER REFERENCES observations(id) -- if summarised into a digest
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
-- Session tracking
|
|
163
|
+
CREATE TABLE sessions (
|
|
164
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
165
|
+
session_id TEXT UNIQUE NOT NULL,
|
|
166
|
+
project_id INTEGER REFERENCES projects(id),
|
|
167
|
+
user_id TEXT NOT NULL,
|
|
168
|
+
device_id TEXT NOT NULL,
|
|
169
|
+
agent TEXT DEFAULT 'claude-code',
|
|
170
|
+
status TEXT DEFAULT 'active', -- active | completed
|
|
171
|
+
observation_count INTEGER DEFAULT 0, -- running count for this session
|
|
172
|
+
started_at_epoch INTEGER,
|
|
173
|
+
completed_at_epoch INTEGER
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
-- Session summaries (generated on Stop hook)
|
|
177
|
+
CREATE TABLE session_summaries (
|
|
178
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
179
|
+
session_id TEXT UNIQUE NOT NULL,
|
|
180
|
+
project_id INTEGER REFERENCES projects(id),
|
|
181
|
+
user_id TEXT NOT NULL,
|
|
182
|
+
request TEXT, -- What was asked
|
|
183
|
+
investigated TEXT, -- What was explored
|
|
184
|
+
learned TEXT, -- Key learnings
|
|
185
|
+
completed TEXT, -- What was delivered
|
|
186
|
+
next_steps TEXT, -- Follow-up items
|
|
187
|
+
created_at_epoch INTEGER
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
-- Sync outbox (offline-first queue)
|
|
191
|
+
CREATE TABLE sync_outbox (
|
|
192
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
193
|
+
record_type TEXT NOT NULL, -- observation | summary
|
|
194
|
+
record_id INTEGER NOT NULL,
|
|
195
|
+
status TEXT DEFAULT 'pending', -- pending | syncing | synced | failed
|
|
196
|
+
retry_count INTEGER DEFAULT 0,
|
|
197
|
+
max_retries INTEGER DEFAULT 10,
|
|
198
|
+
last_error TEXT,
|
|
199
|
+
created_at_epoch INTEGER NOT NULL,
|
|
200
|
+
synced_at_epoch INTEGER,
|
|
201
|
+
next_retry_epoch INTEGER
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
-- Sync high-water mark
|
|
205
|
+
CREATE TABLE sync_state (
|
|
206
|
+
key TEXT PRIMARY KEY,
|
|
207
|
+
value TEXT NOT NULL
|
|
208
|
+
);
|
|
209
|
+
-- Keys: "last_synced_epoch", "last_backfill_epoch"
|
|
210
|
+
|
|
211
|
+
-- FTS5 for local offline search (only active + aging observations)
|
|
212
|
+
CREATE VIRTUAL TABLE observations_fts USING fts5(
|
|
213
|
+
title, narrative, facts, concepts,
|
|
214
|
+
content=observations,
|
|
215
|
+
content_rowid=id
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
-- Indexes
|
|
219
|
+
CREATE INDEX idx_observations_project ON observations(project_id);
|
|
220
|
+
CREATE INDEX idx_observations_type ON observations(type);
|
|
221
|
+
CREATE INDEX idx_observations_created ON observations(created_at_epoch);
|
|
222
|
+
CREATE INDEX idx_observations_session ON observations(session_id);
|
|
223
|
+
CREATE INDEX idx_observations_lifecycle ON observations(lifecycle);
|
|
224
|
+
CREATE INDEX idx_observations_quality ON observations(quality);
|
|
225
|
+
CREATE INDEX idx_projects_canonical ON projects(canonical_id);
|
|
226
|
+
CREATE INDEX idx_outbox_status ON sync_outbox(status, next_retry_epoch);
|
|
227
|
+
CREATE INDEX idx_outbox_record ON sync_outbox(record_type, record_id);
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### File Paths: Always Relative
|
|
231
|
+
|
|
232
|
+
File paths in observations are stored **relative to the project root**, never absolute. This ensures paths match across machines where the project lives at different locations.
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
Absolute (on this machine): /Users/david/code/aimy-agent/models/interview.py
|
|
236
|
+
Project root: /Users/david/code/aimy-agent/
|
|
237
|
+
Stored in observation: models/interview.py
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The plugin resolves relative paths at capture time using the project's `local_path`.
|
|
241
|
+
|
|
242
|
+
### Candengo Vector Document Mapping
|
|
243
|
+
|
|
244
|
+
Each observation maps to a single Candengo Vector document:
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"site_id": "unimpossible",
|
|
249
|
+
"namespace": "dev-memory",
|
|
250
|
+
"source_type": "discovery",
|
|
251
|
+
"source_id": "david-desktop1-obs-1234",
|
|
252
|
+
"content": "## Database type mismatch causing 500 errors\n\nThe interview endpoint was returning 500 errors because project_id was defined as UUID in the model but the database column was TEXT. Fixed by changing the SQLAlchemy column type to String.\n\nFacts:\n- project_id column type mismatch between model (UUID) and database (TEXT)\n- Fix: change Column(UUID) to Column(String) in Interview model\n- Affects: models/interview.py",
|
|
253
|
+
"metadata": {
|
|
254
|
+
"project_canonical": "github.com/unimpossible/aimy-agent",
|
|
255
|
+
"project_name": "aimy-agent",
|
|
256
|
+
"user_id": "david",
|
|
257
|
+
"device_id": "desktop-1",
|
|
258
|
+
"agent": "claude-code",
|
|
259
|
+
"title": "Fixed project_id column type mismatch in Interview model",
|
|
260
|
+
"type": "bugfix",
|
|
261
|
+
"quality": 0.7,
|
|
262
|
+
"concepts": ["problem-solution", "gotcha"],
|
|
263
|
+
"files_modified": ["models/interview.py"],
|
|
264
|
+
"session_id": "abc-123",
|
|
265
|
+
"created_at_epoch": 1740700000,
|
|
266
|
+
"local_id": 1234
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Content composition**: Concatenate `title + narrative + facts` into a single `content` field. Candengo Vector auto-chunks at 400 tokens with 50-token overlap. Most observations fit in a single chunk.
|
|
272
|
+
|
|
273
|
+
**Source ID format**: `"{user_id}-{device_id}-obs-{local_sqlite_id}"` — unique across users AND devices. The old format `"{user_id}-obs-{id}"` could collide because two devices both start local IDs at 1.
|
|
274
|
+
|
|
275
|
+
**Project matching**: Candengo Vector metadata includes `project_canonical` (normalised git remote URL). When searching, the plugin sends its current project's canonical ID as a metadata filter. This ensures "aimy-agent on laptop" and "aimy-agent on desktop" match to the same project — because they share the same git remote.
|
|
276
|
+
|
|
277
|
+
### Deduplication
|
|
278
|
+
|
|
279
|
+
Before saving a new observation, check for near-duplicates:
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
1. Query local SQLite: recent observations (last 24h) for same project
|
|
283
|
+
2. Simple heuristic: title similarity > 0.8 (Jaccard on word tokens)
|
|
284
|
+
3. If duplicate found: merge new facts into existing observation, don't create new row
|
|
285
|
+
4. If no duplicate: save as new observation
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Full semantic deduplication (via Candengo Vector similarity) runs during the compaction job, not on the hot path. Keeps capture fast.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 3. MCP Tool Interface
|
|
293
|
+
|
|
294
|
+
### Tool: `search`
|
|
295
|
+
Find relevant observations from memory.
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
{
|
|
299
|
+
name: "search",
|
|
300
|
+
description: "Search memory for relevant observations, discoveries, and decisions",
|
|
301
|
+
inputSchema: {
|
|
302
|
+
query: string, // Natural language search query
|
|
303
|
+
project?: string, // Filter to specific project (auto-detected from cwd if omitted)
|
|
304
|
+
type?: string, // Filter by observation type
|
|
305
|
+
limit?: number, // Max results (default: 10)
|
|
306
|
+
scope?: "personal" | "team" | "all" // Search scope (default: "all")
|
|
307
|
+
include_archived?: boolean // Include archived observations (default: false)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Returns compact index with IDs (~50-100 tokens per result):
|
|
313
|
+
```
|
|
314
|
+
| ID | Time | Type | Q | Title | Project | By |
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The `Q` column shows quality score as a visual indicator (●●●○○ = 0.6). Results are ranked by `(relevance × quality × lifecycle_weight)`.
|
|
318
|
+
|
|
319
|
+
**Default behaviour**: Searches current project only. Pass `project: "*"` to search across all projects.
|
|
320
|
+
|
|
321
|
+
### Tool: `timeline`
|
|
322
|
+
Get chronological context around a specific observation.
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
{
|
|
326
|
+
name: "timeline",
|
|
327
|
+
inputSchema: {
|
|
328
|
+
anchor: number, // Observation ID to centre on
|
|
329
|
+
depth_before?: number, // Observations before (default: 3)
|
|
330
|
+
depth_after?: number, // Observations after (default: 3)
|
|
331
|
+
project?: string // Auto-detected if omitted
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Tool: `get_observations`
|
|
337
|
+
Fetch full details for specific observation IDs.
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
{
|
|
341
|
+
name: "get_observations",
|
|
342
|
+
inputSchema: {
|
|
343
|
+
ids: number[]
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Returns full observation details (~500-1000 tokens per result).
|
|
349
|
+
|
|
350
|
+
### Tool: `save_observation`
|
|
351
|
+
Manually save an observation (most are captured automatically via hooks).
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
{
|
|
355
|
+
name: "save_observation",
|
|
356
|
+
inputSchema: {
|
|
357
|
+
text: string, // Observation content (required)
|
|
358
|
+
title?: string, // Brief title (auto-generated if omitted)
|
|
359
|
+
type?: string, // Observation type (auto-classified if omitted)
|
|
360
|
+
project?: string, // Project (auto-detected from cwd if omitted)
|
|
361
|
+
pin?: boolean // Pin to prevent aging (default: false)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Tool: `pin_observation`
|
|
367
|
+
Prevent an observation from aging out. Use for architectural decisions, critical gotchas, and other knowledge that stays relevant indefinitely.
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
{
|
|
371
|
+
name: "pin_observation",
|
|
372
|
+
inputSchema: {
|
|
373
|
+
id: number,
|
|
374
|
+
pinned: boolean // true to pin, false to unpin
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## 4. Sync Engine
|
|
382
|
+
|
|
383
|
+
### Sync States
|
|
384
|
+
|
|
385
|
+
```
|
|
386
|
+
┌──────────┐
|
|
387
|
+
│ pending │ ← observation saved to SQLite
|
|
388
|
+
└─────┬─────┘
|
|
389
|
+
│ sync attempt
|
|
390
|
+
┌─────▼─────┐
|
|
391
|
+
│ syncing │ ← HTTP request in flight
|
|
392
|
+
└─────┬─────┘
|
|
393
|
+
╱ ╲
|
|
394
|
+
success failure
|
|
395
|
+
╱ ╲
|
|
396
|
+
┌───────▼┐ ┌────▼────┐
|
|
397
|
+
│ synced │ │ failed │ ← retry_count++
|
|
398
|
+
└────────┘ └────┬────┘
|
|
399
|
+
│ next_retry_epoch reached
|
|
400
|
+
│ (if retry_count < max_retries)
|
|
401
|
+
┌─────▼─────┐
|
|
402
|
+
│ pending │ ← back in queue
|
|
403
|
+
└───────────┘
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Retry Schedule (Exponential Backoff)
|
|
407
|
+
|
|
408
|
+
| Retry # | Delay |
|
|
409
|
+
|---|---|
|
|
410
|
+
| 1 | 30 seconds |
|
|
411
|
+
| 2 | 1 minute |
|
|
412
|
+
| 3 | 2 minutes |
|
|
413
|
+
| 4 | 5 minutes |
|
|
414
|
+
| 5+ | 5 minutes (cap) |
|
|
415
|
+
|
|
416
|
+
### Sync Triggers
|
|
417
|
+
|
|
418
|
+
1. **Immediate**: On observation save → fire-and-forget push
|
|
419
|
+
2. **Timer**: Every 30 seconds → flush pending outbox items (batch of 50)
|
|
420
|
+
3. **Startup**: On boot → check high-water mark, sync anything newer
|
|
421
|
+
4. **Manual**: `engrm sync` CLI command → force full sync
|
|
422
|
+
|
|
423
|
+
### Backfill Algorithm (High-Water Mark)
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
1. Read last_synced_epoch from sync_state table
|
|
427
|
+
2. SELECT * FROM observations
|
|
428
|
+
WHERE created_at_epoch > last_synced_epoch
|
|
429
|
+
AND sensitivity != 'secret'
|
|
430
|
+
3. Batch push to Candengo Vector via POST /v1/ingest/batch
|
|
431
|
+
4. Update last_synced_epoch to max(created_at_epoch) of pushed items
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
No need to query remote for existing IDs. Simple, efficient, scales to any count.
|
|
435
|
+
|
|
436
|
+
### Conflict Resolution
|
|
437
|
+
|
|
438
|
+
**Strategy: Source ID namespacing, no conflicts possible**
|
|
439
|
+
|
|
440
|
+
- Each observation has a unique `source_id`: `"{user_id}-{device_id}-obs-{local_id}"`
|
|
441
|
+
- Candengo Vector upserts by `source_id` — re-syncing is idempotent
|
|
442
|
+
- No two users can overwrite each other's observations
|
|
443
|
+
- No two devices can collide (both start local autoincrement at 1, but device_id distinguishes them)
|
|
444
|
+
|
|
445
|
+
Search results show attribution (`user_id/device_id`) so users know who captured what and where.
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## 5. Lifecycle Management
|
|
450
|
+
|
|
451
|
+
### Aging Job
|
|
452
|
+
|
|
453
|
+
Runs once per day (on plugin startup if >24h since last run):
|
|
454
|
+
|
|
455
|
+
```
|
|
456
|
+
1. UPDATE observations SET lifecycle = 'aging'
|
|
457
|
+
WHERE lifecycle = 'active'
|
|
458
|
+
AND created_at_epoch < now() - 30 days
|
|
459
|
+
|
|
460
|
+
2. For observations moving to 'aging':
|
|
461
|
+
- No immediate action (still in FTS5 and Candengo Vector)
|
|
462
|
+
- Search scoring applies 0.7x weight
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Archival + Compaction Job
|
|
466
|
+
|
|
467
|
+
Runs once per week:
|
|
468
|
+
|
|
469
|
+
```
|
|
470
|
+
1. Find observations WHERE lifecycle = 'aging'
|
|
471
|
+
AND created_at_epoch < now() - 90 days
|
|
472
|
+
|
|
473
|
+
2. Group by (project_id, session_id)
|
|
474
|
+
|
|
475
|
+
3. For each group:
|
|
476
|
+
a. Generate a "digest" observation:
|
|
477
|
+
- Summarise: what was the session about, key findings, outcomes
|
|
478
|
+
- Type: "digest"
|
|
479
|
+
- Quality: max(quality) of source observations
|
|
480
|
+
- Lifecycle: "pinned" (digests don't age out)
|
|
481
|
+
b. Mark source observations:
|
|
482
|
+
- lifecycle = 'archived'
|
|
483
|
+
- compacted_into = digest.id
|
|
484
|
+
- archived_at_epoch = now()
|
|
485
|
+
|
|
486
|
+
4. Remove archived observations from FTS5 index
|
|
487
|
+
|
|
488
|
+
5. Queue removal from Candengo Vector:
|
|
489
|
+
- DELETE source_ids from Candengo Vector
|
|
490
|
+
- Ingest the new digest observation
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
This means a 3-month-old debugging session with 25 observations becomes a single digest observation. The detail is preserved in local SQLite (for forensic use) but doesn't pollute search results or count toward vector storage quota.
|
|
494
|
+
|
|
495
|
+
### Purge Job
|
|
496
|
+
|
|
497
|
+
Runs monthly. Permanently deletes archived observations older than 12 months:
|
|
498
|
+
|
|
499
|
+
```
|
|
500
|
+
DELETE FROM observations
|
|
501
|
+
WHERE lifecycle = 'archived'
|
|
502
|
+
AND archived_at_epoch < now() - 12 months
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Digests (lifecycle = 'pinned') are never purged. Pinned individual observations are never purged.
|
|
506
|
+
|
|
507
|
+
### Quota Calculation
|
|
508
|
+
|
|
509
|
+
For free tier enforcement (10K observation limit):
|
|
510
|
+
|
|
511
|
+
```sql
|
|
512
|
+
SELECT COUNT(*) FROM observations
|
|
513
|
+
WHERE lifecycle IN ('active', 'aging')
|
|
514
|
+
AND sensitivity != 'secret'
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Only active + aging observations that are synced to Candengo Vector count toward the quota. Archived and local-only observations are free. This means compaction directly frees quota — a user generating 500 observations/month who stays under 10K active+aging can use the free tier indefinitely.
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
## 6. Secret Scrubbing Pipeline
|
|
522
|
+
|
|
523
|
+
### Pre-Storage Scrubbing
|
|
524
|
+
|
|
525
|
+
Before any observation is saved (even to local SQLite), content is scrubbed:
|
|
526
|
+
|
|
527
|
+
```
|
|
528
|
+
Input text → Pattern matching → Replacement → Scrubbed text
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Default Patterns
|
|
532
|
+
|
|
533
|
+
| Pattern | Replacement | Catches |
|
|
534
|
+
|---|---|---|
|
|
535
|
+
| `sk-[a-zA-Z0-9]{20,}` | `[REDACTED_API_KEY]` | OpenAI keys |
|
|
536
|
+
| `Bearer [a-zA-Z0-9\-._~+/]+=*` | `[REDACTED_BEARER]` | Auth headers |
|
|
537
|
+
| `password[=:]\s*\S+` | `password=[REDACTED]` | Passwords in config |
|
|
538
|
+
| `postgresql://[^\s]+` | `[REDACTED_DB_URL]` | Postgres connection strings |
|
|
539
|
+
| `mongodb://[^\s]+` | `[REDACTED_DB_URL]` | Mongo connection strings |
|
|
540
|
+
| `mysql://[^\s]+` | `[REDACTED_DB_URL]` | MySQL connection strings |
|
|
541
|
+
| `AKIA[A-Z0-9]{16}` | `[REDACTED_AWS_KEY]` | AWS access keys |
|
|
542
|
+
| `ghp_[a-zA-Z0-9]{36}` | `[REDACTED_GH_TOKEN]` | GitHub tokens |
|
|
543
|
+
| `cvk_[a-f0-9]{64}` | `[REDACTED_CANDENGO_KEY]` | Candengo API keys |
|
|
544
|
+
| Custom patterns from config | User-defined | Organisation-specific |
|
|
545
|
+
|
|
546
|
+
### Sensitivity Levels
|
|
547
|
+
|
|
548
|
+
| Level | Behaviour |
|
|
549
|
+
|---|---|
|
|
550
|
+
| `shared` (default) | Scrubbed, stored locally, synced to Candengo Vector |
|
|
551
|
+
| `personal` | Scrubbed, stored locally, synced but only visible to same user |
|
|
552
|
+
| `secret` | Scrubbed, stored locally only, **never synced** |
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## 7. Configuration & Authentication
|
|
557
|
+
|
|
558
|
+
> **Full auth design**: See `AUTH-DESIGN.md` for the complete authentication specification including all flows, server-side requirements, and implementation timeline.
|
|
559
|
+
|
|
560
|
+
### Settings File: `~/.engrm/settings.json`
|
|
561
|
+
|
|
562
|
+
All configuration lives in a single directory: `~/.engrm/`.
|
|
563
|
+
|
|
564
|
+
```json
|
|
565
|
+
{
|
|
566
|
+
"candengo_url": "https://www.candengo.com",
|
|
567
|
+
"candengo_api_key": "cvk_...",
|
|
568
|
+
"site_id": "unimpossible",
|
|
569
|
+
"namespace": "dev-memory",
|
|
570
|
+
"user_id": "david",
|
|
571
|
+
"user_email": "david@example.com",
|
|
572
|
+
"device_id": "macbook-a1b2c3d4",
|
|
573
|
+
"teams": [
|
|
574
|
+
{ "id": "team_abc123", "name": "Unimpossible", "namespace": "dev-memory" }
|
|
575
|
+
],
|
|
576
|
+
"sync": {
|
|
577
|
+
"enabled": true,
|
|
578
|
+
"interval_seconds": 30,
|
|
579
|
+
"batch_size": 50
|
|
580
|
+
},
|
|
581
|
+
"search": {
|
|
582
|
+
"default_limit": 10,
|
|
583
|
+
"local_boost": 1.2,
|
|
584
|
+
"scope": "all"
|
|
585
|
+
},
|
|
586
|
+
"scrubbing": {
|
|
587
|
+
"enabled": true,
|
|
588
|
+
"custom_patterns": [],
|
|
589
|
+
"default_sensitivity": "shared"
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
**Changes from prior version**: Added `user_email` and `teams[]` array (supports multi-team membership).
|
|
595
|
+
|
|
596
|
+
### Credential Types
|
|
597
|
+
|
|
598
|
+
| Prefix | Type | Lifetime | Purpose |
|
|
599
|
+
|--------|------|----------|---------|
|
|
600
|
+
| `cvk_` | API key | Permanent (revocable) | The ONE credential for all sync API calls |
|
|
601
|
+
| `cmt_` | Provisioning token | 1 hour, single-use | Web signup → exchange for `cvk_` key |
|
|
602
|
+
|
|
603
|
+
The `cvk_` API key is the single credential type for sync operations. Multiple authentication flows exist to **obtain** this key conveniently, but the credential itself is always the same.
|
|
604
|
+
|
|
605
|
+
**CI/CD**: The `ENGRM_TOKEN` environment variable takes precedence over settings.json, allowing pipelines to authenticate without config files.
|
|
606
|
+
|
|
607
|
+
### Authentication Flows
|
|
608
|
+
|
|
609
|
+
Four flows to obtain a `cvk_` API key, covering all environments:
|
|
610
|
+
|
|
611
|
+
| Flow | Command | Use Case |
|
|
612
|
+
|------|---------|----------|
|
|
613
|
+
| **Browser OAuth** | `engrm init` | Desktop developers (default) |
|
|
614
|
+
| **Device code** | `engrm init --no-browser` | SSH, headless, WSL (RFC 8628) |
|
|
615
|
+
| **Provisioning token** | `engrm init --token=cmt_...` | Web signup copy-paste |
|
|
616
|
+
| **Manual** | `engrm init --manual` | Air-gapped, self-hosted |
|
|
617
|
+
|
|
618
|
+
All flows write the same `cvk_` API key to `~/.engrm/settings.json`.
|
|
619
|
+
|
|
620
|
+
#### Browser OAuth Flow (Default)
|
|
621
|
+
|
|
622
|
+
```
|
|
623
|
+
1. User runs: engrm init
|
|
624
|
+
2. CLI opens browser → candengo.com/connect/mem
|
|
625
|
+
3. User logs in → clicks "Authorize"
|
|
626
|
+
4. Redirect to localhost callback with auth code
|
|
627
|
+
5. CLI exchanges code → receives cvk_ API key
|
|
628
|
+
6. Writes settings.json + registers MCP server
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
#### Device Code Flow (Headless)
|
|
632
|
+
|
|
633
|
+
Auto-detected when no browser available, or via `--no-browser`:
|
|
634
|
+
|
|
635
|
+
```
|
|
636
|
+
1. CLI requests device code from server
|
|
637
|
+
2. Prints: "Open https://candengo.com/connect/mem/device — Enter code: XXXX-YYYY"
|
|
638
|
+
3. User authorises on any device with a browser
|
|
639
|
+
4. CLI polls until authorised → receives cvk_ API key
|
|
640
|
+
5. Writes settings.json
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
#### Provisioning Token Flow (Web Signup)
|
|
644
|
+
|
|
645
|
+
```
|
|
646
|
+
1. User signs up at engrm.dev
|
|
647
|
+
2. Page shows: npx engrm init --token=cmt_abc123...
|
|
648
|
+
3. CLI exchanges cmt_ token → receives cvk_ API key
|
|
649
|
+
4. Writes settings.json + registers MCP server
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### Team Provisioning
|
|
653
|
+
|
|
654
|
+
Teams are **explicitly** created and joined — not auto-provisioned.
|
|
655
|
+
|
|
656
|
+
- **Personal namespace**: auto-provisioned on first auth (any flow)
|
|
657
|
+
- **Team creation**: admin at `engrm.dev/team`
|
|
658
|
+
- **Team join**: invite link `engrm.dev/join/{code}` or `engrm team join --code=INVITE_CODE`
|
|
659
|
+
- **Multi-team**: `teams[]` array supports belonging to multiple teams simultaneously
|
|
660
|
+
|
|
661
|
+
### Token Revocation
|
|
662
|
+
|
|
663
|
+
- `engrm auth revoke` — revoke current key
|
|
664
|
+
- `engrm auth rotate` — atomic: create new key, update settings, revoke old
|
|
665
|
+
- Web dashboard: `engrm.dev/dashboard` — manage all keys
|
|
666
|
+
- Server stores key hashes only (SHA-256), with `key_prefix` for identification
|
|
667
|
+
|
|
668
|
+
### What `engrm init` Does
|
|
669
|
+
|
|
670
|
+
```
|
|
671
|
+
1. Authenticate via one of the four flows above
|
|
672
|
+
2. Exchange credentials → get cvk_ API key + account info
|
|
673
|
+
3. Create ~/.engrm/ directory
|
|
674
|
+
4. Write settings.json with credentials
|
|
675
|
+
5. Generate device_id (hostname + random suffix)
|
|
676
|
+
6. Create local SQLite database
|
|
677
|
+
7. Register MCP server in Claude Code:
|
|
678
|
+
- Write to ~/.claude/mcp.json (or project .mcp.json)
|
|
679
|
+
8. Register hooks in Claude Code:
|
|
680
|
+
- Write to ~/.claude/hooks.json (or merge with existing)
|
|
681
|
+
9. Test connection to Candengo Vector (if online)
|
|
682
|
+
10. Print success message with next steps
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### Self-Hosted Provisioning
|
|
686
|
+
|
|
687
|
+
For self-hosted deployments:
|
|
688
|
+
|
|
689
|
+
```bash
|
|
690
|
+
npx engrm init --url=https://vector.internal.company.com --token=cmt_...
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
Or manual config for air-gapped environments:
|
|
694
|
+
|
|
695
|
+
```bash
|
|
696
|
+
npx engrm init --manual
|
|
697
|
+
# Prompts for: endpoint, api_key, site_id, namespace, user_id
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### Device ID
|
|
701
|
+
|
|
702
|
+
Auto-generated on first run using `hostname + random suffix`. Stored in settings. Used to tag all observations so you know which machine they came from.
|
|
703
|
+
|
|
704
|
+
---
|
|
705
|
+
|
|
706
|
+
## 8. Claude Code Integration
|
|
707
|
+
|
|
708
|
+
### MCP Server Registration
|
|
709
|
+
|
|
710
|
+
`.mcp.json` in the project or user config:
|
|
711
|
+
```json
|
|
712
|
+
{
|
|
713
|
+
"mcpServers": {
|
|
714
|
+
"engrm": {
|
|
715
|
+
"type": "stdio",
|
|
716
|
+
"command": "bun",
|
|
717
|
+
"args": ["run", "/path/to/engrm/src/server.ts"]
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### Claude Code Hooks
|
|
724
|
+
|
|
725
|
+
Hooks are configured in `~/.claude/settings.json` (or `.claude/settings.json` for project scope) under the `"hooks"` key:
|
|
726
|
+
|
|
727
|
+
```json
|
|
728
|
+
{
|
|
729
|
+
"hooks": {
|
|
730
|
+
"PostToolUse": [
|
|
731
|
+
{
|
|
732
|
+
"matcher": "Edit|Write|Bash",
|
|
733
|
+
"hooks": [
|
|
734
|
+
{
|
|
735
|
+
"type": "command",
|
|
736
|
+
"command": "bun run /path/to/engrm/hooks/post-tool-use.ts"
|
|
737
|
+
}
|
|
738
|
+
]
|
|
739
|
+
}
|
|
740
|
+
],
|
|
741
|
+
"Stop": [
|
|
742
|
+
{
|
|
743
|
+
"hooks": [
|
|
744
|
+
{
|
|
745
|
+
"type": "command",
|
|
746
|
+
"command": "bun run /path/to/engrm/hooks/stop.ts"
|
|
747
|
+
}
|
|
748
|
+
]
|
|
749
|
+
}
|
|
750
|
+
]
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### Hook Data Flow
|
|
756
|
+
|
|
757
|
+
**PostToolUse** receives JSON on stdin:
|
|
758
|
+
```json
|
|
759
|
+
{
|
|
760
|
+
"session_id": "abc123",
|
|
761
|
+
"hook_event_name": "PostToolUse",
|
|
762
|
+
"tool_name": "Edit",
|
|
763
|
+
"tool_input": { "file_path": "...", "old_string": "...", "new_string": "..." },
|
|
764
|
+
"tool_response": "Successfully edited /path/to/file.txt",
|
|
765
|
+
"cwd": "/path/to/project"
|
|
766
|
+
}
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
The hook runs the observation extractor which decides whether this tool use is worth capturing. Signal (edits, error→fix, dependency changes) = yes. Noise (reads, navigation, git status) = skip.
|
|
770
|
+
|
|
771
|
+
**Stop** receives JSON on stdin:
|
|
772
|
+
```json
|
|
773
|
+
{
|
|
774
|
+
"session_id": "abc123",
|
|
775
|
+
"hook_event_name": "Stop",
|
|
776
|
+
"stop_hook_active": false,
|
|
777
|
+
"last_assistant_message": "...",
|
|
778
|
+
"cwd": "/path/to/project"
|
|
779
|
+
}
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
Stop hook actions:
|
|
783
|
+
1. Check `stop_hook_active` to prevent infinite loops
|
|
784
|
+
2. Mark session as completed in SQLite
|
|
785
|
+
3. Session summary generation (deferred to Phase 3)
|
|
786
|
+
4. Exit 0 to allow Claude to stop
|
|
787
|
+
|
|
788
|
+
---
|
|
789
|
+
|
|
790
|
+
## 9. Search Pipeline
|
|
791
|
+
|
|
792
|
+
### Hybrid Search Architecture
|
|
793
|
+
|
|
794
|
+
```
|
|
795
|
+
Query: "how to handle SSE streaming errors"
|
|
796
|
+
+ project_canonical: "github.com/unimpossible/aimy-agent" (auto-detected)
|
|
797
|
+
│
|
|
798
|
+
├──→ Local SQLite FTS5 (always, instant)
|
|
799
|
+
│ → BM25 keyword matching
|
|
800
|
+
│ → Filter: project_id + lifecycle IN ('active', 'aging', 'pinned')
|
|
801
|
+
│ → Returns top 20 candidates with scores
|
|
802
|
+
│
|
|
803
|
+
└──→ Candengo Vector /v1/search (if online)
|
|
804
|
+
→ metadata_filter: project_canonical = "github.com/unimpossible/aimy-agent"
|
|
805
|
+
→ BGE-M3 hybrid dense+sparse
|
|
806
|
+
→ Cross-encoder reranking
|
|
807
|
+
→ Returns top 20 candidates with scores
|
|
808
|
+
│
|
|
809
|
+
▼
|
|
810
|
+
Result Merger
|
|
811
|
+
→ Deduplicate by source_id
|
|
812
|
+
→ Normalise scores to 0-1
|
|
813
|
+
→ Weighted combination:
|
|
814
|
+
- Candengo score × 0.6 (semantic quality)
|
|
815
|
+
- Local FTS score × 0.15 (keyword precision)
|
|
816
|
+
- Quality score × 0.15 (observation quality)
|
|
817
|
+
- Recency bonus × 0.1 (newer slightly preferred)
|
|
818
|
+
→ Lifecycle weight: active=1.0, aging=0.7, pinned=1.0
|
|
819
|
+
→ Device boost: current device × 1.1
|
|
820
|
+
→ Return top N results
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
**Project scoping**: Search is always scoped to the current project by default. The canonical project ID (from git remote) is sent as a metadata filter to Candengo Vector and as a WHERE clause to local SQLite. This means a search in the `aimy-agent` project on your laptop matches observations from `aimy-agent` on your desktop — because both resolve to `github.com/unimpossible/aimy-agent`.
|
|
824
|
+
|
|
825
|
+
**Cross-project search**: Pass `project: "*"` to search across all projects. Useful for finding patterns that apply across codebases ("how did we handle CORS last time in any project?").
|
|
826
|
+
|
|
827
|
+
### Result Format (Compact Index)
|
|
828
|
+
|
|
829
|
+
```markdown
|
|
830
|
+
### Memory Search Results (aimy-agent)
|
|
831
|
+
|
|
832
|
+
| ID | Time | T | Q | Title | By |
|
|
833
|
+
|----|------|---|---|-------|----|
|
|
834
|
+
| #574 | 4:26 PM | D | ●●●○○ | Conversation lookup allows multiple active per user | david/laptop |
|
|
835
|
+
| #573 | 4:26 PM | B | ●●●●○ | Chat stream crashes due to duplicate conversations | david/desktop |
|
|
836
|
+
| #201 | Feb 12 | P | ●●●●● | SSE streaming must wrap generator in try/finally | sarah/laptop |
|
|
837
|
+
| 📌 #89 | Jan 5 | DC | ●●●●● | Use FastAPI BackgroundTasks for non-blocking writes | david/laptop |
|
|
838
|
+
|
|
839
|
+
Access full details: get_observations([574, 573, 201, 89])
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
Legend: B=bugfix, D=discovery, F=feature, R=refactor, C=change, P=pattern, DC=decision, DG=digest
|
|
843
|
+
📌 = pinned (won't age out)
|