Context Chronicle is a verifiable memory system and tool firewall designed for long-running AI programming conversations. It addresses context loss, constraint violations, and decision regression.
All fact judgments must trace back to raw events. Compressed summaries, sub-agent reports, and Memory Packets are derivatives and cannot replace original evidence.
Uses SQLite local storage by default with no external services required. Optional cloud sync is available, but core functionality works completely offline.
Supports Windows, Linux, and macOS, as well as both TUI and Desktop variants of Claude Code.
- MCP Server: Cross-tool universal capabilities (Cursor, Windsurf, Claude Code)
- Claude Code Hooks: Deep integration capabilities (event interception, compaction protection)
| Responsible OK | Not Responsible NO |
|---|---|
| Event recording, constraint management, tool firewall | Network request interception, HTTP proxy |
| Local SQLite storage and FTS5 full-text search | Real-time filesystem monitoring, git hooks |
| Vector indexing (requires external embedding provider) | Cloud sync, multi-machine collaboration |
| Model switch detection and context bridging | IDE plugins, editor extensions |
| Plugin conflict detection (known plugins) | General process management, resource limits |
| Automatic key/sensitive data redaction | Encrypted storage, access control |
+------------------------------------------------------+
| Claude Code / Cursor / Windsurf |
+------------------------------------------------------+
| |
| +----------------+ +------------------+ |
| | OpenCode | | MCP Client | |
| | Lifecycle | | (Universal) | |
| | Adapter (TS) | | | |
| +-------+--------+ +--------+---------+ |
+----------+-----------------------------+------------+
| |
| stdio | stdio
down down
+--------------------+ +----------------------+
| OpenCodeAdapter | | MCP Server |
| (TypeScript) | | (TypeScript) |
+--------------------+ +----------------------+
| 9 lifecycle hooks: | | ToolHandler interface |
| - onSessionStart | | (handler-interface) |
| - onUserPromptSubmit| | Map-based router |
| - onPreToolUse | | (router.ts) |
| - onPostToolUse | | 6 unified tools: |
| - onPreCompact | | - memorySearch |
| - onPostCompact | | - memoryManage |
| - onStop (auditGate)| | - memoryStatus |
| - onSubagentStop | | - memoryCompact |
| - onModelSwitch | | - guardedBash |
+--------+-----------+ | - memoryHistory |
| +----------+-----------+
| |
+----------+------------------+
down
+----------------------------+
| Storage Layer |
+----------------------------+
| SqliteBackend implements |
| 4 role interfaces from |
| src/storage/repository.ts: |
| - EventRepository |
| - ConstraintRepository |
| - KGRepository |
| - AuditRepository |
| |
| Delegated to specialized |
| storage classes: |
| - EventStorage (events.ts) |
| - PacketStorage (packets) |
| - DecisionStorage |
| - KGStorage (kg-store.ts) |
| |
| - SQLite (Raw Events) |
| - FTS5 (Full-text Search) |
| - sqlite-vec-backend |
| (vec0 + cosine) |
| - session_links |
| (cross-DB search) |
| - memory_packets |
| (constraints / facts / |
| hypotheses / decisions) |
| - Decision Timeline |
| - Rolling Compaction State |
| - Memory Decay State |
| (halfLifeTurns metadata) |
+----------------------------+
graph TB
subgraph "AI Client (OpenCode / Cursor / Windsurf)"
OC[OpenCode Lifecycle Adapter]
MC[MCP Client]
end
subgraph "Context Chronicle Core"
ADAPTER[OpenCodeAdapter<br/>9 lifecycle hooks]
MCP_SRV[MCP Server<br/>6 unified tools]
end
subgraph "Storage Layer"
SQLITE[(SQLite<br/>event_log + FTS5)]
VEC[(sqlite-vec<br/>vec0 + cosine)]
KG[(Knowledge Graph<br/>entities + relations)]
end
subgraph "Subsystems"
COMPACT[Rolling Compaction<br/>pressure -> textrank -> audit -> reinject]
DECAY[Memory Decay<br/>context-distance decay<br/>dual-dimension model]
FIREWALL[Tool Firewall<br/>regex + constraint rules]
SEARCH[Hybrid Search<br/>FTS5 + vector -> RRF -> LLM rank]
AUDIT[Stop Gate<br/>7-dimension completion check]
EMBED[Embedding Pipeline<br/>9 providers + auto-pipeline]
end
OC --> ADAPTER
MC --> MCP_SRV
ADAPTER --> SQLITE
ADAPTER --> COMPACT
ADAPTER --> DECAY
ADAPTER --> FIREWALL
ADAPTER --> AUDIT
MCP_SRV --> SQLITE
MCP_SRV --> SEARCH
MCP_SRV --> KG
EMBED --> VEC
SEARCH --> VEC
SEARCH --> SQLITE
COMPACT --> SQLITE
COMPACT --> EMBED
DECAY --> COMPACT
DECAY --> SQLITE
flowchart LR
EVENTS[New Events] --> PRESSURE[Pressure Score<br/>entropy + importance]
PRESSURE --> CHECK{Pressure >=<br/>threshold?}
CHECK -->|No| SKIP[Skip - below watermark]
CHECK -->|Yes| TEXTRANK[TextRank Summarization<br/>extractive + constraints]
TEXTRANK --> AUDIT_C[Compaction Audit<br/>integrity + coverage check]
AUDIT_C --> PASS{Audit<br/>passed?}
PASS -->|Yes| REINJECT[Build Reinjection<br/>active constraints + tasks]
PASS -->|No| FAIL[Mark failed - retry later]
REINJECT --> WATERMARK[Update Watermark]
REINJECT --> INDEX[Index in Vector Store]
REINJECT --> DECAY_META[Record halfLifeTurns<br/>in CompactionMetadata]
flowchart LR
QUERY[User Query] --> FTS[FTS5 Keyword Search<br/>with CJK auto-routing]
QUERY --> VEC_S[Vector Cosine Search<br/>vec0 + sqlite-vec]
FTS --> MERGE[RRF Merger<br/>Reciprocal Rank Fusion]
VEC_S --> MERGE
MERGE --> DEDUP[Deduplicate + Filter]
DEDUP --> RERANK{Results > 8?}
RERANK -->|Yes| LLM[LLM Re-ranker<br/>subagent template]
RERANK -->|No| OUTPUT[Return Results<br/>token-budgeted]
LLM --> OUTPUT
sequenceDiagram
participant Host as OpenCode Host
participant Adapter as OpenCodeAdapter
participant DB as SQLite Backend
participant Gate as Stop Gate
Host->>Adapter: onSessionStart(sessionId)
Adapter->>DB: load active constraints + decisions
Adapter-->>Host: projectBrief
Host->>Adapter: onUserPromptSubmit(message)
Adapter->>DB: insert event, detect decisions/constraints
Adapter->>Adapter: check compaction trigger
Host->>Adapter: onPreToolUse(tool, args)
Adapter->>DB: firewall check against constraints
Adapter-->>Host: allowed/blocked verdict
Host->>Adapter: onPostToolUse(result)
Adapter->>DB: record tool result
Host->>Adapter: onModelSwitch(old, new)
Adapter->>DB: assess context risk, build bridge
Host->>Adapter: onPreCompact(session)
Adapter->>Adapter: rolling compaction decision
Adapter-->>Host: hostCompactionDisabled
Host->>Adapter: onPostCompact(summary)
Adapter->>DB: save compaction state + reinjection
Host->>Adapter: onStop(session)
Adapter->>Gate: evaluate completion
Gate-->>Adapter: passed/blocked + issues
Adapter-->>Host: blocked decision + auto-fix injection
CREATE TABLE event_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sequence_id INTEGER NOT NULL UNIQUE,
timestamp INTEGER NOT NULL,
session_id TEXT NOT NULL,
project_path TEXT,
event_type TEXT NOT NULL CHECK(event_type IN ('user_message', 'assistant_message', 'tool_call', 'tool_result', 'system_event', 'subagent_message')),
actor TEXT NOT NULL CHECK(actor IN ('user', 'claude', 'system', 'subagent')),
-- Tool call specific
tool_name TEXT,
command TEXT,
-- Result
result_summary TEXT,
result_full TEXT,
-- Firewall
firewall_verdict TEXT CHECK(firewall_verdict IN ('allowed', 'blocked', 'warned', NULL)),
-- Visibility level
visibility TEXT NOT NULL DEFAULT 'user' CHECK(visibility IN ('user', 'internal', 'secret')),
-- Evidence chain (JSON array of event IDs)
evidence_refs TEXT DEFAULT '[]',
-- Metadata
metadata_json TEXT DEFAULT '{}',
-- Subagent & model metadata (v3)
subagent_id TEXT,
parent_session_id TEXT,
model_name TEXT,
-- Event integrity
integrity_hash TEXT
);Design Notes:
id: Auto-incrementing row id (primary key)sequence_id: Monotonically increasing, used to determine strict orderingproject_path: Nullable (some events originate outside any project context)actor: Discriminator between user / claude / system / subagent, drives reranker role weightsevent_type+actor+firewall_verdict+visibilityenable post-hoc filtering and auditintegrity_hash: SHA-256 chain for tamper detection (v4 migration)
There is no separate constraints table. Constraints are stored as memory_packets rows with packet_type='constraint':
CREATE TABLE memory_packets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT,
created_at INTEGER NOT NULL,
expires_at INTEGER,
packet_type TEXT NOT NULL CHECK(packet_type IN ('constraint', 'fact', 'hypothesis', 'decision')),
category TEXT,
content TEXT NOT NULL,
confidence REAL DEFAULT 1.0 CHECK(confidence >= 0 AND confidence <= 1.0),
-- Evidence chain (JSON array of event IDs from event_log)
evidence_chain TEXT NOT NULL DEFAULT '[]',
-- Scope
scope_type TEXT DEFAULT 'session' CHECK(scope_type IN ('session', 'project', 'global')),
-- Vector embedding (stored as JSON array or binary blob)
embedding TEXT,
-- Metadata
metadata_json TEXT DEFAULT '{}'
);Design Notes:
- Constraints are unified with facts/hypotheses/decisions under one table distinguished by
packet_type - Must trace back to raw events (
evidence_chain) scope_typecontrols lifetime: session, project, or global- FTS5 dual index (
memory_packets_fts+memory_packets_fts_cjk) supports keyword search
CREATE TABLE decisions (
id TEXT PRIMARY KEY,
parent_id TEXT,
created_at INTEGER NOT NULL,
question TEXT NOT NULL,
chosen_option TEXT NOT NULL,
rejected_options TEXT DEFAULT '[]',
rationale TEXT,
evidence_refs TEXT DEFAULT '[]',
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'superseded', 'reverted')),
FOREIGN KEY (parent_id) REFERENCES decisions(id)
);Design Notes:
- Tracks technical solution evolution (e.g. Redis -> in-memory LRU)
- Records rejected options (prevents old solutions from resurfacing)
parent_idforms a self-referencing tree of decision revisions
The storage layer uses four role interfaces defined in src/storage/repository.ts:
EventRepository— Event CRUD, FTS5 search, sequence numbering, integrity chainConstraintRepository— Memory packet lifecycle, constraint management, expiration cleanupKGRepository— Knowledge graph entity/relation CRUD, decision timeline, graph searchAuditRepository— Raw SQL access, table statistics, audit reports, integrity verification, session linking, lifecycle management
SqliteBackend (src/storage/sqlite-backend.ts) implements all four interfaces via the implements clause:
export class SqliteBackend implements EventRepository, ConstraintRepository, KGRepository, AuditRepository {It delegates domain-specific operations to four specialized storage classes:
SqliteBackend (facade)
+-- EventStorage (src/storage/events.ts) — Event CRUD, FTS, integrity chain
+-- PacketStorage (src/storage/packets.ts) — Memory packet/constraint lifecycle
+-- DecisionStorage (src/storage/decisions.ts) — Decision CRUD, timeline
+-- KGStorage (src/storage/kg-store.ts) — Entity/relation CRUD, CTE traversal
Cross-cutting infrastructure methods (raw SQL, audit reports, session linking, lifecycle) are implemented directly on SqliteBackend rather than delegated.
| File | Purpose |
|---|---|
src/storage/fts-utils.ts |
FTS5 query escaping, CJK LIKE clause builder |
src/storage/cjk-detector.ts |
CJK character detection for FTS5 routing |
src/storage/kg-types.ts |
KG type definitions (EntityType, KGRelation) |
src/storage/kg-validation.ts |
KG entity/relation input validation |
src/storage/db-connection-manager.ts |
Connection pool and CompactionStateRow |
src/storage/audit-manager.ts |
Audit domain logic |
src/storage/session-link-manager.ts |
Cross-session link management |
src/storage/session-cleanup-manager.ts |
Expired session cleanup |
src/storage/model-switch.ts |
Model switch tracking |
src/storage/event-origin.ts |
Event provenance tracking |
src/storage/sqlite-backend-utils.ts |
Audit types, init/migration helpers |
Every MCP tool conforms to the ToolHandler interface defined in src/mcp/handler-interface.ts:
export interface ToolHandler<TArgs = Record<string, unknown>> {
readonly name: string;
readonly description: string;
readonly inputSchema: z.ZodType<TArgs>;
execute(args: TArgs): Promise<ToolResult>;
}Each handler packages its own Zod schema, MCP description, and execution logic as a single unit.
The old switch-statement tool dispatcher has been replaced by a Map<string, ToolHandler> registry in src/mcp/router.ts:
const handlers = new Map<string, ToolHandler>();
function registerHandler(handler: ToolHandler): void {
handlers.set(handler.name, handler);
}
export async function callTool(name: string, args: unknown) {
const handler = handlers.get(name);
if (!handler) throw ContextChronicleError.config(`Unknown tool: ${name}`);
return handler.execute(parseOrThrow(handler.inputSchema, args));
}Tools are registered at module load time via registerHandler(). The callTool() function looks up the handler by name, validates arguments with Zod, and delegates execution.
9 handler files in src/mcp/handlers/:
| File | Exported Handler | Tools Served |
|---|---|---|
search.ts |
searchHandler |
memorySearch |
memory-status.ts |
manageHandler, statusHandler |
memoryManage, memoryStatus |
memory-constraints.ts |
constraintHandler |
memoryCompact |
memory-tools.ts |
guardedBashHandler |
guardedBash |
history.ts |
historyHandler |
memoryHistory |
graph.ts |
graphHandler |
Internal KG operations |
memory-audit.ts |
(action exports, no standalone handler) | RAG audit/clean/backfill actions |
memory-embedding.ts |
(action exports, no standalone handler) | Embedding config, decay settings |
memory-utils.ts |
Shared LogLevel type guard |
(utility only) |
Handlers that serve multiple actions (e.g. memoryManage) use internal switch-on-action dispatch within their execute() method.
src/cli/command-utils.ts provides two shared entry points used by 8 CLI commands:
openDb()— Resolves paths viagetPaths(), callsrequireInit(), opensSqliteBackend, returnsDbContext { db, paths }requireInit(paths)— Exits with code 1 if the database file does not exist, printingNot initialized. Run: context-chronicle init
Commands using openDb(): audit, compaction, embedding, export, metrics, rag, search, status.
Commands NOT using openDb() (they have their own initialization paths): install, uninstall, init, setup, doctor, config, feature, logs.
16 command files in src/cli/commands/, registered in the CLI router:
audit.ts compaction.ts config.ts doctor.ts
embedding.ts export.ts feature.ts init.ts
install.ts logs.ts metrics.ts rag.ts
search.ts setup.ts status.ts uninstall.ts
Install and uninstall use an InstallDiff contract (src/types/install-diff.ts) that records exactly what was added during installation:
export interface InstallDiff {
version: 1;
installedAt: string;
targetPath: string; // Absolute path to opencode.json
backupPath: string; // Snapshot backup path
mcpKeys: string[]; // MCP keys we added
pluginEntries: string[]; // Plugin entries we appended
commandKeys: string[]; // Slash command keys we added
sections: string[]; // Top-level sections we added
compactionChanged: { from: boolean | undefined; to: false } | null;
snapshotCreated: boolean;
}The diff records only entries that did not already exist — user-owned pre-existing entries are never recorded and therefore never removed on uninstall.
PluginInstallManager.install()
+-- Lazy import SqliteBackend (keeps CLI startup light)
+-- Init SQLite database
+-- OpenCodeConfigManager.applyPluginConfig()
| +-- Create snapshot backup (.context-chronicle.backup)
| +-- Write MCP server, plugin entry, lifecycle config, slash commands
| +-- Set compaction.auto = false (first install)
| +-- Compute InstallDiff (delta only)
| +-- Save install-state.json
+-- Lazy import ContextChronicleDoctor → run diagnostics
+-- Smoke test (verify dist/*.js exist)
+-- TUI plugin auto-install
PluginInstallManager.uninstall()
+-- Tier 1: restoreFromInstallDiff()
| Reads install-state.json → parses InstallDiff
| Applies inverse diff (removes only recorded keys)
| Preserves user changes made after install
| Falls through if: install-state.json missing/corrupt
+-- Tier 2: Full snapshot restore
Copies .context-chronicle.backup → opencode.json
Warns if user made changes after install
Last-resort fallback
At install time, a complete snapshot of opencode.json is saved to <opencode.json>.context-chronicle.backup. This serves as a last-resort recovery path when the diff-based undo fails.
scripts/emergency-cleanup.cjs is a zero-dependency Node.js script that removes all context-chronicle entries from opencode.json when the normal uninstall command is unavailable (e.g. deleted dist/ files). It attempts snapshot restoration first, then manually removes MCP server, plugin, slash commands, and the contextChronicle section.
scripts/build.js uses esbuild to produce self-contained CJS bundles for cli.ts and mcp-server.ts. Deleting individual compiled files in dist/ no longer breaks the uninstall command — the bundles contain all application code inline. Native addons (better-sqlite3, sqlite-vec) remain external since esbuild cannot bundle C++ modules.
PluginInstallManager uses await import() for SqliteBackend and ContextChronicleDoctor inside the install() method body rather than at the top of the file. This keeps the CLI entry point lightweight — context-chronicle --help does not need to load SQLite or the doctor module. The embedding CLI commands use the same pattern for createProviderFromPreset.
The InstallDiff system allows precise undo — only entries installed by context-chronicle are removed. Any user edits to opencode.json between install and uninstall are preserved.
scripts/emergency-cleanup.cjs provides a nuclear option with zero external dependencies. It can be run directly (node scripts/emergency-cleanup.cjs) or copy-pasted into a Node.js REPL when dist/ files are missing.
Query
down
+----------------+
| Query Router | <- Determines which search paths are needed
+----+-----------+
|
+--+--+-------------+
down down down
FTS5 Vector Structured
Search Search Query
| | |
+-----+------+------+
down
+---------------+
| RRF Merger | <- Fuses multi-path results
+-------+-------+
down
+---------------+
| Reranker | <- Re-ranks
+-------+-------+
down
+---------------+
| Context | <- Context expansion
| Expander |
+-------+-------+
down
Memory Packet
- Precise keyword matching
- Supports boolean queries
- Suitable for: command names, file paths, error codes
- Primary: Uses
sqlite-vec-backend.ts(sqlite-vec extension:vec0virtual table +vec_distance_cosinefor native, high-performance vector search) - Fallback: BLOB storage + JS cosine similarity - only activated when the native
sqlite-vecextension is unavailable - Supports 28 cross-vendor Embedding presets
- Suitable for: semantic similarity queries
- See Embedding Subsystem section below for details
function rrf(results: SearchResult[][], k = 60): SearchResult[] {
const scores = new Map<string, number>();
for (const resultSet of results) {
resultSet.forEach((doc, rank) => {
const score = 1 / (k + rank + 1);
scores.set(doc.id, (scores.get(doc.id) || 0) + score);
});
}
return sortByScore(scores);
}- Role weight: user > tool > assistant
- Time decay: Recency weighting
- Constraint signal: Items containing "don't"/"must" get priority
- Scope match: Current project gets priority
- On top of FTS5 results, extends relevant context through entity-relationship graph traversal
- Recursive CTE graph traversal (depth configurable, default 2 hops)
- Graph-aware search provides higher context recall than pure FTS5
- Automatically detects CJK characters in queries -> routes to trigram-tokenized FTS5 tables
- Short terms (1-2 CJK characters) automatically fall back to
LIKE '%...%' - 3 parallel CJK FTS5 tables:
event_log_fts_cjk,memory_packets_fts_cjk,kg_entities_fts_cjk - Storage overhead: approximately 15-25MB increase for a 100K event database
kg_entities -+- kg_relations -+- kg_entities
| |
+- kg_entity_types (whitelist)
Entity types: agent, sub_agent, agents_md_rule, project_file, decision, constraint, memory_packet, task
Relation types: agent_spawned, agent_referenced, file_imports, file_tests, decision_affects, decision_conflicts, constraint_applies, task_produces, event_involves, relates_to
- source_type filtering: compaction_artifact is automatically excluded
- SHA-256 dedup: content_hash UNIQUE constraint
- entity_type whitelist: validateEntity() rejects unknown types
entity- Add entities or relationsget-entity- Query entities and their relations (supports depth and pagination)list- FTS5 full-text search across graph entitiesrag-audit- Health check and statistics
User request -> LLM decision -> Ready to execute tool
down
PreToolUse Hook
down
+--------------+
| Check |
| constraint |
| ledger |
+------+-------+
|
+--------------+--------------+
down down down
Allow Warn Block
| | |
+--------------+--------------+
down
Execute tool / Deny
async function checkFirewall(tool: string, args: any) {
// 1. Read active constraints
const constraints = await db.getActiveConstraints({
type: 'project',
id: currentProjectId,
});
// 2. Pattern matching
for (const rule of constraints) {
if (matchesPattern(tool, args, rule.content)) {
return {
allowed: false,
reason: rule.content,
evidence: await buildEvidenceChain(rule.source_event_ids),
suggestion: generateSuggestion(args, rule),
};
}
}
// 3. Allow
return { allowed: true };
}| Pattern | Match Logic | Example |
|---|---|---|
npm_forbidden |
Command contains npm install / npm i |
"use pnpm not npm" |
protected_path |
Path matches regex | "do not edit generated files" |
forbidden_dep |
Dependency name matches | "no Redis allowed" |
Responsibilities:
- Retrieve candidate evidence
- Judge evidence validity
- Generate Memory Packet
Workflow:
Query
down
Hybrid search (returns candidate chunks)
down
LLM sub-agent analysis
down
Memory Packet (short, structured, traceable)
Prompt Template:
You are a memory verification agent. Analyze retrieved evidence.
Query: {query}
Candidates:
1. [seq: 123, role: user] "This project uses pnpm"
2. [seq: 456, role: assistant] "Should we use npm?"
3. [seq: 789, role: user] "No, pnpm only"
Task:
- Identify which are facts vs questions vs rejections
- Filter outdated or superseded information
- Return structured Memory Packet
Output JSON:
{
"usable_facts": [...],
"rejected_facts": [...],
"uncertain": [...]
}
Context Chronicle dispatches specialized subagents for reranking, entity extraction, constraint staleness detection, decision auditing, and session summarization. All subagents follow a strict text-only, JSON-output protocol to prevent tool-calling loops and output drift.
src/plugin/subagent-templates/index.ts (42 lines, split across 3 template files: templates/search.ts, templates/audit.ts, templates/compaction.ts) - 8 templates, dynamic assembly via buildPrompt().
| Block | Purpose |
|---|---|
NO_TOOLS_PREAMBLE |
Universal constraint: no tool calls, no file reads, no markdown, JSON only |
THINKING_INSTRUCTION |
Structured reasoning in <thinking> blocks before JSON output |
CONFIDENCE_REQUIREMENT |
Mandatory 0.0-1.0 confidence scoring on all outputs |
buildPrompt(templateName, template, vars, context?, options?) -> stringWraps any template with context-aware metadata, retry policies, active constraints, thinking instructions, and disallowed-tool lists. Accepts optional TemplateContext (remaining tokens, session ID, constraints) and TemplateOptions (max retries, latency budget, partial-ok, model hint, template version).
| Template | Key | Purpose |
|---|---|---|
RERANK_TEMPLATE |
RERANK |
Query-intent classification, diversity-constrained result reranking with score breakdown |
ENTITY_EXTRACT_TEMPLATE |
ENTITY_EXTRACT |
Hierarchical entity extraction with coreference resolution, configurable depth (shallow/medium/deep) |
CONFLICT_RESOLUTION_TEMPLATE |
CONFLICT_RESOLUTION |
Cross-session constraint conflict detection with temporal resolution and evidence chains |
RAG_AUDIT_TEMPLATE |
RAG_AUDIT |
Pollution classification (stale/contradictory/hallucinated), coverage gap detection, remediation guidance |
STALE_CONSTRAINT_TEMPLATE |
STALE_CONSTRAINT |
Constraint staleness audit using context-distance decay model, review priority (p0/p1/p2) |
FACT_CHECK_TEMPLATE |
FACT_CHECK |
Claim decomposition and source verification with uncertainty handling (high/medium/low/hearsay) |
DECISION_AUDIT_TEMPLATE |
DECISION_AUDIT |
Post-hoc decision adherence audit: drift detection, reversal detection, completeness scoring |
SESSION_SUMMARY_TEMPLATE |
SESSION_SUMMARY |
Structured session summary for context bridging: goal, decisions, progress, open questions, hooks |
export const TEMPLATE_REGISTRY: Record<string, string> = {
RERANK,
ENTITY_EXTRACT,
CONFLICT_RESOLUTION,
RAG_AUDIT,
STALE_CONSTRAINT,
FACT_CHECK,
DECISION_AUDIT,
SESSION_SUMMARY,
};TemplateContext:remainingTokens,sessionId,activeConstraints,currentTimeTemplateOptions:maxRetries,partialOk,disallowedTools,latencyBudgetMs,modelHint,templateVersion
All templates share a common failure-output pattern: {"error": "reason"} for unrecoverable failures, {"flags": []} or equivalent empty response for no-detection scenarios.
Responsible for converting text into vector representations. It is the foundation of hybrid search and RAG backfill.
| File | Responsibility |
|---|---|
src/embedding/presets/ (13 files) |
Built-in provider presets (28 entries across 11 provider groups, covering all major providers) |
src/embedding/registry.ts |
Single provider registry and factory (createProviderFromPreset()); primary entry point for provider creation |
src/embedding/provider.ts |
Legacy compatibility shim with simple provider implementations (OpenAI, Cohere, Mock) and createProvider() factory |
src/embedding/openai-compatible-provider.ts |
OpenAI compatible protocol generic implementation (covers OpenAI / Azure / DashScope / SiliconFlow / Ollama / Zhipu / Volcengine) |
src/embedding/cohere-provider.ts |
Cohere-specific adapter (embed-v4.0 etc.) |
src/embedding/voyage-provider.ts |
Voyage-specific adapter (voyage-4-large etc.) |
src/embedding/jina-provider.ts |
Jina-specific adapter (embeddings-v3 / clip-v2) |
src/embedding/google-provider.ts |
Google Gemini Embedding specific adapter |
src/embedding/dashscope-provider.ts |
DashScope Alibaba Cloud Bailian specific adapter |
src/embedding/baidu-provider.ts |
Baidu Qianfan specific adapter |
src/embedding/tencent-provider.ts |
Tencent Hunyuan specific adapter |
src/embedding/base-http-provider.ts |
Abstract base class shared by HTTP-based provider implementations |
src/embedding/auto-pipeline.ts |
Automatic embedding pipeline (init / reindex / refresh) |
src/embedding/sqlite-vec-backend.ts |
sqlite-vec extension: vec0 virtual table + vec_distance_cosine native search (BLOB fallback) |
src/embedding/model-validator.ts |
Embedding model validation and health check |
src/embedding/vector-index-manager.ts |
Vector index rebuild and management |
Total: 16 source files, 8 concrete provider implementations, 28 presets. registry.ts is the primary entry point for provider creation; provider.ts remains as a compatibility shim for consumers of the old createProvider() API.
Note: Zhipu, Volcengine, and SiliconFlow do not have standalone provider files - they are implemented through
openai-compatible-provider.ts+ preset definitions inpresets/. Note:provider.js/provider.d.tsin build output are compatibility shims for consumers that import from the old path; they are thin wrappers aroundregistry.ts.
input text
down
embedding use <id> <- CLI selects provider, writes to config.json
down
embedding test <- Health check (probes connectivity + dimensions)
down
embed(text) <- Returns Float32Array
down
sqlite-vec-backend.ts <- Stored in vec0 table; queried via vec_distance_cosine
After switching providers, embedding reindex must be run, as vector dimensions are incompatible across providers.
Core mechanism: plugin takes over compaction, dynamic N backfill, prevents LLM context window from overflowing.
| File | Responsibility |
|---|---|
src/compaction/pressure-score.ts |
Calculates current session pressure score (token count / window capacity / growth rate) |
src/compaction/watermark.ts |
Sets compaction trigger thresholds + historical watermarks |
src/compaction/reinjection.ts |
Builds 10-section structured reinjection template with dynamic N backfill |
src/compaction/prompt-entropy.ts |
Detects prompt entropy increase (repetition/redundancy signals) to trigger early compaction |
src/compaction/decay.ts |
Memory decay: context-distance-based constraint trustworthiness calculation |
src/compaction/engine.ts |
Core compaction engine: TextRank extraction, audit, state persistence |
src/compaction/rolling-engine.ts |
Rolling compaction orchestration with CompactionMetadata (schemaVersion, halfLifeTurns, qualityTier, compactionDurationMs) |
Idle
down pressure > high_watermark
Compress
down generates state-snapshot
Reinject
down evaluates criticality, injects N raw events
Idle
compaction status- View current pressure / watermark / modecompaction mode <auto|manual|off>- Switch modescompaction disable-host- Disable host's native compactioncompaction mode strict-plugin-owned- Enable plugin takeover of compaction
Constraints lose trustworthiness over time - not by calendar age but by context distance (turns without reference). A constraint that has not been mentioned for many turns is less reliable than one recently reaffirmed, regardless of wall-clock time.
The decay formula uses a dual-dimension model where three factors multiply:
adjustedDecay = baseDecay x reinforcementFactor x topicDriftFactor
trustworthiness = 1 - adjustedDecay
- baseDecay: Exponential decay via
1 - e^(-lambda x turnDistance)wherelambda = ln(2) / halfLifeTurns - reinforcementFactor (0.0-2.0): How strongly the constraint was reaffirmed or contradicted
0.0= explicitly negated/superseded -> 100% decay0.1= explicitly reaffirmed -> strongly retained1.0= no reinforcement, standard decay
- topicDriftFactor (0.5-2.0): How related the current conversation is to the constraint topic
0.5= highly related -> slower decay2.0= unrelated -> accelerated decay
| Preset | Half-Life | Label | Use Case |
|---|---|---|---|
| 30 turns | 30 | Tight | Rapidly changing projects, frequent pivots |
| 50 turns | 50 | Moderate | Active iteration phases |
| 80 turns | 80 | Balanced (default) | General-purpose development |
| 120 turns | 120 | Relaxed | Stable projects with long-running tasks |
| 200 turns | 200 | Loose | Mature codebases, well-established conventions |
| 500 turns | 500 | Very loose | Archival constraints, rarely-changing policies |
| Infinity | Infinity | Never decay | Permanent, non-negotiable constraints |
src/compaction/decay.ts:calculateDecay(DecayInput) -> DecayResult- pure function implementing the dual-dimension model- Configuration:
config.json->memoryDecay.halfLifeTurns(default: 80) - Constants:
MEMORY_DECAY_DEFAULT_HALF_LIFE = 80,MEMORY_DECAY_PRESETS = [30, 50, 80, 120, 200, 500, Infinity] - Integration with STALE_CONSTRAINT_TEMPLATE: The staleness audit subagent receives the configured
halfLifeTurnsand applies context-distance decay to identify outdated constraints - Integration with compaction:
CompactionMetadata.halfLifeTurnsis persisted alongside every compaction so downstream tools can apply the correct decay model
Constraint referenced at turn N
down
turnDistance = currentTurn - N
down
calculateDecay({ turnDistance, halfLifeTurns, reinforcementFactor, topicDriftFactor })
down
trustworthiness score (0.0-1.0)
down
needsReview flag if trustworthiness < 0.5
down
STALE_CONSTRAINT_TEMPLATE audit via subagent
Core security mechanism: forcibly finalizes when the LLM is about to stop generating. Controlled by the auditGate feature toggle.
src/audit/final-gate.ts - Triggered by onStop lifecycle hook (in src/plugin/hooks/stop.ts).
Uses a dual-track decision algorithm (shouldTriggerAudit()):
- Forced triggers (any satisfied -> audit): firewall block rate > 30%, >=2 blocks, repeated corrections, compaction triggered, >=2 subagents
- Weighted triggers (total score >= 50 -> audit): token consumption, duration, tool variety, decision count, constraint changes
Simple tasks skip the audit entirely; complex tasks trigger an auto-fix loop (max 3 retries) before blocking completion.
- Summarizes all active constraints from this session
- Summarizes incomplete decisions (status != done)
- Writes an audit event (
event_type = 'stop_gate') - Injects key summary into the next
system prompt, enabling full task graph reconstruction across session restores - Auto-fix loop: on audit failure, injects blocking issues into the conversation for the agent to resolve
- After 3 consecutive failures, requires manual review
- Stop Gate cannot be disabled (hard constraint)
- Once triggered, at least 1 audit event must be produced
- On failure, degrades to warning log without affecting the main flow
OpenCodeTUI plugin lifecycle and adapter layer.
| File | Responsibility |
|---|---|
src/plugin/lifecycle.ts |
Plugin install / enable / disable / uninstall state machine |
src/plugin/opencode-adapter.ts |
OpenCodeTUI adapter (Hook protocol, config injection, stdio communication) |
src/tui/plugin.ts |
TUI settings panel: route + Ctrl+P command palette registration |
src/tui/settings.tsx |
SolidJS interactive settings panel: bilingual EN/ZH, feature toggles, presets |
absent -> installed -> enabled -> active
down
disabled -> active
down
uninstalled
Driven by three CLI commands: context-chronicle install / uninstall / doctor:
install- Registers OpenCodeTUI plugin + writes lifecycle configuninstall- Deregisters and cleans updoctor- Checks registration integrity, stdio connectivity, config consistency
Cross-session vector search: link multiple session databases and search across them as a unified corpus.
CREATE TABLE session_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_session TEXT NOT NULL,
linked_db_path TEXT NOT NULL,
linked_session TEXT NOT NULL,
created_at INTEGER NOT NULL,
label TEXT,
metadata TEXT DEFAULT '{}',
UNIQUE(source_session, linked_db_path, linked_session)
);| File | Responsibility |
|---|---|
src/storage/session-links-schema.sql |
Schema definition for session_links table |
src/storage/sqlite-backend.ts |
linkSession() - insert/delete links; validateDbPath() - verify DB integrity; checkLinkedDbHealth() - health probe |
src/retrieval/multi-session-search.ts |
searchAcrossLinkedSessions() - opens linked DBs as SqliteVecBackend instances, queries in parallel, merges via RRF |
src/mcp/handlers/memory.ts |
link-session / unlink-session / list-linked actions under memoryManage |
Query -> Embedding -> searchAcrossLinkedSessions()
+-- Main DB: vec0 cosine search
+-- Linked DB 1: vec0 cosine search
+-- Linked DB 2: vec0 cosine search
+-- RRF Merge -> ranked results with source_session/source_db tags
| Action | Description |
|---|---|
link-session |
Link another session DB for cross-session search |
unlink-session |
Remove a linked session |
list-linked |
List all currently linked sessions |
Structured file-based logging for all lifecycle hooks and internal operations.
- FileLogWriter (
src/utils/logger.ts): Buffered JSON Lines writer with configurable flush interval (5s) and retention (7 days) - Logger class: Tag-based logger with levels (debug/info/warn/error) for subsystems:
compaction,embedding,tool,model,session,prompt,stop - Log path:
~/.opencode/context-chronicle/logs/cc-YYYY-MM-DD.log - Hook integration:
client.app.log()calls in every lifecycle hook (compact.ts,model.ts,prompt.ts,stop.ts,session.ts,tool.ts)
{
"ts": "2026-06-24T10:30:00.000Z",
"lvl": "info",
"tag": "compaction",
"session": "ses_abc",
"msg": "Compaction triggered at seq 150"
}| Env Variable | Default | Description |
|---|---|---|
CC_LOG_LEVEL |
info |
Minimum log level (debug/info/warn/error) |
CC_LOG_RETENTION |
7 |
Days to retain log files |
| Max total size | 50 MB | Hard limit across all log files |
context-chronicle logs- View and filter log filescontext-chronicle logs --level error- Show errors onlycontext-chronicle logs --tag compaction- Filter by subsystem tag
- ConsolePatcher (formerly
src/utils/console-patcher.ts) has been removed. All logging now goes throughFileLogWriter. - Failed log writes never crash the application (best-effort semantics).
- Logs auto-clean on startup: files older than retention are deleted; oldest files trimmed if total exceeds 50 MB.
const isWindows = process.platform === 'win32';
const installPath = isWindows
? path.join(process.env.USERPROFILE!, '.opencode', 'context-chronicle')
: path.join(process.env.HOME!, '.opencode', 'context-chronicle');- Approach:
sqlite-vec-backend.ts- Usessqlite-vecextension withvec0virtual table andvec_distance_cosinefor native vector search. Falls back to BLOB + JS cosine similarity if the extension cannot be loaded. - Advantages: Near-native performance via extension, cross-platform compatible, backups aligned with SQLite
- Tradeoff: Requires
sqlite-vecnative extension; fallback provides degraded performance for large datasets, but this project's session-level data is under 10K, so impact is minimal
Lifecycle hooks are implemented in TypeScript via OpenCodeAdapter (src/plugin/opencode-adapter.ts). OpenCode invokes the adapter over stdio; no external hook scripts are deployed.
{
"plugin": ["context-chronicle"]
}The adapter registers nine lifecycle methods (onSessionStart, onUserPromptSubmit, onPreToolUse, onPostToolUse, onPreCompact, onPostCompact, onStop, onSubagentStop, onModelSwitch) directly through OpenCode's plugin API.
~/.opencode/context-chronicle/
+-- config.json
+-- db.sqlite
{
"plugin": ["context-chronicle"]
}function getInstallPath() {
switch (process.platform) {
case 'win32':
return path.join(process.env.USERPROFILE!, '.opencode', 'context-chronicle');
case 'darwin':
return path.join(process.env.HOME!, '.opencode', 'context-chronicle');
case 'linux':
return path.join(process.env.HOME!, '.opencode', 'context-chronicle');
default:
throw new Error(`Unsupported platform: ${process.platform}`);
}
}{
"storage": {
"backend": "sqlite",
"path": "~/.opencode/context-chronicle/db.sqlite"
},
"embedding": {
"provider": "openai",
"apiKey": "${OPENAI_API_KEY}",
"model": "text-embedding-3-small",
"dimension": 1536
},
"llm": {
"provider": "anthropic",
"apiKey": "${ANTHROPIC_API_KEY}",
"model": "claude-3-haiku-20240307"
},
"firewall": {
"mode": "warn",
"customRules": []
},
"retrieval": {
"fts5Weight": 0.5,
"vectorWeight": 0.5,
"maxResults": 10
},
"memoryDecay": {
"halfLifeTurns": 80
}
}interface ProviderConfig {
openai?: {
apiKey: string;
baseURL?: string;
};
anthropic?: {
apiKey: string;
};
deepseek?: {
apiKey: string;
baseURL?: string;
};
kimi?: {
apiKey: string;
};
}-- Composite indexes
CREATE INDEX idx_event_scope_seq
ON event_log(session_id, sequence_id);
CREATE INDEX idx_event_project_seq
ON event_log(project_id, sequence_id);
-- Constraints are stored in memory_packets with packet_type='constraint',
-- indexed by the memory_packets FTS5 table and filtered at query time.// Batch vector insertion
async function indexBatch(events: Event[]) {
const embeddings = await embeddingProvider.embed(events.map((e) => e.content));
const stmt = db.prepare(`
INSERT INTO vector_index (sequence_id, embedding)
VALUES (?, ?)
`);
db.transaction(() => {
events.forEach((event, i) => {
stmt.run(event.sequence_id, Buffer.from(new Float32Array(embeddings[i]).buffer));
});
})();
}// LRU cache for search results
const cache = new LRU({
max: 100,
ttl: 3600000, // 1 hour
});
async function cachedSearch(query: string) {
if (cache.has(query)) {
return cache.get(query);
}
const result = await hybridSearch(query);
cache.set(query, result);
return result;
}const SECRET_PATTERNS = [
/sk-[a-zA-Z0-9]{48}/, // OpenAI key
/sk_live_[a-zA-Z0-9]+/, // Stripe key
/ghp_[a-zA-Z0-9]{36}/, // GitHub token
/password\s*=\s*['\"]?([^'"\s]+)/i,
];
function sanitize(content: string): string {
let sanitized = content;
for (const pattern of SECRET_PATTERNS) {
sanitized = sanitized.replace(pattern, '[REDACTED]');
}
return sanitized;
}-- System prompts and developer prompts are excluded from user search
CREATE TABLE event_log (
...
visibility TEXT NOT NULL DEFAULT 'user',
-- user | internal | secret
);
-- Automatically filtered during search
SELECT * FROM event_log
WHERE visibility = 'user'
AND content MATCH ?;| Control | Implementation |
|---|---|
| Config sandbox | ohmyopenagent-adapter.ts parses JSON or runs JS in a locked vm context without require/process/global. |
| Shell-less tool execution | guardedBash.ts and subagent-invoker.ts spawn with shell: false and validate tokens before execution. |
| Interpreter escape blocking | bash -c, node -e, python -c, ruby -e, perl -e, php -r, and similar flags are rejected. |
| Minimal child environment | Only PATH, HOME/USERPROFILE, TEMP/TMP, and locale vars are forwarded; secrets are stripped. |
| SSRF / DNS rebinding | validateResolvedIps() resolves hostnames at request time and blocks private/loopback unless allowLoopback is set. |
| Event integrity | SHA-256 chain computed over sanitized, stored values; verifyEventIntegrity() recomputes from DB rows. |
| Binary integrity | postinstall-smart.cjs verifies better-sqlite3 prebuilt binaries against scripts/prebuild-manifest.json. |
| Path hardening | Model cache and session links are restricted to allowed bases; data dirs use 0o700. |
| JSON safety | safeParseJson() drops __proto__/constructor/prototype keys to prevent prototype pollution. |
Tests are organized in domain subdirectories mirroring src/ — 96 test files, 1311 tests total (excluding tests/load).
| Domain | Path | Focus |
|---|---|---|
| Audit | tests/audit/ |
Stop gate, integrity, task context, coordinator |
| Compaction | tests/compaction/ |
Rolling, watermarks, reinjection, entropy |
| Retrieval | tests/retrieval/ |
Hybrid search, ranking, RAG, multi-session |
| MCP | tests/mcp/ |
Protocol, tool contracts, handler tests |
| Storage | tests/storage/ |
SQLite, FTS5, vector, CJK, model-switch |
| Plugin | tests/plugin/ |
Install, uninstall, config, adapter |
| KG | tests/kg/ |
Entity CRUD, relation, graph search |
| Embedding | tests/embedding/ |
sqlite-vec backend, presets, providers |
| Security | tests/security/ |
Firewall, sanitize, FTS5 escape, API key headers |
| TUI | tests/tui/ |
Settings panel, plugin callbacks |
| Utils | tests/utils/ |
Logger, memory monitor, perf tracker |
| CLI | tests/cli/ |
Log viewer command |
| Scenarios | tests/scenarios/ |
Lifecycle, logging e2e, conflict, bridge |
| Property | tests/property/ |
RRF+search invariants (fuzz) |
| Load | tests/load/ |
10k event performance (manual, excluded from CI) |
tests/test-utils.ts exports createTestDb() which creates an isolated SqliteBackend in the OS temp directory with automatic WAL/joural file cleanup. Non-isolation tests use SqliteBackend(':memory:') for in-memory databases.
import { createTestDb } from './test-utils';
describe('MyModule', () => {
const { db, cleanup } = createTestDb();
afterAll(cleanup);
it('should work correctly', () => {
// db is a fully functional SqliteBackend
});
});Configured in jest.config.js: global thresholds at branches >= 25%, functions >= 30%, lines >= 40%, statements >= 40%. Per-glob thresholds were removed because Jest 29 interprets coverageThreshold keys as file path matches, not globs, making them silently no-op. The single global floor is robust across Jest patch releases and CI matrix jobs.
See TESTING.md for comprehensive testing guide.
CI (ci.yml) runs a 6-cell test matrix (3 OS x 2 Node versions) with one exclusion:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: ['18', '22']
exclude: windows-latest x node 18
fail-fast: false
The windows-latest x Node 18 combination is excluded because better-sqlite3@12.11.1 does not ship a Windows prebuilt binary for the Node 18 ABI, and GitHub-hosted Windows runners lack a compatible Visual Studio C++ toolchain for node-gyp rebuilds. Windows contributors on Node 18 must install the "Desktop development with C++" Visual Studio workload or upgrade to Node 22 (recommended, pinned by .nvmrc).
Each matrix cell runs: pnpm install, pnpm rebuild better-sqlite3, pnpm tsc --noEmit, pnpm run lint, pnpm run build, and pnpm test (excluding tests/load). Coverage is uploaded to Codecov. A gating job (test) depends on the full matrix passing.
- MCP Server (6 unified tools, JSON-RPC stdio, modular handlers in
src/mcp/) - FTS5 full-text search + LIKE fallback
- Constraint management + Memory Packet lifecycle
- CLI tools (init/status/search/setup/install/doctor/embedding/compaction)
- Tool firewall + guardedBash
- Vector search (sqlite-vec native search with fallback behavior)
- OpenCode TUI hooks (user message / tool call / compaction)
- Long-running task detection + audit coordinator + auto-correction
- Decision Timeline + Compaction Engine
- Embedding subsystem (17 provider/infrastructure areas + 29 presets + registry + TypeDoc)
- Rolling compaction subsystem (pressure-score + watermark + reinjection + prompt-entropy)
- Stop Gate (final-gate forced finalization + cross-session recovery)
- Lifecycle Harness (lifecycle.ts + opencode-adapter.ts)
- CLI extensions (install/uninstall/doctor + embedding + compaction)
- MCP extensions (6 unified tools, modular split:
src/mcp/handlers/) - Current scale: 160
.tssource files, 96 test files, 1311 tests
- Attachment file path injection (AttachmentExtractor + forgetting detection algorithm)
- System instruction protection (SystemFileRegistry + pointer/index mode handling)
- RAG data audit and annotation (EventOrigin + RAGDataAuditor)
- Compaction progress display (client.tui.showToast API)
- RAG search engine enhancement (useVector default true + vector search + auto-search + LLM sub-agent)
- Current baseline: 96 test files / 1311 tests (historical snapshot: 45/644 at initial review)
- [PLANNED] Team collaboration
- [PLANNED] Cloud sync (optional)
- [PLANNED] Web Dashboard
See CONTRIBUTING.md
Apache-2.0 License - see package.json and LICENSE
- Package manager policy: this project is pnpm-only; npm is prohibited for installing dependencies. CI, install scripts, and documentation examples should all use pnpm.
- OpenCode TUI policy: the current config schema uses
~/.config/opencode/opencode.json, top-levelmcpand top-levelplugin. The oldmcpServersis considered only for historical compatibility. - (Historical baseline 2026-06-20): 45 suites / 644 tests. As of 2026-06-30: 93 test files / 1281 tests.
- Documentation completeness: rescanned all 24 Markdown files in the project (excluding
node_modules/,dist/,coverage/), relative links intact, old npm / mcpServers / 177 tests / QUICKSTART references have been cleaned up.