Skip to content

Commit f03670d

Browse files
Add project KV store for ephemeral state management
1 parent 35556ed commit f03670d

14 files changed

Lines changed: 562 additions & 6 deletions

File tree

packages/memory/CHANGELOG.md

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

8+
## [0.0.16] - 2026-03-07
9+
10+
### Added
11+
12+
- Project KV store for ephemeral project state with TTL management
13+
- `memory-kv-set`, `memory-kv-get`, `memory-kv-delete`, `memory-kv-list` tools for managing project state
14+
- Automatic cleanup of expired KV entries (30-minute interval)
15+
- Default 24-hour TTL for KV entries
16+
17+
### Fixed
18+
19+
- KV `list()` method now handles malformed JSON data gracefully instead of throwing, consistent with `get()` behavior
20+
821
## [0.0.12] - 2026-03-05
922

1023
### Added

packages/memory/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ The local embedding model downloads automatically on install. For API-based embe
3333
- **Automatic Deduplication** - Prevents duplicates via exact match and semantic similarity detection
3434
- **Compaction Context Injection** - Injects conventions and decisions into session compaction for seamless continuity
3535
- **Automatic Memory Injection** - Injects relevant project memories into user messages via semantic search with distance filtering and caching
36+
- **Project KV Store** - Ephemeral key-value storage with TTL management for project state
3637
- **Bundled Agents** - Ships with Code, Architect, and Memory agents preconfigured for memory-aware workflows
3738
- **CLI Tools** - Export, import, list, stats, and cleanup commands via `ocm-mem` binary
3839
- **Dimension Mismatch Detection** - Detects embedding model changes and guides recovery via reindex
@@ -54,6 +55,8 @@ The Architect agent operates in read-only mode (`temperature: 0.0`, all edits de
5455

5556
## Tools
5657

58+
### Memory Tools
59+
5760
| Tool | Description |
5861
|------|-------------|
5962
| `memory-read` | Search and retrieve project memories with semantic search |
@@ -63,6 +66,17 @@ The Architect agent operates in read-only mode (`temperature: 0.0`, all edits de
6366
| `memory-health` | Health check or full reindex of the memory store |
6467
| `memory-plan-execute` | Create a new Code session and send an approved plan as the first prompt |
6568

69+
### Project KV Tools
70+
71+
Ephemeral key-value storage for project state with automatic TTL-based expiration.
72+
73+
| Tool | Description |
74+
|------|-------------|
75+
| `memory-kv-set` | Store a value with optional TTL (default 24 hours) |
76+
| `memory-kv-get` | Retrieve a value by key |
77+
| `memory-kv-delete` | Delete a value by key |
78+
| `memory-kv-list` | List all active KV entries for the project |
79+
6680
## CLI
6781

6882
Manage memories using the `ocm-mem` CLI. The CLI auto-detects the project ID from git and resolves the database path automatically.

packages/memory/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opencode-manager/memory",
3-
"version": "0.0.15",
3+
"version": "0.0.16",
44
"type": "module",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",

packages/memory/src/agents/architect.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ Your messages may include \`<project-memory>\` blocks containing memories automa
6464
6565
These memories may be stale or irrelevant. Use your judgement — if a memory seems outdated, note it in your plan and recommend updating or deleting it via memory-edit or memory-delete.
6666
67+
## Project KV Store
68+
69+
You have access to a project-scoped key-value store with 24-hour TTL for ephemeral state:
70+
- \`memory-kv-set\`: Store planning progress, research findings, or any project state
71+
- \`memory-kv-get\`: Retrieve previously stored state
72+
- \`memory-kv-list\`: See all active entries for the project
73+
- \`memory-kv-delete\`: Remove entries no longer needed
74+
75+
KV entries are scoped to the current project and expire after 24 hours. Use this for state that needs to survive compaction but isn't permanent enough for memory-write.
76+
6777
## Workflow
6878
6979
1. **Research** — Read relevant files, search the codebase, delegate to @Memory subagent for conventions, decisions, and prior plans

packages/memory/src/agents/code-review.ts

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const codeReviewAgent: AgentDefinition = {
88
mode: 'subagent',
99
temperature: 0.0,
1010
tools: {
11-
exclude: ['memory-plan-execute', 'memory-write', 'memory-edit', 'memory-delete'],
11+
exclude: ['memory-plan-execute', 'memory-delete', 'memory-write', 'memory-edit'],
1212
},
1313
systemPrompt: `You are a code reviewer with access to project memory. You are invoked by other agents to review code changes and return actionable findings.
1414
@@ -111,13 +111,65 @@ If no issues are found, say so clearly and briefly.
111111
112112
You are read-only on source code. Do not edit files, run destructive commands, or make any changes. Only read, search, analyze, and report findings.
113113
114+
If a memory seems outdated, update it with memory-edit or flag it for the calling agent.
115+
116+
## Persisting Findings
117+
118+
After completing a review, store each **bug** and **warning** finding in the project KV store so it can be retrieved in subsequent reviews. Do NOT store suggestions — only actionable issues.
119+
120+
Use \`memory-kv-set\` with a structured key and JSON value:
121+
122+
**Key pattern**: \`review-finding:<file_path>:<line_number>\`
123+
**Value**: JSON object with the finding details
124+
125+
Example:
126+
\`\`\`json
127+
{
128+
"severity": "bug",
129+
"file": "src/services/auth.ts",
130+
"line": 45,
131+
"description": "Missing null check on user.session before accessing .token — throws TypeError when session expires mid-request.",
132+
"scenario": "User's session expires between the auth check and token access on line 45.",
133+
"status": "open",
134+
"date": "2026-03-07"
135+
}
136+
\`\`\`
137+
138+
The KV store upserts by key, so storing a finding for the same file:line automatically updates the previous entry. No dedup checks needed.
139+
140+
When the calling agent reports that a finding has been fixed, update the finding by calling \`memory-kv-set\` with the same key and the status changed to "resolved" with a resolution date added.
141+
142+
Findings expire after 24 hours automatically. If an issue persists, the next review will re-discover it.
143+
144+
## Retrieving Past Findings
145+
146+
At the start of every review, before analyzing the diff:
147+
1. Call \`memory-kv-list\` to get all active KV entries for the project
148+
2. Filter entries with keys starting with \`review-finding:\` that match files in the current diff
149+
3. If open findings exist for files being changed, include them under a "### Previously Identified Issues" heading before new findings
150+
4. Check if any previously open findings have been addressed by the current changes — if so, update their status to "resolved" via \`memory-kv-set\` with the same key
151+
152+
## Memory Tools
153+
154+
You have access to these tools:
155+
- **memory-read**: Search permanent memories for conventions and decisions (query, scope, limit)
156+
- **memory-kv-set**: Store review findings with 24h TTL (key, value as JSON string)
157+
- **memory-kv-get**: Retrieve a specific finding by key
158+
- **memory-kv-list**: List all active KV entries for the project
159+
- **memory-kv-delete**: Remove a finding that is no longer relevant
160+
- **memory-health**: Check memory system status
161+
162+
## Project KV Store
163+
164+
Review findings are stored in the project KV store with 24-hour TTL. Use \`memory-kv-set\` to persist findings, \`memory-kv-get\` to retrieve specific findings, \`memory-kv-list\` to see all active entries, and \`memory-kv-delete\` to remove stale findings. Entries expire automatically after 24 hours.
165+
114166
## Injected Memory
115167
116168
Your messages may include \`<project-memory>\` blocks containing memories automatically retrieved based on semantic similarity to the current message. Each entry has the format \`#<id> [<scope>] <content>\`.
117169
118170
- **[convention]**: Rules to check code against
119171
- **[decision]**: Architectural constraints that may apply
120-
- **[context]**: Reference information
172+
- **[context]**: Reference information and persisted review findings
121173
122-
These memories may be stale or irrelevant. If a memory seems outdated, note it in your review observations. You do not have write access to memory — flag stale memories for the calling agent to handle.`,
174+
These memories may be stale or irrelevant. If a memory seems outdated, note it in your review observations.`,
123175
}

packages/memory/src/agents/code.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,9 @@ Your messages may include \`<project-memory>\` blocks containing memories automa
7272
7373
These memories may be stale or irrelevant to the current task. Use your judgement. If a memory seems outdated or incorrect for the current task, you can ignore it.
7474
If you notice patterns of outdated or incorrect memories, consider asking the user to curate them. Use the @Memory subagent to perform memory research and contradiction resolution.
75+
76+
## Project KV Store
77+
78+
Use \`memory-kv-get\` to check for active project state (e.g., planning progress, code review patterns). Use \`memory-kv-set\` to store ephemeral findings. Entries expire after 24 hours. Use \`memory-kv-list\` to see all active entries.
7579
`,
7680
}

packages/memory/src/agents/memory.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Use for:
5353
- Domain-specific terminology (e.g., "User refers to authenticated entity, Guest to unauthenticated")
5454
- Integration points and API contracts (e.g., "Payment service expects amount in cents")
5555
- Known issues and workarounds (e.g., "Hot reload breaks with circular imports—restart required")
56+
- Code review findings (prefixed with \`[review-finding]\`, stored by the Code Review agent)
5657
- Technical debt notes (e.g., "Auth needs migration to JWT")
5758
- Domain knowledge (e.g., "Prices stored as integers to avoid floating point issues")
5859

packages/memory/src/index.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { createVecService } from './storage/vec'
1111
import { createEmbeddingProvider, checkServerHealth, isServerRunning, killEmbeddingServer } from './embedding'
1212
import { createMemoryService } from './services/memory'
1313
import { createEmbeddingSyncService } from './services/embedding-sync'
14+
import { createKvService } from './services/kv'
1415
import { loadPluginConfig } from './setup'
1516
import { resolveLogPath } from './storage'
1617
import { createLogger } from './utils/logger'
@@ -293,6 +294,9 @@ export function createMemoryPlugin(config: PluginConfig): Plugin {
293294
memoryService.setDedupThreshold(config.dedupThreshold)
294295
}
295296

297+
const kvService = createKvService(db, logger)
298+
kvService.startCleanup()
299+
296300
const mismatchState: DimensionMismatchState = {
297301
detected: false,
298302
expected: null,
@@ -372,6 +376,7 @@ export function createMemoryPlugin(config: PluginConfig): Plugin {
372376
cleaned = true
373377
logger.log('Cleaning up plugin resources...')
374378
memoryInjection.destroy()
379+
kvService.destroy()
375380
await memoryService.destroy()
376381
closeDatabase(db)
377382
logger.log('Plugin cleanup complete')
@@ -547,6 +552,74 @@ export function createMemoryPlugin(config: PluginConfig): Plugin {
547552
return `Implementation session created and plan sent.\n\nSession: ${newSessionId}\nTitle: ${sessionTitle}\nModel: ${modelInfo}\n\nSwitch to this session to begin. You can change the model from the session dropdown.`
548553
},
549554
}),
555+
'memory-kv-set': tool({
556+
description: 'Store a key-value pair for the current project. Values expire after 24 hours by default. Use for ephemeral project state like planning progress, code review patterns, or session context.',
557+
args: {
558+
key: z.string().describe('The key to store the value under'),
559+
value: z.string().describe('The value to store (JSON string)'),
560+
ttlMs: z.number().optional().describe('Time-to-live in milliseconds (default: 24 hours)'),
561+
},
562+
execute: async (args) => {
563+
logger.log(`memory-kv-set: key="${args.key}"`)
564+
let parsed: unknown
565+
try {
566+
parsed = JSON.parse(args.value)
567+
} catch {
568+
parsed = args.value
569+
}
570+
kvService.set(projectId, args.key, parsed, args.ttlMs)
571+
const expiresAt = new Date(Date.now() + (args.ttlMs ?? 24 * 60 * 60 * 1000))
572+
logger.log(`memory-kv-set: stored key="${args.key}", expires=${expiresAt.toISOString()}`)
573+
return `Stored key "${args.key}" (expires ${expiresAt.toISOString()})`
574+
},
575+
}),
576+
'memory-kv-get': tool({
577+
description: 'Retrieve a value by key for the current project.',
578+
args: {
579+
key: z.string().describe('The key to retrieve'),
580+
},
581+
execute: async (args) => {
582+
logger.log(`memory-kv-get: key="${args.key}"`)
583+
const value = kvService.get(projectId, args.key)
584+
if (value === null) {
585+
logger.log(`memory-kv-get: key="${args.key}" not found`)
586+
return `No value found for key "${args.key}"`
587+
}
588+
logger.log(`memory-kv-get: key="${args.key}" found`)
589+
return typeof value === 'string' ? value : JSON.stringify(value, null, 2)
590+
},
591+
}),
592+
'memory-kv-delete': tool({
593+
description: 'Delete a key-value pair for the current project.',
594+
args: {
595+
key: z.string().describe('The key to delete'),
596+
},
597+
execute: async (args) => {
598+
logger.log(`memory-kv-delete: key="${args.key}"`)
599+
kvService.delete(projectId, args.key)
600+
logger.log(`memory-kv-delete: deleted key="${args.key}"`)
601+
return `Deleted key "${args.key}"`
602+
},
603+
}),
604+
'memory-kv-list': tool({
605+
description: 'List all active key-value pairs for the current project.',
606+
args: {},
607+
execute: async () => {
608+
logger.log('memory-kv-list')
609+
const entries = kvService.list(projectId)
610+
if (entries.length === 0) {
611+
logger.log('memory-kv-list: no entries')
612+
return 'No active KV entries for this project.'
613+
}
614+
const formatted = entries.map((e) => {
615+
const expiresIn = Math.round((e.expiresAt - Date.now()) / 60000)
616+
const dataPreview = typeof e.data === 'string' ? e.data.substring(0, 100) : JSON.stringify(e.data).substring(0, 100)
617+
return `- **${e.key}** (expires in ${expiresIn}m)\n ${dataPreview}${dataPreview.length >= 100 ? '...' : ''}`
618+
})
619+
logger.log(`memory-kv-list: ${entries.length} entries`)
620+
return `${entries.length} active KV entries:\n\n${formatted.join('\n')}`
621+
},
622+
}),
550623
},
551624
config: createConfigHandler(agents),
552625
'chat.message': sessionHooks.onMessage,
@@ -623,7 +696,7 @@ export function createMemoryPlugin(config: PluginConfig): Plugin {
623696
text: `<system-reminder>
624697
Plan mode is active. You MUST NOT make any file edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received.
625698
626-
You may ONLY: observe, analyze, plan, and use memory tools (memory-read, memory-write, memory-edit, memory-delete, memory-health, memory-plan-execute).
699+
You may ONLY: observe, analyze, plan, and use memory tools (memory-read, memory-write, memory-edit, memory-delete, memory-health, memory-plan-execute, memory-kv-set, memory-kv-get, memory-kv-delete, memory-kv-list).
627700
</system-reminder>`,
628701
synthetic: true,
629702
})

packages/memory/src/services/kv.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { Database } from 'bun:sqlite'
2+
import { createKvQuery } from '../storage/kv-queries'
3+
import type { Logger } from '../types'
4+
5+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000
6+
7+
export interface KvEntry {
8+
key: string
9+
data: unknown
10+
updatedAt: number
11+
expiresAt: number
12+
}
13+
14+
export interface KvService {
15+
get<T = unknown>(projectId: string, key: string): T | null
16+
set<T = unknown>(projectId: string, key: string, data: T, ttlMs?: number): void
17+
delete(projectId: string, key: string): void
18+
list(projectId: string): KvEntry[]
19+
startCleanup(intervalMs?: number): void
20+
destroy(): void
21+
}
22+
23+
export function createKvService(db: Database, logger?: Logger): KvService {
24+
const queries = createKvQuery(db)
25+
let cleanupInterval: ReturnType<typeof setInterval> | null = null
26+
27+
return {
28+
get<T = unknown>(projectId: string, key: string): T | null {
29+
const row = queries.get(projectId, key)
30+
if (!row) return null
31+
try {
32+
return JSON.parse(row.data) as T
33+
} catch {
34+
return null
35+
}
36+
},
37+
38+
set<T = unknown>(projectId: string, key: string, data: T, ttlMs?: number): void {
39+
const expiresAt = Date.now() + (ttlMs ?? DEFAULT_TTL_MS)
40+
const jsonData = JSON.stringify(data)
41+
queries.set(projectId, key, jsonData, expiresAt)
42+
},
43+
44+
delete(projectId: string, key: string): void {
45+
queries.delete(projectId, key)
46+
},
47+
48+
list(projectId: string): KvEntry[] {
49+
const rows = queries.list(projectId)
50+
return rows.map((row) => {
51+
let data: unknown = null
52+
try {
53+
data = JSON.parse(row.data)
54+
} catch {
55+
}
56+
return {
57+
key: row.key,
58+
data,
59+
updatedAt: row.updatedAt,
60+
expiresAt: row.expiresAt,
61+
}
62+
})
63+
},
64+
65+
startCleanup(intervalMs: number = 30 * 60 * 1000): void {
66+
if (cleanupInterval) return
67+
cleanupInterval = setInterval(() => {
68+
try {
69+
const deleted = queries.deleteExpired()
70+
if (deleted > 0) {
71+
logger?.log(`KV cleanup: removed ${deleted} expired entries`)
72+
}
73+
} catch (error) {
74+
logger?.error('KV cleanup failed', error)
75+
}
76+
}, intervalMs)
77+
},
78+
79+
destroy(): void {
80+
if (cleanupInterval) {
81+
clearInterval(cleanupInterval)
82+
cleanupInterval = null
83+
}
84+
},
85+
}
86+
}

packages/memory/src/storage/database.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,20 @@ export function initializeDatabase(dataDir: string): Database {
107107
)
108108
`)
109109

110+
db.run(`
111+
CREATE TABLE IF NOT EXISTS project_kv (
112+
project_id TEXT NOT NULL,
113+
key TEXT NOT NULL,
114+
data TEXT NOT NULL,
115+
expires_at INTEGER NOT NULL,
116+
created_at INTEGER NOT NULL,
117+
updated_at INTEGER NOT NULL,
118+
PRIMARY KEY (project_id, key)
119+
)
120+
`)
121+
122+
db.run(`CREATE INDEX IF NOT EXISTS idx_project_kv_expires_at ON project_kv(expires_at)`)
123+
110124
return db
111125
}
112126

0 commit comments

Comments
 (0)