clawmem 0.5.1 → 0.7.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/AGENTS.md +5 -4
- package/CLAUDE.md +5 -4
- package/README.md +16 -2
- package/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/clawmem.ts +17 -0
- package/src/consolidation.ts +323 -1
- package/src/hooks/context-surfacing.ts +104 -13
- package/src/hooks/feedback-loop.ts +40 -0
- package/src/hooks/session-bootstrap.ts +20 -2
- package/src/hooks.ts +8 -3
- package/src/mcp.ts +32 -1
- package/src/memory.ts +5 -3
- package/src/recall-attribution.ts +182 -0
- package/src/recall-buffer.ts +85 -0
- package/src/store.ts +306 -13
package/AGENTS.md
CHANGED
|
@@ -250,9 +250,9 @@ ClawMem hooks handle ~90% of retrieval automatically. Agent-initiated MCP calls
|
|
|
250
250
|
| `postcompact-inject` | SessionStart (compact) | 1200 tokens | re-injects authoritative context after compaction: precompact state (600) + recent decisions (400) + antipatterns (150) + vault context (200) → `<vault-postcompact>` |
|
|
251
251
|
| `curator-nudge` | SessionStart | 200 tokens | surfaces curator report actions, nudges when report is stale (>7 days) |
|
|
252
252
|
| `precompact-extract` | PreCompact | — | extracts decisions, file paths, open questions → writes `precompact-state.md` to auto-memory. Query-aware decision ranking. Reindexes auto-memory collection. |
|
|
253
|
-
| `decision-extractor` | Stop | — | LLM extracts observations → `_clawmem/agent/observations/`, infers causal links, detects contradictions
|
|
253
|
+
| `decision-extractor` | Stop | — | LLM extracts observations → `_clawmem/agent/observations/`, infers causal links, detects contradictions, extracts SPO triples from decision/preference/milestone/problem facts. Background consolidation worker synthesizes deductive observations from related facts (Phase 3, every ~15 min). |
|
|
254
254
|
| `handoff-generator` | Stop | — | LLM summarizes session → `_clawmem/agent/handoffs/` |
|
|
255
|
-
| `feedback-loop` | Stop | — | tracks referenced notes → boosts confidence, records usage relations + co-activations between co-referenced docs, tracks utility signals (surfaced vs referenced ratio for lifecycle automation) |
|
|
255
|
+
| `feedback-loop` | Stop | — | tracks referenced notes → boosts confidence, records usage relations + co-activations between co-referenced docs, tracks utility signals (surfaced vs referenced ratio for lifecycle automation), per-turn recall attribution (marks which surfaced docs were cited in which turn) |
|
|
256
256
|
|
|
257
257
|
**Default behavior:** Read injected `<vault-context>` first. If sufficient, answer immediately.
|
|
258
258
|
|
|
@@ -447,7 +447,7 @@ compositeScore = (0.10 × searchScore + 0.70 × recencyScore + 0.20 × confidenc
|
|
|
447
447
|
|
|
448
448
|
| Content Type | Half-Life | Effect |
|
|
449
449
|
|--------------|-----------|--------|
|
|
450
|
-
| decision, preference, hub | ∞ | Never decay |
|
|
450
|
+
| decision, deductive, preference, hub | ∞ | Never decay |
|
|
451
451
|
| antipattern | ∞ | Never decay — accumulated negative patterns persist |
|
|
452
452
|
| project | 120 days | Slow decay |
|
|
453
453
|
| research | 90 days | Moderate decay |
|
|
@@ -456,7 +456,7 @@ compositeScore = (0.10 × searchScore + 0.70 × recencyScore + 0.20 × confidenc
|
|
|
456
456
|
| handoff | 30 days | Fast — recent matters most |
|
|
457
457
|
|
|
458
458
|
Half-lives extend up to 3× for frequently-accessed memories (access reinforcement decays over 90 days).
|
|
459
|
-
Attention decay: non-durable types (handoff, progress, conversation, note, project) lose 5% confidence per week without access. Decision/preference/hub/research/antipattern are exempt.
|
|
459
|
+
Attention decay: non-durable types (handoff, progress, conversation, note, project) lose 5% confidence per week without access. Decision/deductive/preference/hub/research/antipattern are exempt.
|
|
460
460
|
|
|
461
461
|
## Indexing & Graph Building
|
|
462
462
|
|
|
@@ -499,6 +499,7 @@ The `memory_relations` table is populated by multiple independent sources:
|
|
|
499
499
|
| `buildSemanticGraph()` | semantic | `build_graphs` MCP tool (manual) | Pure cosine similarity. PK collision: `INSERT OR IGNORE` means A-MEM semantic edges take precedence if they exist first. |
|
|
500
500
|
| Entity co-occurrence graph | entity | A-MEM enrichment (indexing) | LLM entity extraction → quality filters (title/length/blocklist/location validation) → type-agnostic canonical resolution within compatibility buckets (person, org, location, tech=project/service/tool/concept) → `entity_mentions` + `entity_cooccurrences` tables. Entity edges use IDF-based specificity scoring. Feeds ENTITY intent queries and MPFP `[entity, semantic]` patterns. |
|
|
501
501
|
| `consolidated_observations` | supporting | Consolidation worker (background) | 3-tier consolidation: facts → observations → mental models. Observations track `proof_count`, `trend` (STABLE/STRENGTHENING/WEAKENING/STALE), and source links. |
|
|
502
|
+
| Deductive synthesis | supporting | Consolidation worker Phase 3 (background, every ~15 min) | Combines 2-3 related recent observations (decision/preference/milestone/problem, last 7 days) into `content_type='deductive'` documents with `source_doc_ids` provenance. First-class searchable docs with ∞ half-life. |
|
|
502
503
|
|
|
503
504
|
**Edge collision:** Both `generateMemoryLinks()` and `buildSemanticGraph()` insert `relation_type='semantic'`. PK is `(source_id, target_id, relation_type)` — first writer wins.
|
|
504
505
|
|
package/CLAUDE.md
CHANGED
|
@@ -250,9 +250,9 @@ ClawMem hooks handle ~90% of retrieval automatically. Agent-initiated MCP calls
|
|
|
250
250
|
| `postcompact-inject` | SessionStart (compact) | 1200 tokens | re-injects authoritative context after compaction: precompact state (600) + recent decisions (400) + antipatterns (150) + vault context (200) → `<vault-postcompact>` |
|
|
251
251
|
| `curator-nudge` | SessionStart | 200 tokens | surfaces curator report actions, nudges when report is stale (>7 days) |
|
|
252
252
|
| `precompact-extract` | PreCompact | — | extracts decisions, file paths, open questions → writes `precompact-state.md` to auto-memory. Query-aware decision ranking. Reindexes auto-memory collection. |
|
|
253
|
-
| `decision-extractor` | Stop | — | LLM extracts observations → `_clawmem/agent/observations/`, infers causal links, detects contradictions
|
|
253
|
+
| `decision-extractor` | Stop | — | LLM extracts observations → `_clawmem/agent/observations/`, infers causal links, detects contradictions, extracts SPO triples from decision/preference/milestone/problem facts. Background consolidation worker synthesizes deductive observations from related facts (Phase 3, every ~15 min). |
|
|
254
254
|
| `handoff-generator` | Stop | — | LLM summarizes session → `_clawmem/agent/handoffs/` |
|
|
255
|
-
| `feedback-loop` | Stop | — | tracks referenced notes → boosts confidence, records usage relations + co-activations between co-referenced docs, tracks utility signals (surfaced vs referenced ratio for lifecycle automation) |
|
|
255
|
+
| `feedback-loop` | Stop | — | tracks referenced notes → boosts confidence, records usage relations + co-activations between co-referenced docs, tracks utility signals (surfaced vs referenced ratio for lifecycle automation), per-turn recall attribution (marks which surfaced docs were cited in which turn) |
|
|
256
256
|
|
|
257
257
|
**Default behavior:** Read injected `<vault-context>` first. If sufficient, answer immediately.
|
|
258
258
|
|
|
@@ -447,7 +447,7 @@ compositeScore = (0.10 × searchScore + 0.70 × recencyScore + 0.20 × confidenc
|
|
|
447
447
|
|
|
448
448
|
| Content Type | Half-Life | Effect |
|
|
449
449
|
|--------------|-----------|--------|
|
|
450
|
-
| decision, preference, hub | ∞ | Never decay |
|
|
450
|
+
| decision, deductive, preference, hub | ∞ | Never decay |
|
|
451
451
|
| antipattern | ∞ | Never decay — accumulated negative patterns persist |
|
|
452
452
|
| project | 120 days | Slow decay |
|
|
453
453
|
| research | 90 days | Moderate decay |
|
|
@@ -456,7 +456,7 @@ compositeScore = (0.10 × searchScore + 0.70 × recencyScore + 0.20 × confidenc
|
|
|
456
456
|
| handoff | 30 days | Fast — recent matters most |
|
|
457
457
|
|
|
458
458
|
Half-lives extend up to 3× for frequently-accessed memories (access reinforcement decays over 90 days).
|
|
459
|
-
Attention decay: non-durable types (handoff, progress, conversation, note, project) lose 5% confidence per week without access. Decision/preference/hub/research/antipattern are exempt.
|
|
459
|
+
Attention decay: non-durable types (handoff, progress, conversation, note, project) lose 5% confidence per week without access. Decision/deductive/preference/hub/research/antipattern are exempt.
|
|
460
460
|
|
|
461
461
|
## Indexing & Graph Building
|
|
462
462
|
|
|
@@ -499,6 +499,7 @@ The `memory_relations` table is populated by multiple independent sources:
|
|
|
499
499
|
| `buildSemanticGraph()` | semantic | `build_graphs` MCP tool (manual) | Pure cosine similarity. PK collision: `INSERT OR IGNORE` means A-MEM semantic edges take precedence if they exist first. |
|
|
500
500
|
| Entity co-occurrence graph | entity | A-MEM enrichment (indexing) | LLM entity extraction → quality filters (title/length/blocklist/location validation) → type-agnostic canonical resolution within compatibility buckets (person, org, location, tech=project/service/tool/concept) → `entity_mentions` + `entity_cooccurrences` tables. Entity edges use IDF-based specificity scoring. Feeds ENTITY intent queries and MPFP `[entity, semantic]` patterns. |
|
|
501
501
|
| `consolidated_observations` | supporting | Consolidation worker (background) | 3-tier consolidation: facts → observations → mental models. Observations track `proof_count`, `trend` (STABLE/STRENGTHENING/WEAKENING/STALE), and source links. |
|
|
502
|
+
| Deductive synthesis | supporting | Consolidation worker Phase 3 (background, every ~15 min) | Combines 2-3 related recent observations (decision/preference/milestone/problem, last 7 days) into `content_type='deductive'` documents with `source_doc_ids` provenance. First-class searchable docs with ∞ half-life. |
|
|
502
503
|
|
|
503
504
|
**Edge collision:** Both `generateMemoryLinks()` and `buildSemanticGraph()` insert `relation_type='semantic'`. PK is `(source_id, target_id, relation_type)` — first writer wins.
|
|
504
505
|
|
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ Runs fully local with no API keys and no cloud services. Integrates via Claude C
|
|
|
85
85
|
**Optional integrations:**
|
|
86
86
|
|
|
87
87
|
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) — for hooks + MCP integration
|
|
88
|
-
- [OpenClaw](https://github.com/
|
|
88
|
+
- [OpenClaw](https://github.com/openclaw/openclaw) — for ContextEngine plugin integration
|
|
89
89
|
- [Hermes Agent](https://github.com/NousResearch/hermes-agent) — for MemoryProvider plugin integration
|
|
90
90
|
- [bd CLI](https://github.com/dolthub/dolt) v0.58.0+ — for Beads issue tracker sync (only if using Beads)
|
|
91
91
|
|
|
@@ -823,6 +823,7 @@ For WHY and ENTITY queries, the search pipeline expands results through the memo
|
|
|
823
823
|
| Type | Half-life | Baseline | Notes |
|
|
824
824
|
|---|---|---|---|
|
|
825
825
|
| `decision` | ∞ | 0.85 | Never decays |
|
|
826
|
+
| `deductive` | ∞ | 0.85 | Never decays — cross-session derived insights with source provenance |
|
|
826
827
|
| `preference` | ∞ | 0.80 | Never decays — user preferences are durable facts |
|
|
827
828
|
| `hub` | ∞ | 0.80 | Never decays |
|
|
828
829
|
| `antipattern` | ∞ | 0.75 | Never decays — accumulated negative patterns persist |
|
|
@@ -835,7 +836,7 @@ For WHY and ENTITY queries, the search pipeline expands results through the memo
|
|
|
835
836
|
| `progress` | 45 days | 0.50 | |
|
|
836
837
|
| `note` | 60 days | 0.50 | Default |
|
|
837
838
|
|
|
838
|
-
Content types are inferred from frontmatter or file path patterns. Half-lives extend up to 3× for frequently-accessed memories (access reinforcement, decays over 90 days). Non-durable types (handoff, progress, conversation, note, project) lose 5% confidence per week without access (attention decay). Decision/preference/hub/research/antipattern are exempt.
|
|
839
|
+
Content types are inferred from frontmatter or file path patterns. Half-lives extend up to 3× for frequently-accessed memories (access reinforcement, decays over 90 days). Non-durable types (handoff, progress, conversation, note, project) lose 5% confidence per week without access (attention decay). Decision/deductive/preference/hub/research/antipattern are exempt.
|
|
839
840
|
|
|
840
841
|
**Quality scoring:** Each document gets a `quality_score` (0.0–1.0) computed during indexing based on length, structure (headings, lists), decision/correction keywords, and frontmatter richness. Applied as `qualityMultiplier = 0.7 + 0.6 × qualityScore` (range: 0.7× penalty to 1.3× boost).
|
|
841
842
|
|
|
@@ -884,6 +885,17 @@ Documents are split into semantic fragments (sections, lists, code blocks, front
|
|
|
884
885
|
|
|
885
886
|
Uses the LLM server (shared with query expansion and intent classification) to extract structured observations from session transcripts. Observation types: `decision`, `bugfix`, `feature`, `refactor`, `discovery`, `change`, `preference`, `milestone`, `problem`. Each observation includes title, facts, narrative, concepts, and files read/modified. Preferences, milestones, and problems get first-class content_type treatment with dedicated confidence baselines and half-lives instead of being flattened to generic "observation". Falls back to regex patterns if the model is unavailable.
|
|
886
887
|
|
|
888
|
+
### Recall Tracking
|
|
889
|
+
|
|
890
|
+
Empirical tracking of which documents are surfaced by retrieval, which queries surfaced them, and whether the assistant actually cited them. Provides signals beyond raw search relevance for lifecycle decisions:
|
|
891
|
+
|
|
892
|
+
- **Per-query diversity**: docs surfaced by multiple distinct queries have proven cross-domain relevance
|
|
893
|
+
- **Multi-day spacing**: docs surfaced across separate calendar days (spaced frequency) are more valuable than binge recalls in one session
|
|
894
|
+
- **Negative signals**: docs surfaced frequently but rarely referenced are noise candidates for snooze
|
|
895
|
+
- **Per-turn attribution**: feedback-loop segments the transcript into turns and attributes references to specific context-surfacing invocations, not the session globally
|
|
896
|
+
|
|
897
|
+
Data feeds `lifecycle_status` (pin/snooze candidate reports) and `lifecycle_sweep` (recall-based recommendations). Adapted from [OpenClaw](https://github.com/openclaw/openclaw) dreaming promotion patterns.
|
|
898
|
+
|
|
887
899
|
### User Profile
|
|
888
900
|
|
|
889
901
|
Two-tier auto-curated profile extracted from your decisions and hub documents:
|
|
@@ -1119,9 +1131,11 @@ Built on the shoulders of:
|
|
|
1119
1131
|
- [Engram](https://github.com/Gentleman-Programming/engram) — observation dedup window, topic-key upsert pattern, temporal timeline navigation, duplicate metadata scoring signals
|
|
1120
1132
|
- [Hermes Agent](https://github.com/NousResearch/hermes-agent) — MemoryProvider plugin integration, memory nudge system (periodic lifecycle tool prompting)
|
|
1121
1133
|
- [Hindsight](https://github.com/vectorize-io/hindsight) — entity resolution, MPFP graph traversal, temporal extraction, 3-tier consolidation, observation invalidation, 4-way parallel retrieval
|
|
1134
|
+
- [Honcho](https://github.com/plastic-labs/honcho) — deductive observation synthesis patterns, surprisal-based anomaly scoring concept, embed-state self-healing, retrieval separation (raw vs derived)
|
|
1122
1135
|
- [MAGMA](https://arxiv.org/abs/2501.13956) — multi-graph memory agent
|
|
1123
1136
|
- [MemPalace](https://github.com/milla-jovovich/mempalace) — conversation import patterns, broadened observation taxonomy (preference/milestone/problem), session-bootstrap synthesis
|
|
1124
1137
|
- [memory-lancedb-pro](https://github.com/CortexReach/memory-lancedb-pro) — retrieval gate, length normalization, MMR diversity, access reinforcement algorithms
|
|
1138
|
+
- [OpenClaw](https://github.com/openclaw/openclaw) — recall tracking patterns (per-query diversity, multi-day spacing, negative signal tracking, promotion scoring) extracted from the dreaming memory consolidation system
|
|
1125
1139
|
- [OpenViking](https://github.com/volcengine/OpenViking) — query decomposition patterns, collection-scoped retrieval, transaction-safe indexing
|
|
1126
1140
|
- [QMD](https://github.com/tobi/qmd) — search backend (BM25 + vectors + RRF + reranking)
|
|
1127
1141
|
- [SAME](https://github.com/sgx-labs/statelessagent) — agent memory concepts (recency decay, confidence scoring, session tracking)
|
package/SKILL.md
CHANGED
|
@@ -451,7 +451,7 @@ compositeScore = (0.10 x searchScore + 0.70 x recencyScore + 0.20 x confidenceSc
|
|
|
451
451
|
|
|
452
452
|
| Content Type | Half-Life | Effect |
|
|
453
453
|
|--------------|-----------|--------|
|
|
454
|
-
| decision, preference, hub | infinity | Never decay |
|
|
454
|
+
| decision, deductive, preference, hub | infinity | Never decay |
|
|
455
455
|
| antipattern | infinity | Never decay — accumulated negative patterns persist |
|
|
456
456
|
| project | 120 days | Slow decay |
|
|
457
457
|
| research | 90 days | Moderate decay |
|
package/package.json
CHANGED
package/src/clawmem.ts
CHANGED
|
@@ -410,6 +410,9 @@ async function cmdEmbed(args: string[]) {
|
|
|
410
410
|
|
|
411
411
|
const fragments = splitDocument(body, frontmatter);
|
|
412
412
|
const docStart = Date.now();
|
|
413
|
+
const prevTotalFragments = totalFragments;
|
|
414
|
+
const prevFailedFragments = failedFragments;
|
|
415
|
+
let seq0Succeeded = false;
|
|
413
416
|
console.error(` [${docIdx + 1}/${hashes.length}] ${basename(path)} (${fragments.length} frags, ${body.length} chars)`);
|
|
414
417
|
|
|
415
418
|
if (isCloudEmbed) {
|
|
@@ -463,6 +466,7 @@ async function cmdEmbed(args: string[]) {
|
|
|
463
466
|
result.model, new Date().toISOString(), frag.type, frag.label ?? undefined, canId
|
|
464
467
|
);
|
|
465
468
|
totalFragments++;
|
|
469
|
+
if (seq === 0) seq0Succeeded = true;
|
|
466
470
|
} else {
|
|
467
471
|
failedFragments++;
|
|
468
472
|
}
|
|
@@ -491,6 +495,7 @@ async function cmdEmbed(args: string[]) {
|
|
|
491
495
|
result.model, new Date().toISOString(), frag.type, frag.label ?? undefined, canId
|
|
492
496
|
);
|
|
493
497
|
totalFragments++;
|
|
498
|
+
if (seq === 0) seq0Succeeded = true;
|
|
494
499
|
if (seq === 0 || (seq + 1) % 5 === 0 || seq === fragments.length - 1) {
|
|
495
500
|
console.error(` frag ${seq + 1}/${fragments.length} (${frag.type}) ${fragMs}ms [${text.length} chars]`);
|
|
496
501
|
}
|
|
@@ -505,6 +510,18 @@ async function cmdEmbed(args: string[]) {
|
|
|
505
510
|
}
|
|
506
511
|
}
|
|
507
512
|
|
|
513
|
+
// Track embed state per document — seq=0 (primary) must succeed for synced status
|
|
514
|
+
const docFragsOk = totalFragments - prevTotalFragments;
|
|
515
|
+
const docFragsFail = failedFragments - prevFailedFragments;
|
|
516
|
+
if (seq0Succeeded) {
|
|
517
|
+
s.markEmbedSynced(hash);
|
|
518
|
+
} else if (docFragsOk === 0 && docFragsFail > 0) {
|
|
519
|
+
s.markEmbedFailed(hash, "all fragments failed");
|
|
520
|
+
} else {
|
|
521
|
+
// seq=0 failed but some later fragments succeeded — mark failed so seq=0 gets retried
|
|
522
|
+
s.markEmbedFailed(hash, "primary fragment (seq=0) failed");
|
|
523
|
+
}
|
|
524
|
+
|
|
508
525
|
embedded++;
|
|
509
526
|
const docMs = Date.now() - docStart;
|
|
510
527
|
const elapsed = ((Date.now() - batchStart) / 1000).toFixed(0);
|
package/src/consolidation.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ClawMem Consolidation Worker
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Three-phase background worker:
|
|
5
5
|
* 1. A-MEM backfill: enriches documents missing memory notes
|
|
6
6
|
* 2. 3-tier consolidation: synthesizes clusters of related observations
|
|
7
7
|
* into higher-order consolidated observations with proof counts and trends
|
|
8
|
+
* 3. Deductive synthesis: combines related recent observations into
|
|
9
|
+
* first-class deductive documents with source provenance
|
|
8
10
|
*
|
|
9
11
|
* Pattern H from ENHANCEMENT-PLAN.md (source: Hindsight consolidator.py)
|
|
12
|
+
* Deductive synthesis inspired by Honcho's Dreamer deduction specialist.
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
15
|
import type { Store } from "./store.ts";
|
|
13
16
|
import type { LlamaCpp } from "./llm.ts";
|
|
14
17
|
import { extractJsonFromLLM } from "./amem.ts";
|
|
18
|
+
import { hashContent } from "./indexer.ts";
|
|
15
19
|
|
|
16
20
|
// =============================================================================
|
|
17
21
|
// Types
|
|
@@ -115,6 +119,22 @@ async function tick(store: Store, llm: LlamaCpp): Promise<void> {
|
|
|
115
119
|
if (tickCount % 6 === 0) {
|
|
116
120
|
await consolidateObservations(store, llm);
|
|
117
121
|
}
|
|
122
|
+
|
|
123
|
+
// Phase 3: Deductive synthesis (every 3rd tick, ~15 min at default interval)
|
|
124
|
+
if (tickCount % 3 === 0) {
|
|
125
|
+
await generateDeductiveObservations(store, llm);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Phase 4: Recall stats recomputation (every tick — lightweight SQL aggregation)
|
|
129
|
+
try {
|
|
130
|
+
const updated = store.recomputeRecallStats();
|
|
131
|
+
if (updated > 0) {
|
|
132
|
+
console.log(`[consolidation] Phase 4: recomputed recall_stats for ${updated} docs`);
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
// Non-critical — recall stats are informational, not retrieval-blocking
|
|
136
|
+
console.error("[consolidation] Phase 4 recall stats failed:", err);
|
|
137
|
+
}
|
|
118
138
|
} catch (err) {
|
|
119
139
|
console.error("[consolidation] Tick failed:", err);
|
|
120
140
|
} finally {
|
|
@@ -375,6 +395,308 @@ function updateTrends(store: Store): void {
|
|
|
375
395
|
}
|
|
376
396
|
}
|
|
377
397
|
|
|
398
|
+
// =============================================================================
|
|
399
|
+
// Phase 3: Deductive Observation Synthesis
|
|
400
|
+
// =============================================================================
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Find pairs/groups of recent high-confidence observations that can be combined
|
|
404
|
+
* into higher-level deductive conclusions. Creates first-class documents with
|
|
405
|
+
* content_type='deductive' and source_doc_ids provenance.
|
|
406
|
+
*
|
|
407
|
+
* Only considers decision/preference/milestone/problem observations from the
|
|
408
|
+
* last 7 days that haven't already been used as sources for deductions.
|
|
409
|
+
*/
|
|
410
|
+
async function generateDeductiveObservations(store: Store, llm: LlamaCpp): Promise<number> {
|
|
411
|
+
// Find recent high-value observations not yet used in deductions
|
|
412
|
+
const DEDUCTIVE_TYPES = ['decision', 'preference', 'milestone', 'problem'];
|
|
413
|
+
const recentObs = store.db.prepare(`
|
|
414
|
+
SELECT d.id, d.title, d.facts, d.narrative, d.observation_type, d.content_type,
|
|
415
|
+
d.collection, d.path, d.modified_at
|
|
416
|
+
FROM documents d
|
|
417
|
+
WHERE d.active = 1
|
|
418
|
+
AND d.content_type IN (${DEDUCTIVE_TYPES.map(() => '?').join(',')})
|
|
419
|
+
AND d.observation_type IS NOT NULL
|
|
420
|
+
AND d.facts IS NOT NULL
|
|
421
|
+
AND d.modified_at >= datetime('now', '-7 days')
|
|
422
|
+
AND d.id NOT IN (
|
|
423
|
+
SELECT value FROM (
|
|
424
|
+
SELECT json_each.value as value
|
|
425
|
+
FROM documents dd, json_each(dd.source_doc_ids)
|
|
426
|
+
WHERE dd.content_type = 'deductive' AND dd.active = 1
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
ORDER BY d.modified_at DESC
|
|
430
|
+
LIMIT 20
|
|
431
|
+
`).all(...DEDUCTIVE_TYPES) as {
|
|
432
|
+
id: number; title: string; facts: string; narrative: string;
|
|
433
|
+
observation_type: string; content_type: string; collection: string;
|
|
434
|
+
path: string; modified_at: string;
|
|
435
|
+
}[];
|
|
436
|
+
|
|
437
|
+
if (recentObs.length < 2) return 0;
|
|
438
|
+
|
|
439
|
+
// Build context for LLM
|
|
440
|
+
const obsText = recentObs.map((o, i) =>
|
|
441
|
+
`[${i + 1}] (${o.content_type}/${o.observation_type}) "${o.title}"\n Facts: ${(o.facts || '').slice(0, 300)}\n Narrative: ${(o.narrative || '').slice(0, 200)}`
|
|
442
|
+
).join('\n\n');
|
|
443
|
+
|
|
444
|
+
const prompt = `You are analyzing recent observations from a developer's work sessions. Find logical deductions that can be drawn by combining 2-3 observations.
|
|
445
|
+
|
|
446
|
+
A deduction combines facts from different observations into a NEW conclusion that isn't stated in any single observation alone.
|
|
447
|
+
|
|
448
|
+
Observations:
|
|
449
|
+
${obsText}
|
|
450
|
+
|
|
451
|
+
For each valid deduction:
|
|
452
|
+
1. State the conclusion clearly (1-2 sentences)
|
|
453
|
+
2. List the premises (which observations support it)
|
|
454
|
+
3. List the source indices (1-indexed)
|
|
455
|
+
|
|
456
|
+
Return ONLY valid JSON array:
|
|
457
|
+
[
|
|
458
|
+
{
|
|
459
|
+
"conclusion": "Clear deductive statement",
|
|
460
|
+
"premises": ["Premise from obs 1", "Premise from obs 3"],
|
|
461
|
+
"source_indices": [1, 3]
|
|
462
|
+
}
|
|
463
|
+
]
|
|
464
|
+
|
|
465
|
+
Rules:
|
|
466
|
+
- Each deduction MUST combine 2+ different observations (not restate a single one)
|
|
467
|
+
- Only include conclusions with genuine logical basis
|
|
468
|
+
- Maximum 3 deductions
|
|
469
|
+
- If no valid deductions exist, return []
|
|
470
|
+
Return ONLY the JSON array. /no_think`;
|
|
471
|
+
|
|
472
|
+
const result = await llm.generate(prompt, { temperature: 0.3, maxTokens: 500 });
|
|
473
|
+
if (!result?.text) return 0;
|
|
474
|
+
|
|
475
|
+
const parsed = extractJsonFromLLM(result.text) as Array<{
|
|
476
|
+
conclusion: string;
|
|
477
|
+
premises: string[];
|
|
478
|
+
source_indices: number[];
|
|
479
|
+
}> | null;
|
|
480
|
+
|
|
481
|
+
if (!Array.isArray(parsed)) return 0;
|
|
482
|
+
|
|
483
|
+
let created = 0;
|
|
484
|
+
const timestamp = new Date().toISOString();
|
|
485
|
+
const dateStr = timestamp.slice(0, 10);
|
|
486
|
+
|
|
487
|
+
for (const deduction of parsed) {
|
|
488
|
+
if (!deduction.conclusion || !Array.isArray(deduction.source_indices) || deduction.source_indices.length < 2) continue;
|
|
489
|
+
|
|
490
|
+
const sourceDocIds = deduction.source_indices
|
|
491
|
+
.filter(i => i >= 1 && i <= recentObs.length)
|
|
492
|
+
.map(i => recentObs[i - 1]!.id);
|
|
493
|
+
|
|
494
|
+
if (sourceDocIds.length < 2) continue;
|
|
495
|
+
|
|
496
|
+
// Check for duplicate deduction (Jaccard on conclusion text)
|
|
497
|
+
const existingDedups = store.db.prepare(`
|
|
498
|
+
SELECT id, title FROM documents
|
|
499
|
+
WHERE content_type = 'deductive' AND active = 1
|
|
500
|
+
ORDER BY created_at DESC LIMIT 20
|
|
501
|
+
`).all() as { id: number; title: string }[];
|
|
502
|
+
|
|
503
|
+
const conclusionWords = new Set(deduction.conclusion.toLowerCase().split(/\s+/).filter(w => w.length > 3));
|
|
504
|
+
const isDuplicate = existingDedups.some(d => {
|
|
505
|
+
const titleWords = new Set(d.title.toLowerCase().split(/\s+/).filter(w => w.length > 3));
|
|
506
|
+
const intersection = [...conclusionWords].filter(w => titleWords.has(w)).length;
|
|
507
|
+
const union = new Set([...conclusionWords, ...titleWords]).size;
|
|
508
|
+
return union > 0 && intersection / union > 0.5;
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
if (isDuplicate) continue;
|
|
512
|
+
|
|
513
|
+
// Build the deductive document
|
|
514
|
+
const premisesText = (deduction.premises || []).map(p => `- ${p}`).join('\n');
|
|
515
|
+
const sourceRefs = sourceDocIds.map(id => {
|
|
516
|
+
const obs = recentObs.find(o => o.id === id);
|
|
517
|
+
return obs ? `- "${obs.title}" (${obs.content_type})` : `- doc#${id}`;
|
|
518
|
+
}).join('\n');
|
|
519
|
+
|
|
520
|
+
const body = [
|
|
521
|
+
`---`,
|
|
522
|
+
`content_type: deductive`,
|
|
523
|
+
`tags: [auto-deduced, consolidation]`,
|
|
524
|
+
`---`,
|
|
525
|
+
``,
|
|
526
|
+
`# ${deduction.conclusion.slice(0, 80)}`,
|
|
527
|
+
``,
|
|
528
|
+
deduction.conclusion,
|
|
529
|
+
``,
|
|
530
|
+
`## Premises`,
|
|
531
|
+
``,
|
|
532
|
+
premisesText,
|
|
533
|
+
``,
|
|
534
|
+
`## Sources`,
|
|
535
|
+
``,
|
|
536
|
+
sourceRefs,
|
|
537
|
+
``,
|
|
538
|
+
].join('\n');
|
|
539
|
+
|
|
540
|
+
const dedPath = `deductions/${dateStr}-${sourceDocIds.join('-')}.md`;
|
|
541
|
+
const hash = hashContent(body);
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
store.insertContent(hash, body, timestamp);
|
|
545
|
+
store.insertDocument("_clawmem", dedPath, deduction.conclusion.slice(0, 80), hash, timestamp, timestamp);
|
|
546
|
+
|
|
547
|
+
const doc = store.findActiveDocument("_clawmem", dedPath);
|
|
548
|
+
if (doc) {
|
|
549
|
+
store.updateDocumentMeta(doc.id, {
|
|
550
|
+
content_type: "deductive",
|
|
551
|
+
confidence: 0.85,
|
|
552
|
+
});
|
|
553
|
+
store.updateObservationFields(dedPath, "_clawmem", {
|
|
554
|
+
observation_type: "deductive",
|
|
555
|
+
facts: JSON.stringify(deduction.premises || []),
|
|
556
|
+
narrative: deduction.conclusion,
|
|
557
|
+
});
|
|
558
|
+
// Store source provenance
|
|
559
|
+
store.db.prepare(`UPDATE documents SET source_doc_ids = ? WHERE id = ?`)
|
|
560
|
+
.run(JSON.stringify(sourceDocIds), doc.id);
|
|
561
|
+
|
|
562
|
+
// Create supporting edges in memory_relations
|
|
563
|
+
for (const sourceId of sourceDocIds) {
|
|
564
|
+
try {
|
|
565
|
+
store.db.prepare(`
|
|
566
|
+
INSERT OR IGNORE INTO memory_relations (source_id, target_id, relation_type, weight, created_at)
|
|
567
|
+
VALUES (?, ?, 'supporting', 0.85, datetime('now'))
|
|
568
|
+
`).run(sourceId, doc.id);
|
|
569
|
+
} catch { /* non-fatal */ }
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
created++;
|
|
573
|
+
console.log(`[deductive] Created: "${deduction.conclusion.slice(0, 60)}..." from ${sourceDocIds.length} sources`);
|
|
574
|
+
}
|
|
575
|
+
} catch (err) {
|
|
576
|
+
console.error(`[deductive] Failed to create deduction:`, err);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return created;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Manually trigger deductive synthesis (for CLI or MCP tool).
|
|
585
|
+
*/
|
|
586
|
+
export async function runDeductiveSynthesis(
|
|
587
|
+
store: Store,
|
|
588
|
+
llm: LlamaCpp,
|
|
589
|
+
): Promise<{ created: number }> {
|
|
590
|
+
const created = await generateDeductiveObservations(store, llm);
|
|
591
|
+
return { created };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// =============================================================================
|
|
595
|
+
// Surprisal Scoring (k-NN density anomaly detection)
|
|
596
|
+
// =============================================================================
|
|
597
|
+
|
|
598
|
+
export interface SurprisalResult {
|
|
599
|
+
docId: number;
|
|
600
|
+
title: string;
|
|
601
|
+
path: string;
|
|
602
|
+
collection: string;
|
|
603
|
+
contentType: string;
|
|
604
|
+
avgNeighborDistance: number; // higher = more anomalous
|
|
605
|
+
neighborCount: number;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Compute surprisal scores for observation documents using k-NN average
|
|
610
|
+
* neighbor distance in embedding space. High-surprisal observations are
|
|
611
|
+
* anomalous — they don't fit existing patterns and deserve curator attention.
|
|
612
|
+
*
|
|
613
|
+
* Uses sqlite-vec's built-in KNN query (vec0 virtual table) for efficiency.
|
|
614
|
+
* Only scores documents that have embeddings (content_vectors + vectors_vec).
|
|
615
|
+
*/
|
|
616
|
+
export function computeSurprisalScores(
|
|
617
|
+
store: Store,
|
|
618
|
+
options?: { collection?: string; limit?: number; k?: number; minScore?: number }
|
|
619
|
+
): SurprisalResult[] {
|
|
620
|
+
const k = options?.k ?? 5;
|
|
621
|
+
const limit = options?.limit ?? 20;
|
|
622
|
+
const minScore = options?.minScore ?? 0;
|
|
623
|
+
|
|
624
|
+
// Get observation documents with embeddings (seq=0 = primary fragment)
|
|
625
|
+
let sql = `
|
|
626
|
+
SELECT d.id, d.title, d.path, d.collection, d.content_type,
|
|
627
|
+
cv.hash || '_0' as hash_seq
|
|
628
|
+
FROM documents d
|
|
629
|
+
JOIN content_vectors cv ON d.hash = cv.hash AND cv.seq = 0
|
|
630
|
+
WHERE d.active = 1
|
|
631
|
+
AND d.observation_type IS NOT NULL
|
|
632
|
+
`;
|
|
633
|
+
const params: any[] = [];
|
|
634
|
+
if (options?.collection) {
|
|
635
|
+
sql += ` AND d.collection = ?`;
|
|
636
|
+
params.push(options.collection);
|
|
637
|
+
}
|
|
638
|
+
sql += ` ORDER BY d.modified_at DESC LIMIT 100`;
|
|
639
|
+
|
|
640
|
+
const docs = store.db.prepare(sql).all(...params) as {
|
|
641
|
+
id: number; title: string; path: string; collection: string;
|
|
642
|
+
content_type: string; hash_seq: string;
|
|
643
|
+
}[];
|
|
644
|
+
|
|
645
|
+
if (docs.length < k + 1) return []; // Not enough docs for meaningful k-NN
|
|
646
|
+
|
|
647
|
+
// For each doc, query its k nearest neighbors and compute average distance
|
|
648
|
+
const results: SurprisalResult[] = [];
|
|
649
|
+
|
|
650
|
+
// Check if vectors_vec exists
|
|
651
|
+
const vecTable = store.db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='vectors_vec'`).get();
|
|
652
|
+
if (!vecTable) return [];
|
|
653
|
+
|
|
654
|
+
for (const doc of docs) {
|
|
655
|
+
try {
|
|
656
|
+
// Get this doc's embedding from vectors_vec
|
|
657
|
+
const vecRow = store.db.prepare(
|
|
658
|
+
`SELECT embedding FROM vectors_vec WHERE hash_seq = ?`
|
|
659
|
+
).get(doc.hash_seq) as { embedding: Float32Array | number[] } | null;
|
|
660
|
+
|
|
661
|
+
if (!vecRow?.embedding) continue;
|
|
662
|
+
|
|
663
|
+
// Query k+1 nearest neighbors (first result is the doc itself)
|
|
664
|
+
const neighbors = store.db.prepare(`
|
|
665
|
+
SELECT distance
|
|
666
|
+
FROM vectors_vec
|
|
667
|
+
WHERE embedding MATCH ?
|
|
668
|
+
ORDER BY distance
|
|
669
|
+
LIMIT ?
|
|
670
|
+
`).all(vecRow.embedding, k + 1) as { distance: number }[];
|
|
671
|
+
|
|
672
|
+
// Skip the first result (self, distance ≈ 0) and compute average
|
|
673
|
+
const nonSelf = neighbors.filter(n => n.distance > 0.001);
|
|
674
|
+
if (nonSelf.length === 0) continue;
|
|
675
|
+
|
|
676
|
+
const avgDist = nonSelf.reduce((sum, n) => sum + n.distance, 0) / nonSelf.length;
|
|
677
|
+
|
|
678
|
+
if (avgDist >= minScore) {
|
|
679
|
+
results.push({
|
|
680
|
+
docId: doc.id,
|
|
681
|
+
title: doc.title,
|
|
682
|
+
path: doc.path,
|
|
683
|
+
collection: doc.collection,
|
|
684
|
+
contentType: doc.content_type,
|
|
685
|
+
avgNeighborDistance: avgDist,
|
|
686
|
+
neighborCount: nonSelf.length,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
} catch {
|
|
690
|
+
// Skip docs that fail vector lookup (missing embedding, dimension mismatch)
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Sort by surprisal (highest first) and limit
|
|
696
|
+
results.sort((a, b) => b.avgNeighborDistance - a.avgNeighborDistance);
|
|
697
|
+
return results.slice(0, limit);
|
|
698
|
+
}
|
|
699
|
+
|
|
378
700
|
// =============================================================================
|
|
379
701
|
// Public API for MCP / CLI
|
|
380
702
|
// =============================================================================
|