Skip to content

Commit 0639167

Browse files
Merge pull request #155 from Entrolution/feat/hybrid-default-entity-extraction
Hybrid retrieval default, entity extraction, temporal misrouting fix
2 parents 819385c + 4b3dee5 commit 0639167

69 files changed

Lines changed: 2119 additions & 504 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.10.1] - 2026-03-13
9+
10+
### Added
11+
12+
- **Entity extraction** (`src/ingest/entity-extractor.ts`): Deterministic regex-based extraction of people (@mentions, emails, "X said" patterns), channels (#channels), meetings (standup, retro, 1:1), and URLs from chunk content. Skips code blocks and `[Thinking]` blocks to reduce noise.
13+
- **Entity store** (`src/storage/entity-store.ts`): CRUD layer for entities, aliases, and chunk mentions. Supports alias resolution, re-ingestion safety (INSERT OR IGNORE), and per-entity chunk lookup capped at 100 most recent.
14+
- **Entity-aware retrieval**: Entity mentions in queries are matched against stored entities and injected as an RRF source (weight 1.5) in both keyword and hybrid search paths. Project-scoped — gracefully skips when no project filter is provided.
15+
- **Entity tables** (migration v16): Three new tables (`entities`, `entity_aliases`, `entity_mentions`) with cascade deletes and appropriate indexes.
16+
- **Entity count in stats**: The `stats` MCP tool now reports entity count.
17+
18+
### Changed
19+
20+
- **Hybrid retrieval default**: `retrieval.primary` changed from `'keyword'` to `'hybrid'`. Vector search is now always active at ~14ms cost (local jina-small), which covers narrative/thematic projects without per-project configuration. Backward compatible — `retrieval.primary: 'keyword'` in config still works.
21+
- **Temporal misrouting fix**: Updated `search` and `recall` tool descriptions to redirect recent/latest session queries to `reconstruct`. Updated `reconstruct` description to explicitly claim temporal queries.
22+
- **MCP tool descriptions**: `search` now mentions hybrid retrieval and entity boosting; `recall` and `reconstruct` include temporal routing guidance.
23+
24+
### Fixed
25+
26+
- **MCP integration test timeout**: Increased `beforeAll` hook timeout from 10s to 30s to accommodate heavy module imports (ONNX runtime, tree-sitter, LanceDB).
27+
28+
### Tests
29+
30+
- 2378 tests passing.
31+
832
## [0.10.0] - 2026-03-12
933

1034
### Added

docs/guides/how-it-works.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,19 @@ The pipeline automatically selects the index-based or chunk-based search path at
208208

209209
Both paths converge at cluster expansion, which always operates on chunk IDs and their chunk-level cluster assignments.
210210

211+
### Entity Boosting
212+
213+
During ingestion, Causantic extracts named entities from chunk content using deterministic regex patterns (no LLM required):
214+
215+
- **People**: `@mentions`, email addresses, "X said"/"with X" patterns
216+
- **Channels**: `#channel` references
217+
- **Meetings**: Keywords like standup, retro, 1:1, sync
218+
- **URLs**: Full URL patterns
219+
220+
Entities are resolved to canonical forms with alias tracking (e.g., `@joel` and `Joel` map to the same entity). At query time, if the search query contains recognisable entity references, matching chunks are injected as an additional RRF source with a 1.5x boost weight. This means searching for "@joel" surfaces all chunks mentioning Joel alongside semantically relevant results, without requiring exact keyword matches in every chunk.
221+
222+
Entity extraction skips code blocks and `[Thinking]` blocks to avoid false positives from speculative content.
223+
211224
### Recall/Predict (episodic)
212225

213226
The `recall` and `predict` tools reconstruct narrative chains:

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Deep dives into specific topics:
2828
Technical reference documentation:
2929

3030
- [CLI Commands](reference/cli-commands.md) - Command-line interface reference
31-
- [MCP Tools](reference/mcp-tools.md) - MCP server tool documentation (9 tools)
31+
- [MCP Tools](reference/mcp-tools.md) - MCP server tool documentation (10 tools)
3232
- [Configuration Reference](reference/configuration.md) - All configuration options
3333
- [Storage API](reference/storage-api.md) - Storage layer internals
3434
- [Skills Reference](reference/skills.md) - Skill templates for Claude Code

docs/reference/configuration.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ Controls the semantic index layer, which generates normalised index entries for
103103

104104
When enabled, each chunk gets an LLM-generated description (~130 tokens) at ingestion time. These descriptions are embedded and searched instead of raw chunks, providing uniform information density. See [How It Works](../guides/how-it-works.md#semantic-index) for details.
105105

106+
## Entity Extraction
107+
108+
Entity extraction runs automatically during ingestion with no configuration required. It uses deterministic regex patterns to identify people (`@mentions`, emails, "X said"), channels (`#channel`), meetings (standup, retro, 1:1), and URLs. Extracted entities are stored with alias resolution and used as an RRF boost source (weight 1.5) during search.
109+
110+
Entity extraction skips code blocks and `[Thinking]` blocks to reduce false positives. The feature is always-on with no configuration knobs — it adds zero latency to queries that don't contain entity references.
111+
106112
## Length Penalty Settings
107113

108114
### `lengthPenalty`
@@ -138,10 +144,12 @@ Controls time-decay scoring for search results.
138144

139145
Controls the search retrieval pipeline.
140146

141-
| Property | Type | Default | Description |
142-
| ----------- | -------- | ------- | ------------------------------------------------------------------ |
143-
| `mmrLambda` | `number` | `0.7` | MMR (Maximal Marginal Relevance) lambda parameter (0-1) |
144-
| `feedbackWeight` | `number` | `0.1` | Weight applied to implicit relevance feedback signals (0-1) |
147+
| Property | Type | Default | Description |
148+
| ------------------ | -------- | ---------- | ------------------------------------------------------------------ |
149+
| `primary` | `string` | `"hybrid"` | Primary retrieval method: `"keyword"`, `"vector"`, or `"hybrid"` (BM25 + vector + RRF) |
150+
| `vectorEnrichment` | `boolean`| `false` | Use vector search to enrich keyword results when primary is `"keyword"`. No effect in hybrid mode. |
151+
| `mmrLambda` | `number` | `0.7` | MMR (Maximal Marginal Relevance) lambda parameter (0-1) |
152+
| `feedbackWeight` | `number` | `0.1` | Weight applied to implicit relevance feedback signals (0-1) |
145153

146154
MMR reranks search results to balance relevance with diversity. After RRF fusion and cluster expansion, candidates are reordered so that semantically redundant chunks yield to novel ones.
147155

docs/reference/mcp-tools.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ All tools return plain text responses via the MCP `content` array with `type: "t
1616

1717
### search
1818

19-
Search memory semantically to discover relevant past context. Returns ranked results using hybrid BM25 + vector search with RRF fusion, cluster expansion, and MMR diversity reranking.
19+
Search memory to discover relevant past context. Uses hybrid (BM25 + vector) retrieval with entity boosting. Returns ranked results by relevance. For recent/latest session queries, use `reconstruct` instead.
2020

2121
**Parameters**:
2222

@@ -38,7 +38,7 @@ Found 5 relevant memory chunks (1200 tokens):
3838

3939
### recall
4040

41-
Recall episodic memory by walking backward through causal chains to reconstruct narrative context. Seeds are found by semantic search; the causal graph unfolds them into ordered chains; chains are ranked by aggregate semantic relevance per token. Falls back to search results when no viable chain is found.
41+
Recall episodic memory by walking backward through causal chains to reconstruct narrative context. Seeds are found by semantic search; the causal graph unfolds them into ordered chains; chains are ranked by aggregate semantic relevance per token. Falls back to search results when no viable chain is found. For recent/latest session queries, use `reconstruct` instead.
4242

4343
**Parameters**:
4444

@@ -158,7 +158,7 @@ Returns `"No sessions found for project "[name]"."` if none match.
158158

159159
### reconstruct
160160

161-
Rebuild session context for a project. Call with just `project` to get the most recent history up to the token budget (timeline mode). Optionally specify a time range with `from`/`to`, `days_back`, `session_id`, or `previous_session`.
161+
Use this for all recent/latest/last session queries. Rebuild session context for a project. Call with just `project` to get the most recent history up to the token budget (timeline mode). Optionally specify a time range with `from`/`to`, `days_back`, `session_id`, or `previous_session`.
162162

163163
**Parameters**:
164164

@@ -200,12 +200,13 @@ Show memory statistics including version, chunk/edge/cluster counts, and per-pro
200200
**Example**:
201201

202202
```
203-
Causantic v0.9.4
203+
Causantic v0.10.1
204204
205205
Memory Statistics:
206206
- Chunks: 1234
207207
- Edges: 5678
208208
- Clusters: 42
209+
- Entities: 89
209210
210211
Projects:
211212
- my-app: 800 chunks (Jan 2025 – Feb 2025)

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "causantic",
3-
"version": "0.10.0",
3+
"version": "0.10.1",
44
"description": "Long-term memory for Claude Code — local-first, graph-augmented, self-benchmarking",
55
"type": "module",
66
"private": false,

scripts/backfill-index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ async function main() {
7171
console.log(`Starting index backfill (limit=${limit})...\n`);
7272

7373
const status = indexRefresher.getBackfillStatus();
74-
console.log(`Current status: ${status.indexed}/${status.total} indexed (${status.remaining} remaining)\n`);
74+
console.log(
75+
`Current status: ${status.indexed}/${status.total} indexed (${status.remaining} remaining)\n`,
76+
);
7577

7678
const result = await indexRefresher.backfill({
7779
limit,
@@ -91,12 +93,16 @@ async function main() {
9193
console.log(` Duration: ${(result.durationMs / 1000).toFixed(1)}s`);
9294

9395
const finalStatus = indexRefresher.getBackfillStatus();
94-
console.log(`\nFinal status: ${finalStatus.indexed}/${finalStatus.total} indexed (${finalStatus.remaining} remaining)`);
96+
console.log(
97+
`\nFinal status: ${finalStatus.indexed}/${finalStatus.total} indexed (${finalStatus.remaining} remaining)`,
98+
);
9599

96100
// Show generation method breakdown
97101
const db = getDb();
98102
const methods = db
99-
.prepare('SELECT generation_method, COUNT(*) as cnt FROM index_entries GROUP BY generation_method')
103+
.prepare(
104+
'SELECT generation_method, COUNT(*) as cnt FROM index_entries GROUP BY generation_method',
105+
)
100106
.all() as Array<{ generation_method: string; cnt: number }>;
101107
console.log('\nGeneration methods:');
102108
for (const m of methods) {

src/cli/skill-templates.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ Pass these to the \`search\` MCP tool:
101101
## Guidelines
102102
103103
- **Always pass the \`project\` parameter** scoped to the current project (derive from the working directory) unless the user explicitly asks for cross-project results
104-
- By default, search uses **keyword-first (BM25)** retrieval — great for exact matches on function names, error codes, and specific terms
105-
- Optional vector enrichment can be enabled in config for semantic similarity matching
104+
- By default, search uses **hybrid (BM25 + vector)** retrieval with entity boosting — combines exact keyword matching with semantic similarity
105+
- For recent/latest session queries, use \`reconstruct\` instead
106106
- Use \`search\` for discovery, \`recall\` for narrative reconstruction
107107
- Combine with \`/causantic-recall\` when you need causal chain context (how things led to outcomes)
108108
`,

src/clusters/cluster-manager.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -640,9 +640,7 @@ export class ClusterManager {
640640
* Creates clusters in the `index_entry_clusters` table (parallel to `chunk_clusters`).
641641
* Each cluster elects a representative (entry closest to centroid) for browsing.
642642
*/
643-
async reclusterIndexEntries(
644-
options: ClusteringOptions = {},
645-
): Promise<ClusteringResult> {
643+
async reclusterIndexEntries(options: ClusteringOptions = {}): Promise<ClusteringResult> {
646644
const startTime = Date.now();
647645
const { minClusterSize = this.config.minClusterSize } = options;
648646

@@ -657,9 +655,7 @@ export class ClusterManager {
657655
PRIMARY KEY (index_entry_id, cluster_id)
658656
)
659657
`);
660-
db.exec(
661-
'CREATE INDEX IF NOT EXISTS idx_iec_cluster ON index_entry_clusters(cluster_id)',
662-
);
658+
db.exec('CREATE INDEX IF NOT EXISTS idx_iec_cluster ON index_entry_clusters(cluster_id)');
663659

664660
// Clear existing index entry cluster assignments
665661
db.exec('DELETE FROM index_entry_clusters');

0 commit comments

Comments
 (0)