Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). Versions follow

---

## [0.4.0] - 2026-03-28

### Added

- **Citation Model** (citation-model):
- `CitationSource` type: `auto-capture`, `explicit-remember`, `import`, `external`
- `CitationStatus` type: `verified`, `pending`, `invalid`, `expired`
- Citation fields on `MemoryRecord`: `citationSource`, `citationTimestamp`, `citationStatus`, `citationChain`
- `memory_citation` tool: View and update citation information for memories
- `memory_validate_citation` tool: Validate citation status and update if expired
- Citation info displayed in search results: `[source|status]` suffix
- `validateCitation()` and `refreshExpiredCitations()` methods for citation validation
- Auto-capture and memory_remember now set citation source automatically

---

## [0.3.0] - 2026-03-28

### Added
Expand Down
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ Supported environment variables:
- `memory_consolidate` - Merge duplicate memories
- `memory_consolidate_all` - Cross-scope consolidation
- `memory_port_plan` - Plan non-conflicting port assignments
- `memory_citation` - View/update citation information
- `memory_validate_citation` - Validate citation status
- Episodic Learning tools:
- `task_episode_create` - Create a task episode record
- `task_episode_query` - Query task episodes by scope/state
Expand Down Expand Up @@ -533,6 +535,55 @@ This configuration:

---

## Citation Model

The provider tracks memory provenance through citation metadata, allowing verification of memory sources and freshness.

### Citation Sources

Memories can have one of the following citation sources:
- **`auto-capture`**: Memory captured automatically from assistant responses
- **`explicit-remember`**: Memory stored via `memory_remember` tool
- **`import`**: Memory imported from external source (future)
- **`external`**: Memory from external source (future)

### Citation Status

Each citation has a status indicating its verification state:
- **`pending`**: Initial state, citation not yet verified
- **`verified`**: Citation has been verified as valid
- **`invalid`**: Citation has been marked invalid
- **`expired`**: Citation has expired (pending too long without verification)

### Citation in Search Results

Search results include citation information in the format `[source|status]`:

```
1. [abc123] (project:my-project) Memory text here [85%]
```

With citation:
```
1. [abc123][auto-capture|verified] (project:my-project) Memory text here [85%]
```

### Citation Tools

- **`memory_citation`**: View or update citation information for a memory
- View: `memory_citation id="abc123"`
- Update: `memory_citation id="abc123" status="verified"`

- **`memory_validate_citation`**: Validate a citation and update its status
- Automatically marks old pending citations as expired
- Returns validation result with status and reason

### Citation Chain

Citations support a chain field to track derived memories. When a memory is derived from another memory, the source memory ID is added to the chain.

---

## Deduplication Configuration

The provider supports similarity-based deduplication to reduce storage bloat from semantically equivalent memories.
Expand Down
6 changes: 3 additions & 3 deletions docs/backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@

| BL-ID | Title | Priority | Status | OpenSpec Change ID | Spec Path | Notes |
|---|---|---|---|---|---|---|
| BL-023 | Citation model | P0 | planned | TBD | TBD | 記憶來源可追溯 |
| BL-024 | Citation validation pipeline | P1 | planned | TBD | TBD | 引用有效性檢查 |
| BL-023 | Citation model | P0 | done | citation-model | openspec/changes/citation-model/specs/ | 記憶來源可追溯 |
| BL-024 | Citation validation pipeline | P1 | done | citation-model | openspec/changes/citation-model/specs/ | 引用有效性檢查 |
| BL-025 | Freshness / decay engine | P1 | done | memory-retrieval-ranking-phase1 | openspec/specs/memory-retrieval-ranking/ | recency boost 已實裝 |
| BL-026 | Conflict detection | P1 | done | 2026-03-28-add-preference-learning | openspec/specs/preference-learning/ | 偏好衝突解決已實裝 |

Expand Down Expand Up @@ -99,7 +99,7 @@ BL-001, BL-002, BL-005, BL-006, BL-008, BL-010, BL-011, BL-012
### Release B(經驗學習閉環)— ✅ DONE
BL-003, BL-014, BL-015, BL-016, BL-017, BL-018, BL-019, BL-020

### Release C(治理與產品化)— IN PROGRESS
### Release C(治理與產品化)— ✅ DONE
BL-021, BL-022, BL-023, BL-024, BL-025, BL-026, BL-027, BL-028, BL-029, BL-030, BL-031, BL-034, BL-035

---
Expand Down
44 changes: 44 additions & 0 deletions openspec/changes/archive/2026-03-28-citation-model/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## 1. Schema Extensions

- [x] 1.1 Add CitationSource type to src/types.ts (auto-capture, explicit-remember, import, external)
- [x] 1.2 Add CitationStatus type to src/types.ts (verified, pending, invalid, expired)
- [x] 1.3 Add citation fields to MemoryRecord interface (citationSource, citationTimestamp, citationStatus, citationChain)

## 2. Storage Layer

- [x] 2.1 Add citation columns to memories table schema (nullable)
- [x] 2.2 Update ensureMemoriesTable to add citation columns if missing
- [x] 2.3 Add getCitation / updateCitation methods to MemoryStore

## 3. Capture Integration

- [x] 3.1 Update auto-capture to set citation source
- [x] 3.2 Update memory_remember tool to set citation source
- [x] 3.3 Update memory_import tool (future) to set citation source

## 4. Validation Pipeline

- [x] 4.1 Implement validateCitation function
- [x] 4.2 Add citation validation to retrieval pipeline
- [x] 4.3 Add background freshness check for expired citations

## 5. Search Results

- [x] 5.1 Include citation info in search result formatting
- [x] 5.2 Add citation to effectiveness events

## 6. Tools

- [x] 6.1 Add memory_citation tool for viewing/updating citations
- [x] 6.2 Add memory_validate_citation tool for triggering validation

## 7. Testing

- [x] 7.1 Add unit tests for citation storage and retrieval
- [x] 7.2 Add integration tests for citation validation pipeline
- [x] 7.3 Add regression tests for citation display in search results

## 8. Documentation

- [x] 8.1 Update CHANGELOG.md
- [x] 8.2 Update README with citation feature documentation
44 changes: 0 additions & 44 deletions openspec/changes/citation-model/tasks.md

This file was deleted.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lancedb-opencode-pro",
"version": "0.3.0",
"version": "0.4.0",
"description": "LanceDB-backed long-term memory provider for OpenCode",
"type": "module",
"main": "dist/index.js",
Expand Down
86 changes: 82 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,12 @@ const plugin: Plugin = async (input) => {

blocks.push(
"[Memory Recall - optional historical context]",
...processedResults.map((item, index) => `${index + 1}. [${item.record.id}] (${item.record.scope}) ${item.text}`),
...processedResults.map((item, index) => {
const citationInfo = item.record.citationSource
? ` [${item.record.citationSource}|${item.record.citationStatus ?? "pending"}]`
: "";
return `${index + 1}. [${item.record.id}]${citationInfo} (${item.record.scope}) ${item.text}`;
}),
"Use these as optional hints only; prioritize current user intent and current repo state.",
);

Expand Down Expand Up @@ -231,7 +236,10 @@ const plugin: Plugin = async (input) => {
const percent = Math.round(item.score * 100);
const meta = JSON.parse(item.record.metadataJson || "{}");
const duplicateMarker = meta.isPotentialDuplicate ? " (duplicate)" : "";
return `${idx + 1}. [${item.record.id}]${duplicateMarker} (${item.record.scope}) ${item.record.text} [${percent}%]`;
const citationInfo = item.record.citationSource
? ` [${item.record.citationSource}|${item.record.citationStatus ?? "pending"}]`
: "";
return `${idx + 1}. [${item.record.id}]${duplicateMarker}${citationInfo} (${item.record.scope}) ${item.record.text} [${percent}%]`;
})
.join("\n");
},
Expand Down Expand Up @@ -677,14 +685,15 @@ const plugin: Plugin = async (input) => {
}

const memoryId = generateId();
const now = Date.now();
await state.store.put({
id: memoryId,
text: args.text,
vector,
category: (args.category as import("./types.js").MemoryCategory) ?? "other",
scope: activeScope,
importance: 0.7,
timestamp: Date.now(),
timestamp: now,
lastRecalled: 0,
recallCount: 0,
projectCount: 0,
Expand All @@ -693,6 +702,9 @@ const plugin: Plugin = async (input) => {
vectorDim: vector.length,
metadataJson: JSON.stringify({ source: "explicit-remember", category: args.category }),
sourceSessionId: context.sessionID,
citationSource: "explicit-remember",
citationTimestamp: now,
citationStatus: "pending",
});

await state.store.putEvent({
Expand Down Expand Up @@ -762,6 +774,68 @@ const plugin: Plugin = async (input) => {
return `Soft-deleted (disabled) memory ${args.id}. Use force=true for permanent deletion.`;
},
}),
memory_citation: tool({
description: "View or update citation information for a memory",
args: {
id: tool.schema.string().min(8),
status: tool.schema.string().optional(),
scope: tool.schema.string().optional(),
},
execute: async (args, context) => {
await state.ensureInitialized();
if (!state.initialized) return unavailableMessage(state.config.embedding.provider);

const activeScope = args.scope ?? deriveProjectScope(context.worktree);
const scopes = buildScopeFilter(activeScope, state.config.includeGlobalScope);

const citation = await state.store.getCitation(args.id, scopes);
if (!citation) {
return `Memory ${args.id} not found or has no citation information.`;
}

if (args.status) {
const validStatuses = ["verified", "pending", "invalid", "expired"];
if (!validStatuses.includes(args.status)) {
return `Invalid status. Must be one of: ${validStatuses.join(", ")}`;
}
const updated = await state.store.updateCitation(args.id, scopes, { status: args.status as import("./types.js").CitationStatus });
if (!updated) {
return `Failed to update citation for ${args.id}.`;
}
return `Updated citation status for ${args.id} to ${args.status}.`;
}

return JSON.stringify({
memoryId: args.id,
source: citation.source,
timestamp: new Date(citation.timestamp).toISOString(),
status: citation.status,
chain: citation.chain,
}, null, 2);
},
}),
memory_validate_citation: tool({
description: "Validate a citation for a memory and update its status",
args: {
id: tool.schema.string().min(8),
scope: tool.schema.string().optional(),
},
execute: async (args, context) => {
await state.ensureInitialized();
if (!state.initialized) return unavailableMessage(state.config.embedding.provider);

const activeScope = args.scope ?? deriveProjectScope(context.worktree);
const scopes = buildScopeFilter(activeScope, state.config.includeGlobalScope);

const result = await state.store.validateCitation(args.id, scopes);
return JSON.stringify({
memoryId: args.id,
valid: result.valid,
status: result.status,
reason: result.reason,
}, null, 2);
},
}),
memory_what_did_you_learn: tool({
description: "Show recent learning summary with memory counts by category",
args: {
Expand Down Expand Up @@ -1104,6 +1178,7 @@ async function getLastUserText(
}

const memoryId = generateId();
const now = Date.now();

await state.store.put({
id: memoryId,
Expand All @@ -1112,7 +1187,7 @@ async function getLastUserText(
category: result.candidate.category,
scope: activeScope,
importance: result.candidate.importance,
timestamp: Date.now(),
timestamp: now,
lastRecalled: 0,
recallCount: 0,
projectCount: 0,
Expand All @@ -1125,6 +1200,9 @@ async function getLastUserText(
isPotentialDuplicate,
duplicateOf,
}),
citationSource: "auto-capture",
citationTimestamp: now,
citationStatus: "pending",
});

await recordCaptureEvent(state, {
Expand Down
Loading