recallnest 2.2.0 → 2.3.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/.env +4 -0
- package/CLAUDE.md +1 -1
- package/README.md +30 -5
- package/README_CN.md +30 -5
- package/config.json +55 -0
- package/connectors/examples/email-adapter.ts +73 -0
- package/connectors/examples/logs-adapter.ts +103 -0
- package/connectors/examples/rss-adapter.ts +95 -0
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +60 -0
- package/logs/ingest-2026-04-11.log +110 -0
- package/logs/ingest-2026-04-12.log +90 -0
- package/logs/ingest-2026-04-13.log +87 -0
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
- package/package.json +1 -1
- package/src/cli.ts +30 -0
- package/src/connector-types.ts +57 -0
- package/src/conversation-importer.ts +25 -1
- package/src/data-checkup.ts +31 -1
- package/src/doctor.ts +29 -0
- package/src/ingest.ts +203 -0
- package/src/mcp-server.ts +2 -2
- package/src/obsidian-connector.ts +282 -0
- package/src/session-schema.ts +2 -2
- package/src/source-heartbeat.ts +108 -0
- package/.claude-plugin/CLAUDE.md +0 -29
- package/.claude-plugin/marketplace.json +0 -18
- package/.dockerignore +0 -11
- package/.github/workflows/ci.yml +0 -28
- package/.mcp.json +0 -9
- package/CHANGELOG.md +0 -51
- package/DEMO.md +0 -94
- package/Dockerfile +0 -20
- package/EVAL.md +0 -89
- package/FAILURES.md +0 -75
- package/OPERATIONS.md +0 -59
- package/ROADMAP.md +0 -245
- package/SKILL.md +0 -250
- package/assets/dashboard.png +0 -0
- package/assets/knowledge-graph.png +0 -0
- package/assets/recallnest-workbench.png +0 -0
- package/assets/screenshots/ui-full.png +0 -0
- package/assets/screenshots/ui-overview.png +0 -0
- package/assets/screenshots/ui-search.png +0 -0
- package/assets/ui/app.js +0 -1031
- package/assets/ui/index.html +0 -224
- package/assets/ui/styles.css +0 -781
- package/assets/wechat-qr.jpg +0 -0
- package/bun.lock +0 -337
- package/daily-reflections/2026-03-05.md +0 -15
- package/daily-reflections/2026-03-06.md +0 -15
- package/daily-reflections/2026-03-07.md +0 -7
- package/daily-reflections/2026-03-08.md +0 -34
- package/daily-reflections/2026-03-09.md +0 -35
- package/daily-reflections/2026-03-10.md +0 -36
- package/daily-reflections/2026-03-11.md +0 -35
- package/daily-reflections/2026-03-12.md +0 -36
- package/daily-reflections/2026-03-13.md +0 -35
- package/daily-reflections/2026-03-14.md +0 -35
- package/daily-reflections/2026-03-15.md +0 -35
- package/daily-reflections/2026-03-16.md +0 -41
- package/daily-reflections/2026-03-17.md +0 -10
- package/daily-reflections/2026-03-18.md +0 -10
- package/daily-reflections/2026-03-19.md +0 -10
- package/daily-reflections/2026-03-20.md +0 -10
- package/daily-reflections/2026-03-21.md +0 -10
- package/daily-reflections/2026-03-22.md +0 -10
- package/daily-reflections/2026-03-23.md +0 -10
- package/daily-reflections/2026-03-24.md +0 -10
- package/daily-reflections/2026-03-25.md +0 -32
- package/daily-reflections/2026-03-26.md +0 -37
- package/daily-reflections/2026-03-27.md +0 -35
- package/daily-reflections/2026-03-28.md +0 -35
- package/daily-reflections/2026-03-29.md +0 -35
- package/daily-reflections/2026-03-30.md +0 -36
- package/daily-reflections/2026-03-31.md +0 -35
- package/daily-reflections/2026-04-01.md +0 -35
- package/daily-reflections/2026-04-02.md +0 -35
- package/daily-reflections/2026-04-03.md +0 -37
- package/daily-reflections/2026-04-04.md +0 -37
- package/daily-reflections/2026-04-05.md +0 -35
- package/daily-reflections/2026-04-06.md +0 -36
- package/daily-reflections/2026-04-07.md +0 -35
- package/daily-reflections/2026-04-08.md +0 -35
- package/daily-reflections/2026-04-12.md +0 -35
- package/data/alias-map.example.json +0 -22
- package/docs/api-reference.md +0 -1291
- package/docs/architecture.md +0 -168
- package/docs/memory-boundary-contract.md +0 -226
- package/docs/memory-categories.md +0 -119
- package/docs/pipeline-audit-ultramemory.md +0 -177
- package/docs/plans/2026-03-08-query-expansion.md +0 -270
- package/docs/plans/2026-03-16-continuity-layer.md +0 -196
- package/docs/plans/2026-04-01-recall-boost.md +0 -100
- package/docs/plans/mcp-tiering-plan.md +0 -126
- package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
- package/docs/self-evolution.md +0 -198
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
- package/docs/superpowers/plans/2026-04-11-constructive-retrieval-emotion.md +0 -1195
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
- package/docs/superpowers/specs/2026-04-11-constructive-retrieval-emotion-design.md +0 -398
- package/eval/continuity/results-2026-03-16.md +0 -117
- package/eval/continuity/results-2026-03-19.md +0 -453
- package/eval/continuity/runbook.md +0 -110
- package/eval/continuity/seed-cases.md +0 -54
- package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
- package/eval/reports/2026-03-06-baseline.md +0 -82
- package/eval/reports/query-expansion.md +0 -138
- package/glama.json +0 -6
- package/lm +0 -19
- package/plan-philosophy-upgrades.md +0 -363
- package/plan-product-upgrades.md +0 -155
- package/plan-v1.2-distributable.md +0 -105
- package/plan.md +0 -140
- package/research-memory-evolution.md +0 -440
- package/research-rl-agent-memory.md +0 -330
- package/scripts/archive-distilled-junk.ts +0 -116
- package/scripts/backfill-anchors.ts +0 -130
- package/scripts/backfill-l0l1l2.ts +0 -98
- package/scripts/blind-scan.ts +0 -588
- package/scripts/cleanup-step1-exact-dups.ts +0 -117
- package/scripts/cleanup-step2-template-dups.ts +0 -113
- package/scripts/cleanup-step3-stale-content.ts +0 -149
- package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
- package/scripts/delete-distilled-junk.ts +0 -88
- package/scripts/diagnose-vector-search.ts +0 -132
- package/scripts/distill-facts.ts +0 -305
- package/scripts/health-check.ts +0 -343
- package/scripts/incremental-ingest.sh +0 -71
- package/scripts/migrate-language.ts +0 -75
- package/scripts/mlp-health-check.ts +0 -382
- package/scripts/pull-claude-desktop.ts +0 -515
- package/scripts/re-ingest-cc.sh +0 -66
- package/scripts/rebalance-memory-health.ts +0 -181
- package/scripts/run-desktop-ingest.sh +0 -11
- package/scripts/start-server.sh +0 -12
- package/scripts/verify-emotion-decay.ts +0 -148
- package/scripts/viz-dashboard.html +0 -423
- package/scripts/viz-export.ts +0 -703
- package/src/__tests__/access-tracker-novelty.test.ts +0 -228
- package/src/__tests__/activity-counter.test.ts +0 -96
- package/src/__tests__/admission-control.test.ts +0 -206
- package/src/__tests__/anchor-generator.test.ts +0 -67
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/babel-integration.test.ts +0 -139
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/capture-engine.test.ts +0 -1144
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -33
- package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
- package/src/__tests__/circuit-breaker.test.ts +0 -99
- package/src/__tests__/cluster-consolidation.test.ts +0 -712
- package/src/__tests__/confidence-metadata.test.ts +0 -125
- package/src/__tests__/confidence-tracker.test.ts +0 -249
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -374
- package/src/__tests__/consolidation-engine.test.ts +0 -181
- package/src/__tests__/constructive-retrieval.test.ts +0 -509
- package/src/__tests__/context-collapse-renderer.test.ts +0 -214
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-reconstructor.test.ts +0 -65
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/conversation-importer.test.ts +0 -692
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/data-checkup.test.ts +0 -225
- package/src/__tests__/decay-exemption.test.ts +0 -84
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/dedup-guard.test.ts +0 -39
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/distill-lock.test.ts +0 -176
- package/src/__tests__/doctor.test.ts +0 -303
- package/src/__tests__/dream-pipeline.test.ts +0 -232
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/emotion-decay.test.ts +0 -46
- package/src/__tests__/emotion-detector.test.ts +0 -56
- package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
- package/src/__tests__/emotion-retrieval.test.ts +0 -56
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/ethics-layer.test.ts +0 -476
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/feature-flags-integration.test.ts +0 -54
- package/src/__tests__/frequency-tracker.test.ts +0 -113
- package/src/__tests__/graph-export.test.ts +0 -489
- package/src/__tests__/hotness-blend.test.ts +0 -76
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/interference-detector.test.ts +0 -203
- package/src/__tests__/kg-extractor.test.ts +0 -184
- package/src/__tests__/kg-store.test.ts +0 -198
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/mcp-efficiency.test.ts +0 -288
- package/src/__tests__/memory-boundaries.test.ts +0 -237
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -243
- package/src/__tests__/memory-evolution.test.ts +0 -271
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-lint.test.ts +0 -271
- package/src/__tests__/memory-output.test.ts +0 -215
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/narrative-architecture.test.ts +0 -456
- package/src/__tests__/pending-review.test.ts +0 -77
- package/src/__tests__/post-retrieval-filter.test.ts +0 -178
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/prediction-engine.test.ts +0 -349
- package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -230
- package/src/__tests__/preference-slots.test.ts +0 -205
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander-alias.test.ts +0 -89
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-governor.test.ts +0 -227
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4898
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/salience-scoring.test.ts +0 -106
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/session-checkpoint.test.ts +0 -259
- package/src/__tests__/session-distiller.test.ts +0 -611
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/skill-engine.test.ts +0 -546
- package/src/__tests__/skill-promotion.test.ts +0 -349
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/supersede-chain.test.ts +0 -154
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/temporal-validity.test.ts +0 -160
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-output-compressor.test.ts +0 -133
- package/src/__tests__/topic-tag.test.ts +0 -113
- package/src/__tests__/trusted-memory.test.ts +0 -828
- package/src/__tests__/version-manager.test.ts +0 -213
- package/src/__tests__/workflow-observation.test.ts +0 -400
- package/src/__tests__/write-verifier.test.ts +0 -122
|
@@ -1,1195 +0,0 @@
|
|
|
1
|
-
# Constructive Retrieval + Emotional Valence Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
-
|
|
5
|
-
**Goal:** Add simulation-theory-inspired context reconstruction and emotional valence scoring to RecallNest's retrieval and decay pipelines.
|
|
6
|
-
|
|
7
|
-
**Architecture:** Two independent features behind feature flags. Emotion valence adds a metadata field + heuristic detector + decay/retrieval scoring stages. Constructive retrieval adds a post-pipeline LLM reconstruction layer with triple-fallback grounding. Both degrade gracefully to current behavior when disabled.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** Bun, TypeScript strict, LanceDB, Jina v5 embeddings, OpenAI-compatible LLM API, Zod validation.
|
|
10
|
-
|
|
11
|
-
**Spec:** `docs/superpowers/specs/2026-04-11-constructive-retrieval-emotion-design.md`
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## File Map
|
|
16
|
-
|
|
17
|
-
| File | Action | Responsibility |
|
|
18
|
-
|------|--------|----------------|
|
|
19
|
-
| `src/emotion-detector.ts` | **CREATE** | Heuristic + LLM emotion detection |
|
|
20
|
-
| `src/context-reconstructor.ts` | **CREATE** | Reconstruction engine + grounding validator |
|
|
21
|
-
| `src/memory-schema.ts` | MODIFY | Add EmotionMetadata type |
|
|
22
|
-
| `src/decay-engine.ts` | MODIFY | Add emotion-adjusted half-life |
|
|
23
|
-
| `src/retriever.ts` | MODIFY | Add emotion scoring stage + reconstruction gate |
|
|
24
|
-
| `src/store.ts` | MODIFY | Call emotion detector at store time |
|
|
25
|
-
| `src/context-composer.ts` | MODIFY | Call reconstructor in resume flow |
|
|
26
|
-
| `src/mcp-server.ts` | MODIFY | Add `reconstruct` param to search_memory |
|
|
27
|
-
| `src/llm-client.ts` | MODIFY | Extend smartExtract prompt for emotion |
|
|
28
|
-
| `src/__tests__/emotion-detector.test.ts` | **CREATE** | Tests for emotion detection |
|
|
29
|
-
| `src/__tests__/emotion-decay.test.ts` | **CREATE** | Tests for emotion-adjusted decay |
|
|
30
|
-
| `src/__tests__/emotion-retrieval.test.ts` | **CREATE** | Tests for emotion scoring stage |
|
|
31
|
-
| `src/__tests__/context-reconstructor.test.ts` | **CREATE** | Tests for reconstruction + grounding |
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## Phase 1: Emotional Valence (Tasks 1-6)
|
|
36
|
-
|
|
37
|
-
### Task 1: EmotionMetadata Type + Feature Flag
|
|
38
|
-
|
|
39
|
-
**Files:**
|
|
40
|
-
- Modify: `src/memory-schema.ts` (add type after line 37)
|
|
41
|
-
|
|
42
|
-
- [ ] **Step 1: Add EmotionMetadata type to memory-schema.ts**
|
|
43
|
-
|
|
44
|
-
Open `src/memory-schema.ts`. After the existing type exports (line 37), add:
|
|
45
|
-
|
|
46
|
-
```typescript
|
|
47
|
-
// --- Emotional Valence (Philosophy of Memory: Affective Memory) ---
|
|
48
|
-
export interface EmotionMetadata {
|
|
49
|
-
/** Negative (-1) to Positive (+1) */
|
|
50
|
-
valence: number;
|
|
51
|
-
/** Calm (0) to Excited (1) */
|
|
52
|
-
arousal: number;
|
|
53
|
-
/** Human-readable label */
|
|
54
|
-
label?: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export const EmotionMetadataSchema = z.object({
|
|
58
|
-
valence: z.number().min(-1).max(1),
|
|
59
|
-
arousal: z.number().min(0).max(1),
|
|
60
|
-
label: z.string().max(30).optional(),
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
/** Parse emotion from metadata JSON string, returns null if absent */
|
|
64
|
-
export function parseEmotion(metadata: string | undefined): EmotionMetadata | null {
|
|
65
|
-
if (!metadata) return null;
|
|
66
|
-
try {
|
|
67
|
-
const parsed = JSON.parse(metadata);
|
|
68
|
-
if (parsed.emotion && typeof parsed.emotion.valence === "number") {
|
|
69
|
-
return EmotionMetadataSchema.parse(parsed.emotion);
|
|
70
|
-
}
|
|
71
|
-
} catch { /* malformed metadata - safe to ignore */ }
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Default neutral emotion for memories without emotion data */
|
|
76
|
-
export const NEUTRAL_EMOTION: EmotionMetadata = { valence: 0, arousal: 0, label: "neutral" };
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
- [ ] **Step 2: Add feature flag function**
|
|
80
|
-
|
|
81
|
-
Add to the bottom of `src/memory-schema.ts`, following the pattern in `src/multi-vector.ts:24-26`:
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
export function isEmotionScoringEnabled(): boolean {
|
|
85
|
-
return process.env.RECALLNEST_EMOTION_SCORING === "true";
|
|
86
|
-
}
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
- [ ] **Step 3: Verify TypeScript compiles**
|
|
90
|
-
|
|
91
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun run build 2>&1 | head -20`
|
|
92
|
-
Expected: No errors related to emotion types.
|
|
93
|
-
|
|
94
|
-
- [ ] **Step 4: Commit**
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
cd /Users/anxianjingya/Projects/recallnest
|
|
98
|
-
git add src/memory-schema.ts
|
|
99
|
-
git commit -m "feat: add EmotionMetadata type and feature flag for emotion scoring"
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
---
|
|
103
|
-
|
|
104
|
-
### Task 2: Emotion Detector Module
|
|
105
|
-
|
|
106
|
-
**Files:**
|
|
107
|
-
- Create: `src/emotion-detector.ts`
|
|
108
|
-
- Create: `src/__tests__/emotion-detector.test.ts`
|
|
109
|
-
|
|
110
|
-
- [ ] **Step 1: Write the failing tests**
|
|
111
|
-
|
|
112
|
-
Create `src/__tests__/emotion-detector.test.ts`:
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
import { describe, test, expect } from "bun:test";
|
|
116
|
-
import { detectEmotion } from "../emotion-detector.js";
|
|
117
|
-
|
|
118
|
-
describe("emotion-detector: heuristic", () => {
|
|
119
|
-
test("detects strong negative text", () => {
|
|
120
|
-
const result = detectEmotion("This bug is driving me crazy, everything is broken and wrong");
|
|
121
|
-
expect(result.valence).toBeLessThan(-0.3);
|
|
122
|
-
expect(result.label).toBe("negative");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("detects strong positive text", () => {
|
|
126
|
-
const result = detectEmotion("Finally solved it! Works perfectly, great progress today");
|
|
127
|
-
expect(result.valence).toBeGreaterThan(0.3);
|
|
128
|
-
expect(result.label).toBe("positive");
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test("detects neutral text", () => {
|
|
132
|
-
const result = detectEmotion("The user prefers dark mode and uses VS Code");
|
|
133
|
-
expect(Math.abs(result.valence)).toBeLessThan(0.3);
|
|
134
|
-
expect(result.label).toBe("neutral");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("detects high arousal", () => {
|
|
138
|
-
const result = detectEmotion("URGENT! Critical production issue, fix immediately!");
|
|
139
|
-
expect(result.arousal).toBeGreaterThan(0.3);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("detects low arousal for calm text", () => {
|
|
143
|
-
const result = detectEmotion("The default configuration uses port 3000");
|
|
144
|
-
expect(result.arousal).toBeLessThan(0.3);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test("handles mixed signals with net positive", () => {
|
|
148
|
-
const result = detectEmotion("Had a frustrating bug but finally solved it perfectly");
|
|
149
|
-
expect(result.valence).toBeGreaterThan(0);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("handles Chinese text", () => {
|
|
153
|
-
const result = detectEmotion("Too painful, keeps failing");
|
|
154
|
-
expect(result.valence).toBeLessThan(-0.3);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("handles empty text gracefully", () => {
|
|
158
|
-
const result = detectEmotion("");
|
|
159
|
-
expect(result.valence).toBe(0);
|
|
160
|
-
expect(result.arousal).toBe(0);
|
|
161
|
-
expect(result.label).toBe("neutral");
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test("clamps valence to [-1, 1]", () => {
|
|
165
|
-
const result = detectEmotion("broken error wrong bug failure hate broken error wrong");
|
|
166
|
-
expect(result.valence).toBeGreaterThanOrEqual(-1);
|
|
167
|
-
expect(result.valence).toBeLessThanOrEqual(1);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test("clamps arousal to [0, 1]", () => {
|
|
171
|
-
const result = detectEmotion("URGENT! CRITICAL! IMMEDIATELY! ASAP!");
|
|
172
|
-
expect(result.arousal).toBeGreaterThanOrEqual(0);
|
|
173
|
-
expect(result.arousal).toBeLessThanOrEqual(1);
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
- [ ] **Step 2: Run tests to verify they fail**
|
|
179
|
-
|
|
180
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test src/__tests__/emotion-detector.test.ts`
|
|
181
|
-
Expected: FAIL with module not found.
|
|
182
|
-
|
|
183
|
-
- [ ] **Step 3: Implement emotion-detector.ts**
|
|
184
|
-
|
|
185
|
-
Create `src/emotion-detector.ts`:
|
|
186
|
-
|
|
187
|
-
```typescript
|
|
188
|
-
import type { EmotionMetadata } from "./memory-schema.js";
|
|
189
|
-
import { isEmotionScoringEnabled } from "./memory-schema.js";
|
|
190
|
-
|
|
191
|
-
// --- Signal Dictionaries ---
|
|
192
|
-
|
|
193
|
-
const NEGATIVE_SIGNALS: string[] = [
|
|
194
|
-
"fail", "failed", "failure", "broken", "bug", "error", "wrong", "crash",
|
|
195
|
-
"frustrat", "hate", "terrible", "awful", "annoying", "pain", "stuck",
|
|
196
|
-
"problem", "issue", "mess", "ugly", "worst",
|
|
197
|
-
];
|
|
198
|
-
|
|
199
|
-
const POSITIVE_SIGNALS: string[] = [
|
|
200
|
-
"solved", "fixed", "works", "perfect", "great", "love", "excellent",
|
|
201
|
-
"success", "breakthrough", "finally", "awesome", "beautiful", "clean",
|
|
202
|
-
"elegant", "smooth", "done", "shipped",
|
|
203
|
-
];
|
|
204
|
-
|
|
205
|
-
const HIGH_AROUSAL_SIGNALS: string[] = [
|
|
206
|
-
"!", "!!", "urgent", "critical", "immediately", "ASAP", "emergency",
|
|
207
|
-
];
|
|
208
|
-
|
|
209
|
-
function clamp(value: number, min: number, max: number): number {
|
|
210
|
-
return Math.max(min, Math.min(max, value));
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Detect emotional valence and arousal from text using keyword heuristics.
|
|
215
|
-
* Zero LLM cost. Returns neutral emotion for empty text.
|
|
216
|
-
*/
|
|
217
|
-
export function detectEmotion(text: string): EmotionMetadata {
|
|
218
|
-
if (!text || text.length === 0) {
|
|
219
|
-
return { valence: 0, arousal: 0, label: "neutral" };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const lower = text.toLowerCase();
|
|
223
|
-
|
|
224
|
-
const negCount = NEGATIVE_SIGNALS.filter(s => lower.includes(s.toLowerCase())).length;
|
|
225
|
-
const posCount = POSITIVE_SIGNALS.filter(s => lower.includes(s.toLowerCase())).length;
|
|
226
|
-
const arousalCount = HIGH_AROUSAL_SIGNALS.filter(s => text.includes(s)).length;
|
|
227
|
-
|
|
228
|
-
const valence = clamp((posCount - negCount) * 0.25, -1, 1);
|
|
229
|
-
const arousal = clamp(arousalCount * 0.25, 0, 1);
|
|
230
|
-
const label = valence > 0.3 ? "positive" : valence < -0.3 ? "negative" : "neutral";
|
|
231
|
-
|
|
232
|
-
return { valence, arousal, label };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Conditionally detect emotion. Returns null when feature flag is off.
|
|
237
|
-
*/
|
|
238
|
-
export function detectEmotionIfEnabled(text: string): EmotionMetadata | null {
|
|
239
|
-
if (!isEmotionScoringEnabled()) return null;
|
|
240
|
-
return detectEmotion(text);
|
|
241
|
-
}
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
- [ ] **Step 4: Run tests to verify they pass**
|
|
245
|
-
|
|
246
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test src/__tests__/emotion-detector.test.ts`
|
|
247
|
-
Expected: All 10 tests PASS.
|
|
248
|
-
|
|
249
|
-
- [ ] **Step 5: Run full test suite**
|
|
250
|
-
|
|
251
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test`
|
|
252
|
-
Expected: All existing tests pass + 10 new.
|
|
253
|
-
|
|
254
|
-
- [ ] **Step 6: Commit**
|
|
255
|
-
|
|
256
|
-
```bash
|
|
257
|
-
cd /Users/anxianjingya/Projects/recallnest
|
|
258
|
-
git add src/emotion-detector.ts src/__tests__/emotion-detector.test.ts
|
|
259
|
-
git commit -m "feat: add heuristic emotion detector"
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
---
|
|
263
|
-
|
|
264
|
-
### Task 3: Emotion-Adjusted Decay
|
|
265
|
-
|
|
266
|
-
**Files:**
|
|
267
|
-
- Modify: `src/decay-engine.ts` (after line 87)
|
|
268
|
-
- Create: `src/__tests__/emotion-decay.test.ts`
|
|
269
|
-
|
|
270
|
-
- [ ] **Step 1: Write the failing tests**
|
|
271
|
-
|
|
272
|
-
Create `src/__tests__/emotion-decay.test.ts`:
|
|
273
|
-
|
|
274
|
-
```typescript
|
|
275
|
-
import { describe, test, expect } from "bun:test";
|
|
276
|
-
import { weibullDecay, adjustHalfLifeForEmotion, computeArousalBoost } from "../decay-engine.js";
|
|
277
|
-
|
|
278
|
-
describe("emotion-adjusted decay", () => {
|
|
279
|
-
test("returns base half-life when no emotion", () => {
|
|
280
|
-
expect(adjustHalfLifeForEmotion(60, undefined)).toBe(60);
|
|
281
|
-
expect(adjustHalfLifeForEmotion(60, null)).toBe(60);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test("strong negative emotion extends half-life ~27%", () => {
|
|
285
|
-
const adjusted = adjustHalfLifeForEmotion(60, { valence: -0.9, arousal: 0.5, label: "frustration" });
|
|
286
|
-
expect(adjusted).toBeCloseTo(60 * 1.27, 0);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
test("strong positive emotion extends half-life ~24%", () => {
|
|
290
|
-
const adjusted = adjustHalfLifeForEmotion(60, { valence: 0.8, arousal: 0.3, label: "excitement" });
|
|
291
|
-
expect(adjusted).toBeCloseTo(60 * 1.24, 0);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test("neutral emotion has negligible effect", () => {
|
|
295
|
-
const adjusted = adjustHalfLifeForEmotion(60, { valence: 0.05, arousal: 0.1, label: "neutral" });
|
|
296
|
-
expect(adjusted).toBeCloseTo(60, 0);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
test("emotional memory decays slower at 30 days", () => {
|
|
300
|
-
const baseHL = 60;
|
|
301
|
-
const emotionalHL = adjustHalfLifeForEmotion(baseHL, { valence: -0.8, arousal: 0.7, label: "frustration" });
|
|
302
|
-
const neutralDecay = weibullDecay(30, baseHL, "working");
|
|
303
|
-
const emotionalDecay = weibullDecay(30, emotionalHL, "working");
|
|
304
|
-
expect(emotionalDecay).toBeGreaterThan(neutralDecay);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test("arousal boost is 1.0 for zero arousal", () => {
|
|
308
|
-
expect(computeArousalBoost({ valence: 0.5, arousal: 0, label: "positive" })).toBe(1.0);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
test("arousal boost max ~1.1 for high arousal", () => {
|
|
312
|
-
const boost = computeArousalBoost({ valence: 0.5, arousal: 0.9, label: "excitement" });
|
|
313
|
-
expect(boost).toBeCloseTo(1.09, 1);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
test("arousal boost is 1.0 for null emotion", () => {
|
|
317
|
-
expect(computeArousalBoost(undefined)).toBe(1.0);
|
|
318
|
-
expect(computeArousalBoost(null)).toBe(1.0);
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
- [ ] **Step 2: Run tests to verify they fail**
|
|
324
|
-
|
|
325
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test src/__tests__/emotion-decay.test.ts`
|
|
326
|
-
Expected: FAIL.
|
|
327
|
-
|
|
328
|
-
- [ ] **Step 3: Implement in decay-engine.ts**
|
|
329
|
-
|
|
330
|
-
Open `src/decay-engine.ts`. Add import at top:
|
|
331
|
-
|
|
332
|
-
```typescript
|
|
333
|
-
import type { EmotionMetadata } from "./memory-schema.js";
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
After the `weibullDecay` function (after line 87), add:
|
|
337
|
-
|
|
338
|
-
```typescript
|
|
339
|
-
/**
|
|
340
|
-
* Adjust half-life based on emotional intensity.
|
|
341
|
-
* Strong emotion extends half-life by up to 30%.
|
|
342
|
-
*/
|
|
343
|
-
export function adjustHalfLifeForEmotion(
|
|
344
|
-
baseHalfLife: number,
|
|
345
|
-
emotion: EmotionMetadata | null | undefined,
|
|
346
|
-
): number {
|
|
347
|
-
if (!emotion) return baseHalfLife;
|
|
348
|
-
const intensity = Math.abs(emotion.valence);
|
|
349
|
-
return baseHalfLife * (1 + 0.3 * intensity);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Compute initial strength boost from arousal (flashbulb memory effect).
|
|
354
|
-
* Returns multiplier in [1.0, 1.1].
|
|
355
|
-
*/
|
|
356
|
-
export function computeArousalBoost(
|
|
357
|
-
emotion: EmotionMetadata | null | undefined,
|
|
358
|
-
): number {
|
|
359
|
-
if (!emotion) return 1.0;
|
|
360
|
-
return 1 + 0.1 * emotion.arousal;
|
|
361
|
-
}
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
- [ ] **Step 4: Run tests to verify they pass**
|
|
365
|
-
|
|
366
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test src/__tests__/emotion-decay.test.ts`
|
|
367
|
-
Expected: All 8 tests PASS.
|
|
368
|
-
|
|
369
|
-
- [ ] **Step 5: Run full test suite**
|
|
370
|
-
|
|
371
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test`
|
|
372
|
-
Expected: All pass.
|
|
373
|
-
|
|
374
|
-
- [ ] **Step 6: Commit**
|
|
375
|
-
|
|
376
|
-
```bash
|
|
377
|
-
cd /Users/anxianjingya/Projects/recallnest
|
|
378
|
-
git add src/decay-engine.ts src/__tests__/emotion-decay.test.ts
|
|
379
|
-
git commit -m "feat: add emotion-adjusted half-life and arousal boost to decay engine"
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
---
|
|
383
|
-
|
|
384
|
-
### Task 4: Emotion Scoring Stage in Retriever
|
|
385
|
-
|
|
386
|
-
**Files:**
|
|
387
|
-
- Modify: `src/retriever.ts`
|
|
388
|
-
- Create: `src/__tests__/emotion-retrieval.test.ts`
|
|
389
|
-
|
|
390
|
-
- [ ] **Step 1: Write the failing tests**
|
|
391
|
-
|
|
392
|
-
Create `src/__tests__/emotion-retrieval.test.ts`:
|
|
393
|
-
|
|
394
|
-
```typescript
|
|
395
|
-
import { describe, test, expect } from "bun:test";
|
|
396
|
-
import { applyEmotionWeight } from "../retriever.js";
|
|
397
|
-
|
|
398
|
-
describe("applyEmotionWeight", () => {
|
|
399
|
-
const makeResult = (id: string, score: number, emotionJson?: object) => ({
|
|
400
|
-
id,
|
|
401
|
-
text: "test",
|
|
402
|
-
vector: [],
|
|
403
|
-
category: "events" as const,
|
|
404
|
-
scope: "test",
|
|
405
|
-
importance: 0.5,
|
|
406
|
-
timestamp: Date.now(),
|
|
407
|
-
metadata: emotionJson ? JSON.stringify({ emotion: emotionJson }) : "{}",
|
|
408
|
-
score,
|
|
409
|
-
sources: {},
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
test("null query emotion returns unchanged", () => {
|
|
413
|
-
const results = [makeResult("a", 0.8, { valence: 0.9, arousal: 0.5 })];
|
|
414
|
-
const output = applyEmotionWeight(results, null);
|
|
415
|
-
expect(output[0].score).toBe(0.8);
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
test("low-valence query returns unchanged", () => {
|
|
419
|
-
const queryEmotion = { valence: 0.1, arousal: 0, label: "neutral" };
|
|
420
|
-
const results = [makeResult("a", 0.8, { valence: 0.9, arousal: 0.5 })];
|
|
421
|
-
const output = applyEmotionWeight(results, queryEmotion);
|
|
422
|
-
expect(output[0].score).toBe(0.8);
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
test("negative query boosts negative memories", () => {
|
|
426
|
-
const queryEmotion = { valence: -0.8, arousal: 0.5, label: "frustration" };
|
|
427
|
-
const results = [
|
|
428
|
-
makeResult("neg", 0.7, { valence: -0.7, arousal: 0.5 }),
|
|
429
|
-
makeResult("pos", 0.7, { valence: 0.8, arousal: 0.3 }),
|
|
430
|
-
];
|
|
431
|
-
const output = applyEmotionWeight(results, queryEmotion);
|
|
432
|
-
expect(output.find(r => r.id === "neg")!.score).toBeGreaterThan(
|
|
433
|
-
output.find(r => r.id === "pos")!.score
|
|
434
|
-
);
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
test("no emotion data leaves score unchanged", () => {
|
|
438
|
-
const queryEmotion = { valence: -0.8, arousal: 0.5, label: "negative" };
|
|
439
|
-
const results = [makeResult("no-emo", 0.7)];
|
|
440
|
-
const output = applyEmotionWeight(results, queryEmotion);
|
|
441
|
-
expect(output[0].score).toBe(0.7);
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
test("max boost is 15%", () => {
|
|
445
|
-
const queryEmotion = { valence: 1.0, arousal: 1.0, label: "positive" };
|
|
446
|
-
const results = [makeResult("a", 1.0, { valence: 1.0, arousal: 1.0 })];
|
|
447
|
-
const output = applyEmotionWeight(results, queryEmotion);
|
|
448
|
-
expect(output[0].score).toBeCloseTo(1.15, 2);
|
|
449
|
-
});
|
|
450
|
-
});
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
- [ ] **Step 2: Run tests to verify they fail**
|
|
454
|
-
|
|
455
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test src/__tests__/emotion-retrieval.test.ts`
|
|
456
|
-
Expected: FAIL.
|
|
457
|
-
|
|
458
|
-
- [ ] **Step 3: Add applyEmotionWeight to retriever.ts**
|
|
459
|
-
|
|
460
|
-
Add imports at top of `src/retriever.ts`:
|
|
461
|
-
|
|
462
|
-
```typescript
|
|
463
|
-
import { parseEmotion, isEmotionScoringEnabled } from "./memory-schema.js";
|
|
464
|
-
import type { EmotionMetadata } from "./memory-schema.js";
|
|
465
|
-
import { detectEmotion } from "./emotion-detector.js";
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
Add the exported function near other `apply*Weight` functions:
|
|
469
|
-
|
|
470
|
-
```typescript
|
|
471
|
-
/**
|
|
472
|
-
* Emotion scoring stage: boost memories matching query emotional tone.
|
|
473
|
-
* No-op when query is neutral or memory lacks emotion data.
|
|
474
|
-
*/
|
|
475
|
-
export function applyEmotionWeight(
|
|
476
|
-
results: Array<{ score: number; metadata: string; [k: string]: unknown }>,
|
|
477
|
-
queryEmotion: EmotionMetadata | null,
|
|
478
|
-
): typeof results {
|
|
479
|
-
if (!queryEmotion || Math.abs(queryEmotion.valence) < 0.2) {
|
|
480
|
-
return results;
|
|
481
|
-
}
|
|
482
|
-
return results.map(r => {
|
|
483
|
-
const memEmotion = parseEmotion(r.metadata);
|
|
484
|
-
if (!memEmotion) return r;
|
|
485
|
-
const alignment = 1 - Math.abs(queryEmotion.valence - memEmotion.valence) / 2;
|
|
486
|
-
const boost = 1.0 + 0.15 * alignment;
|
|
487
|
-
return { ...r, score: r.score * boost };
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
Integrate into `retrieve()` pipeline after importance weight:
|
|
493
|
-
|
|
494
|
-
```typescript
|
|
495
|
-
if (isEmotionScoringEnabled()) {
|
|
496
|
-
const queryEmotion = detectEmotion(context.query);
|
|
497
|
-
scored = applyEmotionWeight(scored, queryEmotion);
|
|
498
|
-
}
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
- [ ] **Step 4: Run tests**
|
|
502
|
-
|
|
503
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test src/__tests__/emotion-retrieval.test.ts`
|
|
504
|
-
Expected: All 5 PASS.
|
|
505
|
-
|
|
506
|
-
- [ ] **Step 5: Full suite**
|
|
507
|
-
|
|
508
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test`
|
|
509
|
-
Expected: All pass.
|
|
510
|
-
|
|
511
|
-
- [ ] **Step 6: Commit**
|
|
512
|
-
|
|
513
|
-
```bash
|
|
514
|
-
cd /Users/anxianjingya/Projects/recallnest
|
|
515
|
-
git add src/retriever.ts src/__tests__/emotion-retrieval.test.ts
|
|
516
|
-
git commit -m "feat: add emotion scoring stage to retrieval pipeline"
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
---
|
|
520
|
-
|
|
521
|
-
### Task 5: Store Path Integration
|
|
522
|
-
|
|
523
|
-
**Files:**
|
|
524
|
-
- Modify: `src/store.ts` (store method ~line 343, storeBatch ~line 369)
|
|
525
|
-
|
|
526
|
-
- [ ] **Step 1: Add import**
|
|
527
|
-
|
|
528
|
-
At top of `src/store.ts`:
|
|
529
|
-
|
|
530
|
-
```typescript
|
|
531
|
-
import { detectEmotionIfEnabled } from "./emotion-detector.js";
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
- [ ] **Step 2: Inject emotion in store() method**
|
|
535
|
-
|
|
536
|
-
In `store()` (line 343), after metadata is set (line 350), add:
|
|
537
|
-
|
|
538
|
-
```typescript
|
|
539
|
-
const emotionResult = detectEmotionIfEnabled(entry.text);
|
|
540
|
-
if (emotionResult) {
|
|
541
|
-
const existingMeta = JSON.parse(fullEntry.metadata || "{}");
|
|
542
|
-
existingMeta.emotion = emotionResult;
|
|
543
|
-
fullEntry.metadata = JSON.stringify(existingMeta);
|
|
544
|
-
}
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
- [ ] **Step 3: Inject emotion in storeBatch() method**
|
|
548
|
-
|
|
549
|
-
In `storeBatch()` (line 369), before the table add call, map entries:
|
|
550
|
-
|
|
551
|
-
```typescript
|
|
552
|
-
const enrichedEntries = fullEntries.map(e => {
|
|
553
|
-
const emotionResult = detectEmotionIfEnabled(e.text);
|
|
554
|
-
if (emotionResult) {
|
|
555
|
-
const meta = JSON.parse(e.metadata || "{}");
|
|
556
|
-
meta.emotion = emotionResult;
|
|
557
|
-
return { ...e, metadata: JSON.stringify(meta) };
|
|
558
|
-
}
|
|
559
|
-
return e;
|
|
560
|
-
});
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
Use `enrichedEntries` in the table add.
|
|
564
|
-
|
|
565
|
-
- [ ] **Step 4: Run full test suite**
|
|
566
|
-
|
|
567
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test`
|
|
568
|
-
Expected: All pass (emotion is no-op when flag off).
|
|
569
|
-
|
|
570
|
-
- [ ] **Step 5: Verify with flag ON**
|
|
571
|
-
|
|
572
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && RECALLNEST_EMOTION_SCORING=true bun test`
|
|
573
|
-
Expected: All pass.
|
|
574
|
-
|
|
575
|
-
- [ ] **Step 6: Commit**
|
|
576
|
-
|
|
577
|
-
```bash
|
|
578
|
-
cd /Users/anxianjingya/Projects/recallnest
|
|
579
|
-
git add src/store.ts
|
|
580
|
-
git commit -m "feat: inject emotion metadata at store time"
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
---
|
|
584
|
-
|
|
585
|
-
### Task 6: LLM Emotion Piggyback (Optional)
|
|
586
|
-
|
|
587
|
-
**Files:**
|
|
588
|
-
- Modify: `src/llm-client.ts` (SmartExtraction interface ~line 128, smartExtract ~line 222)
|
|
589
|
-
|
|
590
|
-
- [ ] **Step 1: Extend SmartExtraction interface**
|
|
591
|
-
|
|
592
|
-
Add to `SmartExtraction` interface (line 128-138):
|
|
593
|
-
|
|
594
|
-
```typescript
|
|
595
|
-
emotion?: {
|
|
596
|
-
valence: number;
|
|
597
|
-
arousal: number;
|
|
598
|
-
label?: string;
|
|
599
|
-
};
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
- [ ] **Step 2: Extend smartExtract system prompt**
|
|
603
|
-
|
|
604
|
-
Append to the system prompt in smartExtract:
|
|
605
|
-
|
|
606
|
-
```
|
|
607
|
-
Also rate emotional tone:
|
|
608
|
-
- emotion.valence: [-1, 1] (negative to positive)
|
|
609
|
-
- emotion.arousal: [0, 1] (calm to excited)
|
|
610
|
-
- emotion.label: one word (e.g., "frustration", "neutral")
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
- [ ] **Step 3: Parse emotion from response**
|
|
614
|
-
|
|
615
|
-
In the JSON parsing section of smartExtract, add:
|
|
616
|
-
|
|
617
|
-
```typescript
|
|
618
|
-
if (isEmotionScoringEnabled() && parsed.emotion) {
|
|
619
|
-
result.emotion = {
|
|
620
|
-
valence: Math.max(-1, Math.min(1, parsed.emotion.valence ?? 0)),
|
|
621
|
-
arousal: Math.max(0, Math.min(1, parsed.emotion.arousal ?? 0)),
|
|
622
|
-
label: parsed.emotion.label,
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
Add import: `import { isEmotionScoringEnabled } from "./memory-schema.js";`
|
|
628
|
-
|
|
629
|
-
- [ ] **Step 4: Full test suite**
|
|
630
|
-
|
|
631
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test`
|
|
632
|
-
Expected: All pass.
|
|
633
|
-
|
|
634
|
-
- [ ] **Step 5: Commit**
|
|
635
|
-
|
|
636
|
-
```bash
|
|
637
|
-
cd /Users/anxianjingya/Projects/recallnest
|
|
638
|
-
git add src/llm-client.ts
|
|
639
|
-
git commit -m "feat: piggyback emotion detection on LLM smartExtract"
|
|
640
|
-
```
|
|
641
|
-
|
|
642
|
-
---
|
|
643
|
-
|
|
644
|
-
## Phase 2: Constructive Retrieval (Tasks 7-10)
|
|
645
|
-
|
|
646
|
-
### Task 7: Context Reconstructor Core
|
|
647
|
-
|
|
648
|
-
**Files:**
|
|
649
|
-
- Create: `src/context-reconstructor.ts`
|
|
650
|
-
- Create: `src/__tests__/context-reconstructor.test.ts`
|
|
651
|
-
|
|
652
|
-
- [ ] **Step 1: Write the failing tests**
|
|
653
|
-
|
|
654
|
-
Create `src/__tests__/context-reconstructor.test.ts`:
|
|
655
|
-
|
|
656
|
-
```typescript
|
|
657
|
-
import { describe, test, expect } from "bun:test";
|
|
658
|
-
import {
|
|
659
|
-
shouldReconstruct,
|
|
660
|
-
extractCitedIds,
|
|
661
|
-
removeSentencesWithId,
|
|
662
|
-
computeCoverage,
|
|
663
|
-
} from "../context-reconstructor.js";
|
|
664
|
-
|
|
665
|
-
describe("reconstruction gate", () => {
|
|
666
|
-
test("false when flag off", () => {
|
|
667
|
-
expect(shouldReconstruct({ flagEnabled: false, callerOptIn: true, resultCount: 5, llmAvailable: true })).toBe(false);
|
|
668
|
-
});
|
|
669
|
-
test("false when no opt-in", () => {
|
|
670
|
-
expect(shouldReconstruct({ flagEnabled: true, callerOptIn: false, resultCount: 5, llmAvailable: true })).toBe(false);
|
|
671
|
-
});
|
|
672
|
-
test("false when too few results", () => {
|
|
673
|
-
expect(shouldReconstruct({ flagEnabled: true, callerOptIn: true, resultCount: 2, llmAvailable: true })).toBe(false);
|
|
674
|
-
});
|
|
675
|
-
test("false when LLM down", () => {
|
|
676
|
-
expect(shouldReconstruct({ flagEnabled: true, callerOptIn: true, resultCount: 5, llmAvailable: false })).toBe(false);
|
|
677
|
-
});
|
|
678
|
-
test("true when all conditions met", () => {
|
|
679
|
-
expect(shouldReconstruct({ flagEnabled: true, callerOptIn: true, resultCount: 5, llmAvailable: true })).toBe(true);
|
|
680
|
-
});
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
describe("extractCitedIds", () => {
|
|
684
|
-
test("extracts [src:ID] references", () => {
|
|
685
|
-
expect(extractCitedIds("A [src:abc]. B [src:def].")).toEqual(["abc", "def"]);
|
|
686
|
-
});
|
|
687
|
-
test("empty for no citations", () => {
|
|
688
|
-
expect(extractCitedIds("No refs.")).toEqual([]);
|
|
689
|
-
});
|
|
690
|
-
test("deduplicates", () => {
|
|
691
|
-
expect(extractCitedIds("A [src:abc]. B [src:abc].")).toEqual(["abc"]);
|
|
692
|
-
});
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
describe("removeSentencesWithId", () => {
|
|
696
|
-
test("removes sentence with invalid ID", () => {
|
|
697
|
-
const text = "Valid [src:real]. Fake [src:bad]. Also valid [src:ok].";
|
|
698
|
-
const result = removeSentencesWithId(text, "bad");
|
|
699
|
-
expect(result).toContain("[src:real]");
|
|
700
|
-
expect(result).toContain("[src:ok]");
|
|
701
|
-
expect(result).not.toContain("[src:bad]");
|
|
702
|
-
});
|
|
703
|
-
test("unchanged if ID not found", () => {
|
|
704
|
-
const text = "All valid [src:a].";
|
|
705
|
-
expect(removeSentencesWithId(text, "nope")).toBe(text);
|
|
706
|
-
});
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
describe("computeCoverage", () => {
|
|
710
|
-
test("high for identical text", () => {
|
|
711
|
-
expect(computeCoverage("user prefers dark mode", ["user prefers dark mode"])).toBeGreaterThan(0.8);
|
|
712
|
-
});
|
|
713
|
-
test("low for unrelated text", () => {
|
|
714
|
-
expect(computeCoverage("quantum physics wave function", ["user prefers dark mode"])).toBeLessThan(0.3);
|
|
715
|
-
});
|
|
716
|
-
test("partial for mixed", () => {
|
|
717
|
-
const cov = computeCoverage("User likes TypeScript. Weather is sunny.", ["User likes TypeScript", "User uses Bun"]);
|
|
718
|
-
expect(cov).toBeGreaterThan(0.3);
|
|
719
|
-
expect(cov).toBeLessThan(0.9);
|
|
720
|
-
});
|
|
721
|
-
});
|
|
722
|
-
```
|
|
723
|
-
|
|
724
|
-
- [ ] **Step 2: Run tests to fail**
|
|
725
|
-
|
|
726
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test src/__tests__/context-reconstructor.test.ts`
|
|
727
|
-
Expected: FAIL.
|
|
728
|
-
|
|
729
|
-
- [ ] **Step 3: Implement context-reconstructor.ts**
|
|
730
|
-
|
|
731
|
-
Create `src/context-reconstructor.ts`:
|
|
732
|
-
|
|
733
|
-
```typescript
|
|
734
|
-
import type { RetrievalResult } from "./retriever.js";
|
|
735
|
-
import type { LLMClient } from "./llm-client.js";
|
|
736
|
-
|
|
737
|
-
// --- Types ---
|
|
738
|
-
|
|
739
|
-
export interface ReconstructionInput {
|
|
740
|
-
query: string;
|
|
741
|
-
results: RetrievalResult[];
|
|
742
|
-
mode: "resume" | "search";
|
|
743
|
-
maxTokens?: number;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
export interface ReconstructionOutput {
|
|
747
|
-
reconstructed: string | null;
|
|
748
|
-
sources: string[];
|
|
749
|
-
confidence: number;
|
|
750
|
-
fallbackReason?: string;
|
|
751
|
-
raw: RetrievalResult[];
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
export interface GateConditions {
|
|
755
|
-
flagEnabled: boolean;
|
|
756
|
-
callerOptIn: boolean;
|
|
757
|
-
resultCount: number;
|
|
758
|
-
llmAvailable: boolean;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
// --- Gate ---
|
|
762
|
-
|
|
763
|
-
export function shouldReconstruct(c: GateConditions): boolean {
|
|
764
|
-
return c.flagEnabled && c.callerOptIn && c.resultCount >= 3 && c.llmAvailable;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// --- Grounding Utilities ---
|
|
768
|
-
|
|
769
|
-
export function extractCitedIds(text: string): string[] {
|
|
770
|
-
const ids = new Set<string>();
|
|
771
|
-
const regex = /\[src:([^\]]+)\]/g;
|
|
772
|
-
let match: RegExpExecArray | null;
|
|
773
|
-
while ((match = regex.exec(text)) !== null) {
|
|
774
|
-
ids.add(match[1]);
|
|
775
|
-
}
|
|
776
|
-
return [...ids];
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
export function removeSentencesWithId(text: string, id: string): string {
|
|
780
|
-
const sentences = text.split(/(?<=[.!?])\s+/);
|
|
781
|
-
return sentences.filter(s => !s.includes(`[src:${id}]`)).join(" ");
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
/**
|
|
785
|
-
* Word-overlap coverage: fraction of reconstructed sentences grounded in sources.
|
|
786
|
-
* Lightweight heuristic, no embedding calls.
|
|
787
|
-
*/
|
|
788
|
-
export function computeCoverage(reconstructed: string, sourceTexts: string[]): number {
|
|
789
|
-
const sentences = reconstructed.split(/(?<=[.!?])\s+/).filter(s => s.length > 5);
|
|
790
|
-
if (sentences.length === 0) return 0;
|
|
791
|
-
|
|
792
|
-
const sourceWords = new Set<string>();
|
|
793
|
-
for (const src of sourceTexts) {
|
|
794
|
-
for (const w of src.toLowerCase().split(/\s+/)) {
|
|
795
|
-
if (w.length > 2) sourceWords.add(w);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
let covered = 0;
|
|
800
|
-
for (const sent of sentences) {
|
|
801
|
-
const words = sent.toLowerCase().split(/\s+/).filter(w => w.length > 2);
|
|
802
|
-
if (words.length === 0) continue;
|
|
803
|
-
const overlap = words.filter(w => sourceWords.has(w)).length / words.length;
|
|
804
|
-
if (overlap > 0.3) covered++;
|
|
805
|
-
}
|
|
806
|
-
return covered / sentences.length;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
// --- Prompt ---
|
|
810
|
-
|
|
811
|
-
function buildPrompt(input: ReconstructionInput): { system: string; user: string } {
|
|
812
|
-
const modeHint = input.mode === "resume"
|
|
813
|
-
? "Focus on what the user was working on, key decisions, and pending actions."
|
|
814
|
-
: "Focus on the most relevant facts for the query.";
|
|
815
|
-
|
|
816
|
-
const system = `You are a memory reconstruction engine. Synthesize a coherent summary from stored memories.
|
|
817
|
-
|
|
818
|
-
${modeHint}
|
|
819
|
-
|
|
820
|
-
Rules:
|
|
821
|
-
1. Every claim MUST cite [src:MEMORY_ID]
|
|
822
|
-
2. Do NOT invent facts not in source memories
|
|
823
|
-
3. Contradictions: present both with [conflict]
|
|
824
|
-
4. Keep under ${input.maxTokens ?? 500} tokens`;
|
|
825
|
-
|
|
826
|
-
const block = input.results.slice(0, 10).map(r =>
|
|
827
|
-
`[ID: ${r.id}] ${r.text} (importance: ${r.importance})`
|
|
828
|
-
).join("\n\n");
|
|
829
|
-
|
|
830
|
-
return { system, user: `Context: ${input.query}\n\nMemories:\n${block}\n\nReconstruct:` };
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// --- Main Pipeline ---
|
|
834
|
-
|
|
835
|
-
const TIMEOUT_MS = 3000;
|
|
836
|
-
const COVERAGE_FLOOR = 0.6;
|
|
837
|
-
|
|
838
|
-
export async function reconstruct(
|
|
839
|
-
input: ReconstructionInput,
|
|
840
|
-
llmClient: LLMClient,
|
|
841
|
-
): Promise<ReconstructionOutput> {
|
|
842
|
-
const raw = input.results;
|
|
843
|
-
|
|
844
|
-
const timeout = new Promise<ReconstructionOutput>(resolve =>
|
|
845
|
-
setTimeout(() => resolve({
|
|
846
|
-
reconstructed: null, sources: [], confidence: 0, fallbackReason: "timeout", raw,
|
|
847
|
-
}), TIMEOUT_MS)
|
|
848
|
-
);
|
|
849
|
-
|
|
850
|
-
const work = (async (): Promise<ReconstructionOutput> => {
|
|
851
|
-
const { system, user } = buildPrompt(input);
|
|
852
|
-
const response = await llmClient.generateReconstruction(system, user);
|
|
853
|
-
|
|
854
|
-
if (!response) {
|
|
855
|
-
return { reconstructed: null, sources: [], confidence: 0, fallbackReason: "llm_empty", raw };
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
let text = response;
|
|
859
|
-
let confidence = 1.0;
|
|
860
|
-
|
|
861
|
-
// Layer 1: ID verification
|
|
862
|
-
const validIds = new Set(raw.map(r => r.id));
|
|
863
|
-
for (const id of extractCitedIds(text)) {
|
|
864
|
-
if (!validIds.has(id)) {
|
|
865
|
-
text = removeSentencesWithId(text, id);
|
|
866
|
-
confidence -= 0.2;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
confidence = Math.max(0, confidence);
|
|
870
|
-
|
|
871
|
-
// Layer 2: Coverage
|
|
872
|
-
const coverage = computeCoverage(text, raw.map(r => r.text));
|
|
873
|
-
if (coverage < COVERAGE_FLOOR) {
|
|
874
|
-
return { reconstructed: null, sources: [], confidence: 0, fallbackReason: "low_grounding", raw };
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
const validSources = extractCitedIds(text).filter(id => validIds.has(id));
|
|
878
|
-
return {
|
|
879
|
-
reconstructed: text,
|
|
880
|
-
sources: validSources,
|
|
881
|
-
confidence: Math.min(confidence, coverage),
|
|
882
|
-
raw,
|
|
883
|
-
};
|
|
884
|
-
})();
|
|
885
|
-
|
|
886
|
-
return Promise.race([work, timeout]);
|
|
887
|
-
}
|
|
888
|
-
```
|
|
889
|
-
|
|
890
|
-
- [ ] **Step 4: Run tests**
|
|
891
|
-
|
|
892
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test src/__tests__/context-reconstructor.test.ts`
|
|
893
|
-
Expected: All 12 PASS.
|
|
894
|
-
|
|
895
|
-
- [ ] **Step 5: Full suite**
|
|
896
|
-
|
|
897
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test`
|
|
898
|
-
Expected: All pass.
|
|
899
|
-
|
|
900
|
-
- [ ] **Step 6: Commit**
|
|
901
|
-
|
|
902
|
-
```bash
|
|
903
|
-
cd /Users/anxianjingya/Projects/recallnest
|
|
904
|
-
git add src/context-reconstructor.ts src/__tests__/context-reconstructor.test.ts
|
|
905
|
-
git commit -m "feat: context reconstructor with grounding validator and triple fallback"
|
|
906
|
-
```
|
|
907
|
-
|
|
908
|
-
---
|
|
909
|
-
|
|
910
|
-
### Task 8: LLM Client Extension
|
|
911
|
-
|
|
912
|
-
**Files:**
|
|
913
|
-
- Modify: `src/llm-client.ts` (add public methods)
|
|
914
|
-
|
|
915
|
-
- [ ] **Step 1: Add generateReconstruction method**
|
|
916
|
-
|
|
917
|
-
In `src/llm-client.ts`, after the existing public methods, add:
|
|
918
|
-
|
|
919
|
-
```typescript
|
|
920
|
-
/** Public wrapper for reconstruction pipeline */
|
|
921
|
-
async generateReconstruction(system: string, user: string): Promise<string | null> {
|
|
922
|
-
return this.chat(system, user);
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
/** Check if LLM circuit breaker allows requests */
|
|
926
|
-
isAvailable(): boolean {
|
|
927
|
-
return !this.circuitBreaker.isOpen();
|
|
928
|
-
}
|
|
929
|
-
```
|
|
930
|
-
|
|
931
|
-
- [ ] **Step 2: Full test suite**
|
|
932
|
-
|
|
933
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test`
|
|
934
|
-
Expected: All pass.
|
|
935
|
-
|
|
936
|
-
- [ ] **Step 3: Commit**
|
|
937
|
-
|
|
938
|
-
```bash
|
|
939
|
-
cd /Users/anxianjingya/Projects/recallnest
|
|
940
|
-
git add src/llm-client.ts
|
|
941
|
-
git commit -m "feat: add generateReconstruction and isAvailable to LLMClient"
|
|
942
|
-
```
|
|
943
|
-
|
|
944
|
-
---
|
|
945
|
-
|
|
946
|
-
### Task 9: Retriever + Composer + MCP Integration
|
|
947
|
-
|
|
948
|
-
**Files:**
|
|
949
|
-
- Modify: `src/retriever.ts` (RetrievalContext + output, ~line 136-155, ~line 670)
|
|
950
|
-
- Modify: `src/context-composer.ts` (resume flow, ~line 445)
|
|
951
|
-
- Modify: `src/mcp-server.ts` (search_memory schema, ~line 889)
|
|
952
|
-
|
|
953
|
-
- [ ] **Step 1: Extend RetrievalContext in retriever.ts**
|
|
954
|
-
|
|
955
|
-
Add to `RetrievalContext` interface (line 136-155):
|
|
956
|
-
|
|
957
|
-
```typescript
|
|
958
|
-
/** Request constructive retrieval reconstruction */
|
|
959
|
-
reconstruct?: boolean;
|
|
960
|
-
```
|
|
961
|
-
|
|
962
|
-
- [ ] **Step 2: Add reconstruction gate to retrieve() output**
|
|
963
|
-
|
|
964
|
-
At end of `retrieve()` (around line 670), before final return, add:
|
|
965
|
-
|
|
966
|
-
```typescript
|
|
967
|
-
import { shouldReconstruct, reconstruct as runReconstruction, type ReconstructionOutput } from "./context-reconstructor.js";
|
|
968
|
-
|
|
969
|
-
// In retrieve(), before return:
|
|
970
|
-
const constructiveFlag = process.env.RECALLNEST_CONSTRUCTIVE_RETRIEVAL === "true";
|
|
971
|
-
if (shouldReconstruct({
|
|
972
|
-
flagEnabled: constructiveFlag,
|
|
973
|
-
callerOptIn: context.reconstruct === true,
|
|
974
|
-
resultCount: finalResults.length,
|
|
975
|
-
llmAvailable: this.llmClient?.isAvailable() ?? false,
|
|
976
|
-
})) {
|
|
977
|
-
try {
|
|
978
|
-
const reconstruction = await runReconstruction(
|
|
979
|
-
{ query: context.query, results: finalResults, mode: "search" },
|
|
980
|
-
this.llmClient!,
|
|
981
|
-
);
|
|
982
|
-
// Attach to first result's metadata as side-channel
|
|
983
|
-
// (preserves return type compatibility)
|
|
984
|
-
if (finalResults.length > 0) {
|
|
985
|
-
const meta = JSON.parse(finalResults[0].metadata || "{}");
|
|
986
|
-
meta._reconstruction = {
|
|
987
|
-
text: reconstruction.reconstructed,
|
|
988
|
-
sources: reconstruction.sources,
|
|
989
|
-
confidence: reconstruction.confidence,
|
|
990
|
-
fallbackReason: reconstruction.fallbackReason,
|
|
991
|
-
};
|
|
992
|
-
finalResults[0] = { ...finalResults[0], metadata: JSON.stringify(meta) };
|
|
993
|
-
}
|
|
994
|
-
} catch {
|
|
995
|
-
// Silent degradation
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
```
|
|
999
|
-
|
|
1000
|
-
- [ ] **Step 3: Add reconstruct param to search_memory in mcp-server.ts**
|
|
1001
|
-
|
|
1002
|
-
In `src/mcp-server.ts`, find search_memory input schema (line 889-906). Add:
|
|
1003
|
-
|
|
1004
|
-
```typescript
|
|
1005
|
-
reconstruct: z.boolean().default(false).describe(
|
|
1006
|
-
"Return LLM-synthesized reconstruction alongside raw results. Requires RECALLNEST_CONSTRUCTIVE_RETRIEVAL=true."
|
|
1007
|
-
),
|
|
1008
|
-
```
|
|
1009
|
-
|
|
1010
|
-
Pass it through in the handler:
|
|
1011
|
-
|
|
1012
|
-
```typescript
|
|
1013
|
-
reconstruct: input.reconstruct,
|
|
1014
|
-
```
|
|
1015
|
-
|
|
1016
|
-
In the response formatting, check for reconstruction data:
|
|
1017
|
-
|
|
1018
|
-
```typescript
|
|
1019
|
-
// After building the results sections
|
|
1020
|
-
const firstMeta = results[0] ? JSON.parse(results[0].metadata || "{}") : {};
|
|
1021
|
-
if (firstMeta._reconstruction?.text) {
|
|
1022
|
-
const r = firstMeta._reconstruction;
|
|
1023
|
-
sections.unshift(
|
|
1024
|
-
`## Reconstructed Context (confidence: ${r.confidence.toFixed(2)})\n${r.text}\n\nSources: ${r.sources.join(", ")}`
|
|
1025
|
-
);
|
|
1026
|
-
}
|
|
1027
|
-
```
|
|
1028
|
-
|
|
1029
|
-
- [ ] **Step 4: Integrate into context-composer.ts**
|
|
1030
|
-
|
|
1031
|
-
In `src/context-composer.ts`, in `composeResumeContext` (around line 445), add:
|
|
1032
|
-
|
|
1033
|
-
```typescript
|
|
1034
|
-
import { shouldReconstruct, reconstruct as runReconstruction } from "./context-reconstructor.js";
|
|
1035
|
-
|
|
1036
|
-
// After stable + patterns + cases assembly:
|
|
1037
|
-
const constructiveFlag = process.env.RECALLNEST_CONSTRUCTIVE_RETRIEVAL === "true";
|
|
1038
|
-
if (constructiveFlag && deps.llmClient?.isAvailable?.()) {
|
|
1039
|
-
const allResults = [...(stableResults || []), ...(patternResults || []), ...(caseResults || [])];
|
|
1040
|
-
if (allResults.length >= 3) {
|
|
1041
|
-
try {
|
|
1042
|
-
const taskQuery = latestCheckpoint?.summary ?? "general context";
|
|
1043
|
-
const recon = await runReconstruction(
|
|
1044
|
-
{ query: taskQuery, results: allResults, mode: "resume", maxTokens: 600 },
|
|
1045
|
-
deps.llmClient,
|
|
1046
|
-
);
|
|
1047
|
-
if (recon.reconstructed) {
|
|
1048
|
-
response.reconstructedContext = recon.reconstructed;
|
|
1049
|
-
response.reconstructionConfidence = recon.confidence;
|
|
1050
|
-
}
|
|
1051
|
-
} catch { /* silent fallback */ }
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
```
|
|
1055
|
-
|
|
1056
|
-
Add `reconstructedContext?: string` and `reconstructionConfidence?: number` to the response schema.
|
|
1057
|
-
|
|
1058
|
-
- [ ] **Step 5: Full test suite**
|
|
1059
|
-
|
|
1060
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test`
|
|
1061
|
-
Expected: All pass (feature flag off = no-op).
|
|
1062
|
-
|
|
1063
|
-
- [ ] **Step 6: Commit**
|
|
1064
|
-
|
|
1065
|
-
```bash
|
|
1066
|
-
cd /Users/anxianjingya/Projects/recallnest
|
|
1067
|
-
git add src/retriever.ts src/mcp-server.ts src/context-composer.ts
|
|
1068
|
-
git commit -m "feat: integrate constructive retrieval into search_memory and resume_context"
|
|
1069
|
-
```
|
|
1070
|
-
|
|
1071
|
-
---
|
|
1072
|
-
|
|
1073
|
-
### Task 10: Integration Tests + Docs + Ship
|
|
1074
|
-
|
|
1075
|
-
**Files:**
|
|
1076
|
-
- Create: `src/__tests__/feature-flags-integration.test.ts`
|
|
1077
|
-
- Modify: `CLAUDE.md` (feature flags section)
|
|
1078
|
-
|
|
1079
|
-
- [ ] **Step 1: Write integration tests**
|
|
1080
|
-
|
|
1081
|
-
Create `src/__tests__/feature-flags-integration.test.ts`:
|
|
1082
|
-
|
|
1083
|
-
```typescript
|
|
1084
|
-
import { describe, test, expect, afterAll } from "bun:test";
|
|
1085
|
-
|
|
1086
|
-
describe("feature flag isolation", () => {
|
|
1087
|
-
const saved = {
|
|
1088
|
-
emo: process.env.RECALLNEST_EMOTION_SCORING,
|
|
1089
|
-
con: process.env.RECALLNEST_CONSTRUCTIVE_RETRIEVAL,
|
|
1090
|
-
};
|
|
1091
|
-
|
|
1092
|
-
afterAll(() => {
|
|
1093
|
-
if (saved.emo !== undefined) process.env.RECALLNEST_EMOTION_SCORING = saved.emo;
|
|
1094
|
-
else delete process.env.RECALLNEST_EMOTION_SCORING;
|
|
1095
|
-
if (saved.con !== undefined) process.env.RECALLNEST_CONSTRUCTIVE_RETRIEVAL = saved.con;
|
|
1096
|
-
else delete process.env.RECALLNEST_CONSTRUCTIVE_RETRIEVAL;
|
|
1097
|
-
});
|
|
1098
|
-
|
|
1099
|
-
test("emotion off -> detectEmotionIfEnabled returns null", () => {
|
|
1100
|
-
delete process.env.RECALLNEST_EMOTION_SCORING;
|
|
1101
|
-
const { detectEmotionIfEnabled } = require("../emotion-detector.js");
|
|
1102
|
-
expect(detectEmotionIfEnabled("terrible bug")).toBeNull();
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
test("constructive off -> shouldReconstruct false", () => {
|
|
1106
|
-
const { shouldReconstruct } = require("../context-reconstructor.js");
|
|
1107
|
-
expect(shouldReconstruct({
|
|
1108
|
-
flagEnabled: false, callerOptIn: true, resultCount: 10, llmAvailable: true,
|
|
1109
|
-
})).toBe(false);
|
|
1110
|
-
});
|
|
1111
|
-
});
|
|
1112
|
-
|
|
1113
|
-
describe("emotion + decay end-to-end", () => {
|
|
1114
|
-
test("emotional memory retains more score at 30 days", () => {
|
|
1115
|
-
const { adjustHalfLifeForEmotion } = require("../decay-engine.js");
|
|
1116
|
-
const { weibullDecay } = require("../decay-engine.js");
|
|
1117
|
-
|
|
1118
|
-
const baseHL = 60;
|
|
1119
|
-
const emoHL = adjustHalfLifeForEmotion(baseHL, { valence: -0.8, arousal: 0.7, label: "frustration" });
|
|
1120
|
-
expect(weibullDecay(30, emoHL, "working")).toBeGreaterThan(weibullDecay(30, baseHL, "working"));
|
|
1121
|
-
});
|
|
1122
|
-
});
|
|
1123
|
-
|
|
1124
|
-
describe("grounding end-to-end", () => {
|
|
1125
|
-
test("removes phantom citations and lowers confidence", () => {
|
|
1126
|
-
const { extractCitedIds, removeSentencesWithId } = require("../context-reconstructor.js");
|
|
1127
|
-
|
|
1128
|
-
const text = "Valid [src:r1]. Fake [src:phantom]. Also valid [src:r2].";
|
|
1129
|
-
const validIds = new Set(["r1", "r2"]);
|
|
1130
|
-
let result = text;
|
|
1131
|
-
let confidence = 1.0;
|
|
1132
|
-
|
|
1133
|
-
for (const id of extractCitedIds(result)) {
|
|
1134
|
-
if (!validIds.has(id)) {
|
|
1135
|
-
result = removeSentencesWithId(result, id);
|
|
1136
|
-
confidence -= 0.2;
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
expect(result).not.toContain("phantom");
|
|
1141
|
-
expect(confidence).toBe(0.8);
|
|
1142
|
-
});
|
|
1143
|
-
});
|
|
1144
|
-
```
|
|
1145
|
-
|
|
1146
|
-
- [ ] **Step 2: Run integration tests**
|
|
1147
|
-
|
|
1148
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test src/__tests__/feature-flags-integration.test.ts`
|
|
1149
|
-
Expected: All PASS.
|
|
1150
|
-
|
|
1151
|
-
- [ ] **Step 3: Full suite flags OFF**
|
|
1152
|
-
|
|
1153
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && bun test`
|
|
1154
|
-
Expected: ALL pass.
|
|
1155
|
-
|
|
1156
|
-
- [ ] **Step 4: Full suite flags ON**
|
|
1157
|
-
|
|
1158
|
-
Run: `cd /Users/anxianjingya/Projects/recallnest && RECALLNEST_EMOTION_SCORING=true RECALLNEST_CONSTRUCTIVE_RETRIEVAL=true bun test`
|
|
1159
|
-
Expected: All pass.
|
|
1160
|
-
|
|
1161
|
-
- [ ] **Step 5: Update CLAUDE.md feature flags**
|
|
1162
|
-
|
|
1163
|
-
Add to section 5 in `CLAUDE.md`:
|
|
1164
|
-
|
|
1165
|
-
```markdown
|
|
1166
|
-
- `RECALLNEST_EMOTION_SCORING=true` — Emotion detection + adjusted decay + retrieval scoring
|
|
1167
|
-
- `RECALLNEST_CONSTRUCTIVE_RETRIEVAL=true` — LLM context reconstruction with grounding (resume default, search opt-in)
|
|
1168
|
-
```
|
|
1169
|
-
|
|
1170
|
-
- [ ] **Step 6: Commit + push**
|
|
1171
|
-
|
|
1172
|
-
```bash
|
|
1173
|
-
cd /Users/anxianjingya/Projects/recallnest
|
|
1174
|
-
git add src/__tests__/feature-flags-integration.test.ts CLAUDE.md
|
|
1175
|
-
git commit -m "feat: integration tests and feature flag documentation for v1.5"
|
|
1176
|
-
git push
|
|
1177
|
-
```
|
|
1178
|
-
|
|
1179
|
-
---
|
|
1180
|
-
|
|
1181
|
-
## Task Summary
|
|
1182
|
-
|
|
1183
|
-
| # | Phase | Component | Risk | Est. LOC |
|
|
1184
|
-
|---|-------|-----------|------|----------|
|
|
1185
|
-
| 1 | Emotion | Types + flag | None | ~40 |
|
|
1186
|
-
| 2 | Emotion | Heuristic detector | None | ~70+80 |
|
|
1187
|
-
| 3 | Emotion | Decay adjustment | Low | ~25+50 |
|
|
1188
|
-
| 4 | Emotion | Retrieval scoring | Low | ~25+50 |
|
|
1189
|
-
| 5 | Emotion | Store integration | Low | ~15 |
|
|
1190
|
-
| 6 | Emotion | LLM piggyback | Low | ~20 |
|
|
1191
|
-
| 7 | Constructive | Reconstructor core | Medium | ~180+80 |
|
|
1192
|
-
| 8 | Constructive | LLM client extension | Low | ~15 |
|
|
1193
|
-
| 9 | Constructive | Pipeline integration | Medium | ~60 |
|
|
1194
|
-
| 10 | Both | Integration tests + docs | None | ~80 |
|
|
1195
|
-
| | | **Total** | | **~790** |
|