opencode-lcm 0.11.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/LICENSE +21 -0
  3. package/README.md +207 -0
  4. package/dist/archive-transform.d.ts +45 -0
  5. package/dist/archive-transform.js +81 -0
  6. package/dist/constants.d.ts +12 -0
  7. package/dist/constants.js +16 -0
  8. package/dist/doctor.d.ts +22 -0
  9. package/dist/doctor.js +44 -0
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.js +306 -0
  12. package/dist/logging.d.ts +14 -0
  13. package/dist/logging.js +28 -0
  14. package/dist/options.d.ts +3 -0
  15. package/dist/options.js +217 -0
  16. package/dist/preview-providers.d.ts +20 -0
  17. package/dist/preview-providers.js +246 -0
  18. package/dist/privacy.d.ts +16 -0
  19. package/dist/privacy.js +92 -0
  20. package/dist/search-ranking.d.ts +12 -0
  21. package/dist/search-ranking.js +98 -0
  22. package/dist/sql-utils.d.ts +31 -0
  23. package/dist/sql-utils.js +80 -0
  24. package/dist/store-artifacts.d.ts +50 -0
  25. package/dist/store-artifacts.js +374 -0
  26. package/dist/store-retention.d.ts +39 -0
  27. package/dist/store-retention.js +90 -0
  28. package/dist/store-search.d.ts +37 -0
  29. package/dist/store-search.js +298 -0
  30. package/dist/store-snapshot.d.ts +133 -0
  31. package/dist/store-snapshot.js +325 -0
  32. package/dist/store-types.d.ts +14 -0
  33. package/dist/store-types.js +5 -0
  34. package/dist/store.d.ts +316 -0
  35. package/dist/store.js +3673 -0
  36. package/dist/types.d.ts +117 -0
  37. package/dist/types.js +1 -0
  38. package/dist/utils.d.ts +35 -0
  39. package/dist/utils.js +414 -0
  40. package/dist/workspace-path.d.ts +1 -0
  41. package/dist/workspace-path.js +15 -0
  42. package/dist/worktree-key.d.ts +1 -0
  43. package/dist/worktree-key.js +6 -0
  44. package/package.json +61 -0
  45. package/src/archive-transform.ts +147 -0
  46. package/src/bun-sqlite.d.ts +18 -0
  47. package/src/constants.ts +20 -0
  48. package/src/doctor.ts +83 -0
  49. package/src/index.ts +330 -0
  50. package/src/logging.ts +41 -0
  51. package/src/options.ts +297 -0
  52. package/src/preview-providers.ts +298 -0
  53. package/src/privacy.ts +122 -0
  54. package/src/search-ranking.ts +145 -0
  55. package/src/sql-utils.ts +107 -0
  56. package/src/store-artifacts.ts +666 -0
  57. package/src/store-retention.ts +152 -0
  58. package/src/store-search.ts +440 -0
  59. package/src/store-snapshot.ts +582 -0
  60. package/src/store-types.ts +16 -0
  61. package/src/store.ts +4926 -0
  62. package/src/types.ts +132 -0
  63. package/src/utils.ts +444 -0
  64. package/src/workspace-path.ts +20 -0
  65. package/src/worktree-key.ts +5 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,83 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+ - Opt-in `perf:archive` harness for large-archive regression coverage across transform, grep, snapshot, reopen, resume, and retention paths
12
+ - Separate advisory `Archive Performance` workflow for scheduled/manual perf runs with JSON artifact upload
13
+
14
+ ## [0.11.0] - 2026-04-02
15
+
16
+ ### Added
17
+ - Cross-platform CI matrix for Linux, Windows, and macOS on Node 22 and 24
18
+ - Opt-in CI dogfood smoke job for the existing `dogfood:opencode` flow, including workflow-managed OpenCode CLI installation
19
+ - Privacy controls for excluding tool payloads, suppressing matching file-path capture, and redacting configured regex patterns before archive storage/indexing
20
+
21
+ ### Changed
22
+ - `lcm_status` now reports configured privacy-control counts and excluded tool prefixes
23
+ - `tests/store.test.mjs` cleanup now retries transient Windows SQLite file-lock races
24
+ - Archived recall and summary reminders now use terse inline formatting and attach after the active user text instead of leading it
25
+ - Automatic-retrieval sanitization now strips archived reminder boilerplate before indexing and artifact externalization
26
+
27
+ ### Fixed
28
+ - Pasted reminder text no longer pollutes retrieval candidates or dominates later archive recall
29
+
30
+ ## [0.1.0] - 2026-03-31
31
+
32
+ ### Added
33
+ - Initial release of opencode-lcm plugin
34
+ - SQLite-based session storage with FTS5 full-text search
35
+ - Hierarchical summary graph for message archiving
36
+ - Artifact deduplication via content hashing
37
+ - Snapshot export/import for portable session data
38
+ - Automatic retrieval with TF-IDF query weighting
39
+ - Session lineage tracking (parent/child/root relationships)
40
+ - Retention policy enforcement (stale sessions, deleted sessions, orphan blobs)
41
+ - Resume notes for session continuation
42
+ - Doctor command for diagnosing and repairing store integrity
43
+ - Binary preview providers for file artifacts (image dimensions, PDF metadata, ZIP entries)
44
+ - Context-mode interop for sandboxed command execution
45
+ - Worktree-aware scoping for multi-workspace projects
46
+ - 141 tests covering core functionality
47
+ - `CHANGELOG.md` for tracking release history
48
+ - Direct regression coverage for scoped FTS refresh and snapshot replace-import stale-row cleanup
49
+
50
+ ### Changed
51
+ - `parseJson()` now wraps errors with input preview and original message for easier debugging
52
+ - Silent `catch {}` blocks in `store-search.ts` replaced with `getLogger().debug()` logging
53
+ - Bun SQLite import replaced with direct `await import('bun:sqlite')` + ambient type declaration (`bun-sqlite.d.ts`)
54
+ - Duplicated artifact hydration switch/case extracted into `hydratePartFromArtifacts()` helper
55
+ - All manual `BEGIN/COMMIT/ROLLBACK` blocks replaced with `withTransaction()` helper
56
+ - Row type definitions unified — `store-snapshot.ts` is the canonical source
57
+ - `validateRow()` added to `sql-utils.ts` for runtime SQL result validation; applied to all `stats()` method queries
58
+ - Dead modules (`store-schema.ts`, `store-session-read.ts`) folded into `store.ts` as private functions with re-exports for test compatibility
59
+ - TF-IDF `filterTokensByTfidf()` doc-frequency ratio fixed to use actual `docFreq/totalDocs`
60
+ - `computeTfidfWeights()` return type extended with `docFreq` field
61
+ - `buildFtsQuery()` now preserves quoted phrases as FTS5 phrase clauses
62
+ - `resolveWorkspacePath()` absolute-path bypass fixed — absolute paths now validated against workspace root
63
+ - Duplicate `COUNT(*)` queries in TF-IDF eliminated — extracted `getTotalDocCount()` helper
64
+ - `importStoreSnapshot()` manual transaction replaced with `withTransaction()`
65
+ - Deduplicated `truncate()`, `shortNodeID()` from `archive-transform.ts` → imported from `utils.ts`
66
+ - Deduplicated `clamp()` from `store.ts` → imported from `utils.ts`
67
+ - Snapshot paths now support absolute paths (portable snapshots) and relative paths resolved from workspace with traversal guard
68
+ - `resolveWorkspacePath()` false-positive fix: names like `..hidden` no longer rejected
69
+ - Search index maintenance can now refresh only selected sessions instead of always rebuilding every FTS table
70
+ - Binary preview providers now use async file reads, and session/message externalization awaits preview generation before writing transactionally
71
+ - Test workspace cleanup now retries transient Windows SQLite file-lock races
72
+
73
+ ### Fixed
74
+ - TF-IDF retrieval filtering bug where document frequency ratio was computed incorrectly
75
+ - Phrase query support broken in FTS5 — quoted strings now passed through as phrase clauses
76
+ - Workspace path security bypass where absolute paths skipped containment check
77
+ - Snapshot path resolution broke portable snapshot imports
78
+ - `store-retention.ts` module recreated after accidental deletion
79
+ - Snapshot replace-import left stale FTS rows behind for replaced sessions
80
+
81
+ ### Security
82
+ - Fixed workspace path validation bypass that allowed absolute paths to escape the workspace root
83
+ - Added `validateRow()` runtime validation for all SQL query results in `stats()` method
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenCode LCM contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # opencode-lcm
2
+
3
+ [![CI](https://github.com/Plutarch01/opencode-lcm/actions/workflows/ci.yml/badge.svg)](https://github.com/Plutarch01/opencode-lcm/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ A transparent long-memory plugin for [OpenCode](https://github.com/sst/opencode), based on the [Lossless Context Memory (LCM)](https://papers.voltropy.com/LCM) research. It captures older session context outside the active prompt, compresses it into searchable summaries and artifacts, then automatically recalls relevant details back into the prompt when the current turn needs them. The model does not become smarter, but it behaves much better across long, compacted sessions because important prior context stops disappearing.
7
+
8
+ <!-- Add a demo screenshot or GIF here -->
9
+ <!-- ![opencode-lcm in action](assets/images/lcm-demo.png) -->
10
+
11
+ > [!NOTE]
12
+ > This is an early community plugin for OpenCode and is not affiliated with or endorsed by the OpenCode project. Behavior, internals, and configuration may change as the project evolves.
13
+
14
+ ## context-mode
15
+
16
+ `opencode-lcm` preserves archived conversation context so the assistant can recall earlier decisions without re-reading old files. Pairing it with [context-mode](https://github.com/mksglu/context-mode/) reduces tool-output token waste and keeps the active prompt lean.
17
+
18
+ ## Installation
19
+
20
+ Add to your `opencode.json` (project or global `~/.config/opencode/opencode.json`):
21
+
22
+ ```json
23
+ {
24
+ "$schema": "https://opencode.ai/config.json",
25
+ "plugin": ["opencode-lcm"]
26
+ }
27
+ ```
28
+
29
+ OpenCode will automatically download the latest version from npm on startup. No manual install needed.
30
+
31
+ ### From source
32
+
33
+ ```sh
34
+ git clone https://github.com/plutarch01/opencode-lcm.git
35
+ cd opencode-lcm
36
+ npm install
37
+ npm run build
38
+ ```
39
+
40
+ ## How It Works
41
+
42
+ OpenCode handles compaction normally — when the conversation gets too large, it shrinks the prompt. `opencode-lcm` works alongside that by saving older details *outside* the prompt, then searching that archive later to pull back only what matters.
43
+
44
+ ### Archive
45
+
46
+ Listens to OpenCode events and stores session state, messages, parts, and artifacts in `.lcm/lcm.db`. Builds deterministic summary nodes for archived turns and automatically repairs summary, index, lineage, and resume drift.
47
+
48
+ ### Automatic Recall
49
+
50
+ Inserts archived context into the prompt via `experimental.chat.messages.transform`. Starts with the current session and escalates to broader scopes when needed. Uses TF-IDF weighted retrieval with bigram phrase queries for corpus-aware ranking.
51
+
52
+ ### Resume Notes
53
+
54
+ Appends a compact resume note during compaction so important context survives the shrink without overriding the compaction prompt.
55
+
56
+ ## Capabilities
57
+
58
+ - **Session lineage** — track parent/root relationships for branched sessions
59
+ - **Artifact externalization** — deduplicated storage with metadata for oversized payloads
60
+ - **FTS search** — SQLite FTS5 across archived messages, summaries, and artifacts
61
+ - **Snapshot export/import** — portable snapshots with safe merge and worktree modes
62
+ - **Privacy controls** — tool-output exclusion, path-based capture exclusion, regex redaction
63
+ - **Configurable retrieval** — scope ordering, per-scope budgets, stop rules, recency-aware ranking
64
+ - **16 tools** — `lcm_status`, `lcm_resume`, `lcm_grep`, `lcm_describe`, `lcm_lineage`, `lcm_expand`, `lcm_artifact`, `lcm_pin_session`, `lcm_unpin_session`, `lcm_blob_stats`, `lcm_blob_gc`, `lcm_doctor`, `lcm_retention_report`, `lcm_retention_prune`, `lcm_export_snapshot`, `lcm_import_snapshot`
65
+ - **Legacy migration** — auto-migrates `.lcm/events.jsonl`, `.lcm/resume.json`, `.lcm/sessions/*.json`
66
+
67
+ ## Configuration
68
+
69
+ Add `opencode-lcm` to your `opencode.json` (project or global `~/.config/opencode/opencode.json`):
70
+
71
+ ```json
72
+ {
73
+ "$schema": "https://opencode.ai/config.json",
74
+ "plugin": [
75
+ ["opencode-lcm", {
76
+ "scopeDefaults": { "grep": "session", "describe": "session" },
77
+ "automaticRetrieval": {
78
+ "enabled": true,
79
+ "scopeOrder": ["session", "root", "worktree"],
80
+ "scopeBudgets": { "session": 16, "root": 12, "worktree": 8, "all": 6 }
81
+ },
82
+ "retention": {
83
+ "staleSessionDays": 90,
84
+ "deletedSessionDays": 30,
85
+ "orphanBlobDays": 14
86
+ }
87
+ }]
88
+ ]
89
+ }
90
+ ```
91
+
92
+ > [!IMPORTANT]
93
+ > All defaults are applied automatically. Expand below only if you need to override settings.
94
+
95
+ <details>
96
+ <summary><strong>Full Configuration</strong> (click to expand)</summary>
97
+
98
+ ```json
99
+ {
100
+ "$schema": "https://opencode.ai/config.json",
101
+ "plugin": [
102
+ ["opencode-lcm", {
103
+ "scopeDefaults": {
104
+ "grep": "session",
105
+ "describe": "session"
106
+ },
107
+ "retention": {
108
+ "staleSessionDays": 90,
109
+ "deletedSessionDays": 30,
110
+ "orphanBlobDays": 14
111
+ },
112
+ "privacy": {
113
+ "excludeToolPrefixes": ["playwright_browser_"],
114
+ "excludePathPatterns": ["[\\\\/]secrets[\\\\/]", "\\\\.env($|\\\\.)"],
115
+ "redactPatterns": ["sk-[A-Za-z0-9_-]+", "ZX729ALBATROSS"]
116
+ },
117
+ "automaticRetrieval": {
118
+ "enabled": true,
119
+ "scopeOrder": ["session", "root", "worktree"],
120
+ "scopeBudgets": {
121
+ "session": 16,
122
+ "root": 12,
123
+ "worktree": 8,
124
+ "all": 6
125
+ },
126
+ "stop": {
127
+ "targetHits": 3,
128
+ "stopOnFirstScopeWithHits": false
129
+ },
130
+ "maxMessageHits": 2,
131
+ "maxSummaryHits": 1,
132
+ "maxArtifactHits": 1
133
+ },
134
+ "freshTailMessages": 10,
135
+ "minMessagesForTransform": 16,
136
+ "summaryCharBudget": 1500,
137
+ "systemHint": true,
138
+ "binaryPreviewProviders": ["fingerprint", "byte-peek", "image-dimensions", "pdf-metadata"],
139
+ "previewBytePeek": 16
140
+ }]
141
+ ]
142
+ }
143
+ ```
144
+
145
+ </details>
146
+
147
+ ## Privacy Controls
148
+
149
+ Privacy patterns run before archived content is stored or indexed.
150
+
151
+ - **`excludeToolPrefixes`** — do not archive tool payloads for matching tools
152
+ - **`excludePathPatterns`** — suppress file capture for matching paths, redact matching path strings
153
+ - **`redactPatterns`** — replace matching content with `[REDACTED]` before storage and indexing
154
+
155
+ These controls are not encryption and not retroactive. Existing archived rows keep their previous content until rewritten.
156
+
157
+ ## context-mode Interop
158
+
159
+ Pairing with [context-mode](https://github.com/mksglu/context-mode/) reduces tool-output token waste. Add the `interop` block to avoid hook conflicts:
160
+
161
+ ```json
162
+ {
163
+ "$schema": "https://opencode.ai/config.json",
164
+ "mcp": {
165
+ "context-mode": {
166
+ "type": "local",
167
+ "command": ["context-mode"]
168
+ }
169
+ },
170
+ "plugin": [
171
+ "context-mode",
172
+ ["opencode-lcm", {
173
+ "interop": {
174
+ "contextMode": true,
175
+ "neverOverrideCompactionPrompt": true,
176
+ "ignoreToolPrefixes": ["ctx_"]
177
+ },
178
+ "scopeDefaults": { "grep": "session", "describe": "session" },
179
+ "retention": {
180
+ "staleSessionDays": 90,
181
+ "deletedSessionDays": 30,
182
+ "orphanBlobDays": 14
183
+ }
184
+ }]
185
+ ]
186
+ }
187
+ ```
188
+
189
+ ## Disable
190
+
191
+ Remove `opencode-lcm` from the `plugin` array and restart OpenCode. To keep the archive but stop automatic recall, set `automaticRetrieval.enabled` to `false`.
192
+
193
+ ## Performance
194
+
195
+ Run the opt-in archive performance harness locally:
196
+
197
+ ```sh
198
+ npm run perf:archive -- --json-out perf-results/archive-perf.json
199
+ ```
200
+
201
+ Useful knobs: `--medium-messages`, `--large-messages`, `--samples`, `--warm-runs`, `--keep-workspaces`.
202
+
203
+ There is also a separate `Archive Performance` GitHub Actions workflow for scheduled/manual advisory runs.
204
+
205
+ ## License
206
+
207
+ MIT
@@ -0,0 +1,45 @@
1
+ import type { ConversationMessage, SearchResult } from './types.js';
2
+ export type AutomaticRetrievalHit = {
3
+ kind: 'message' | 'summary' | 'artifact';
4
+ id: string;
5
+ label: string;
6
+ sessionID?: string;
7
+ snippet: string;
8
+ };
9
+ export type ArchiveSummaryRoot = {
10
+ nodeID: string;
11
+ summaryText: string;
12
+ };
13
+ export type ArchiveTransformWindow = {
14
+ anchor: ConversationMessage;
15
+ archived: ConversationMessage[];
16
+ recent: ConversationMessage[];
17
+ recentStart: number;
18
+ };
19
+ export type AutomaticRetrievalTelemetry = {
20
+ queries: string[];
21
+ rawResults: number;
22
+ stopReason: string;
23
+ scopeStats: Array<{
24
+ scope: string;
25
+ budget: number;
26
+ rawResults: number;
27
+ selectedHits: number;
28
+ }>;
29
+ };
30
+ type AutomaticRetrievalQuotas = {
31
+ message: number;
32
+ summary: number;
33
+ artifact: number;
34
+ };
35
+ export declare function resolveArchiveTransformWindow(messages: ConversationMessage[], freshTailMessages: number): ArchiveTransformWindow | undefined;
36
+ export declare function selectAutomaticRetrievalHits(input: {
37
+ recent: ConversationMessage[];
38
+ tokens: string[];
39
+ results: SearchResult[];
40
+ quotas: AutomaticRetrievalQuotas;
41
+ isFreshResult: (result: SearchResult, freshMessageIDs: Set<string>) => boolean;
42
+ }): AutomaticRetrievalHit[];
43
+ export declare function renderAutomaticRetrievalContext(scopes: string | string[], hits: AutomaticRetrievalHit[], maxChars: number, _telemetry?: AutomaticRetrievalTelemetry): string;
44
+ export declare function buildActiveSummaryText(roots: ArchiveSummaryRoot[], archivedCount: number, maxChars: number): string;
45
+ export {};
@@ -0,0 +1,81 @@
1
+ import { shortNodeID, truncate } from './utils.js';
2
+ function pluralize(count, singular, plural = `${singular}s`) {
3
+ return count === 1 ? singular : plural;
4
+ }
5
+ function formatAutomaticRetrievalHit(hit) {
6
+ const session = hit.sessionID ? ` session=${hit.sessionID}` : '';
7
+ const id = hit.kind === 'summary' ? shortNodeID(hit.id) : hit.id;
8
+ const label = hit.label !== hit.kind ? ` (${hit.label})` : '';
9
+ return `${hit.kind}${session} id=${id}${label}: ${truncate(hit.snippet, 180)}`;
10
+ }
11
+ export function resolveArchiveTransformWindow(messages, freshTailMessages) {
12
+ let latestUserIndex = -1;
13
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
14
+ if (messages[index]?.info.role === 'user') {
15
+ latestUserIndex = index;
16
+ break;
17
+ }
18
+ }
19
+ if (latestUserIndex < 0)
20
+ return undefined;
21
+ let recentStart = Math.max(0, messages.length - Math.max(0, freshTailMessages));
22
+ if (latestUserIndex < recentStart) {
23
+ recentStart = latestUserIndex;
24
+ }
25
+ if (recentStart <= 0)
26
+ return undefined;
27
+ return {
28
+ anchor: messages[latestUserIndex],
29
+ archived: messages.slice(0, recentStart),
30
+ recent: messages.slice(recentStart),
31
+ recentStart,
32
+ };
33
+ }
34
+ export function selectAutomaticRetrievalHits(input) {
35
+ const freshMessageIDs = new Set(input.recent.map((message) => message.info.id));
36
+ const quotas = { ...input.quotas };
37
+ // With few tokens each one is critical — require at least 2 token matches when possible
38
+ const minSnippetMatches = input.tokens.length >= 2 ? 2 : 1;
39
+ const hits = [];
40
+ for (const result of input.results) {
41
+ const kind = result.type === 'summary'
42
+ ? 'summary'
43
+ : result.type.startsWith('artifact:')
44
+ ? 'artifact'
45
+ : 'message';
46
+ if (quotas[kind] <= 0)
47
+ continue;
48
+ if (input.isFreshResult(result, freshMessageIDs))
49
+ continue;
50
+ const lowerSnippet = result.snippet.toLowerCase();
51
+ const matchedTokens = input.tokens.filter((token) => lowerSnippet.includes(token)).length;
52
+ if (matchedTokens < minSnippetMatches && input.tokens.length > 1)
53
+ continue;
54
+ hits.push({
55
+ kind,
56
+ id: result.id,
57
+ label: result.type,
58
+ sessionID: result.sessionID,
59
+ snippet: result.snippet,
60
+ });
61
+ quotas[kind] -= 1;
62
+ if (quotas.message <= 0 && quotas.summary <= 0 && quotas.artifact <= 0)
63
+ break;
64
+ }
65
+ return hits;
66
+ }
67
+ export function renderAutomaticRetrievalContext(scopes, hits, maxChars, _telemetry) {
68
+ const scopeLabel = Array.isArray(scopes) ? scopes.join(' -> ') : scopes;
69
+ const lines = [
70
+ `[Archived by opencode-lcm: recalled ${hits.length} archived ${pluralize(hits.length, 'hit')} for this turn (scope=${scopeLabel}).]`,
71
+ `Archived hits: ${hits.map((hit) => formatAutomaticRetrievalHit(hit)).join(' | ')}`,
72
+ ];
73
+ return truncate(lines.join('\n'), maxChars);
74
+ }
75
+ export function buildActiveSummaryText(roots, archivedCount, maxChars) {
76
+ const lines = [
77
+ `[Archived by opencode-lcm: compacted ${archivedCount} older conversation ${pluralize(archivedCount, 'turn')} into ${roots.length} archived summary ${pluralize(roots.length, 'node')}.]`,
78
+ `Summary roots: ${roots.map((node) => `${node.nodeID}: ${truncate(node.summaryText, 140)}`).join(' | ')}`,
79
+ ];
80
+ return truncate(lines.join('\n'), maxChars);
81
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Store-level constants used across the SQLite LCM store.
3
+ * These are configuration-like values that control store behavior.
4
+ */
5
+ export declare const SUMMARY_LEAF_MESSAGES = 6;
6
+ export declare const SUMMARY_BRANCH_FACTOR = 3;
7
+ export declare const SUMMARY_NODE_CHAR_LIMIT = 260;
8
+ export declare const STORE_SCHEMA_VERSION = 1;
9
+ export declare const EXPAND_MESSAGE_LIMIT = 6;
10
+ export declare const AUTOMATIC_RETRIEVAL_QUERY_TOKENS = 8;
11
+ export declare const AUTOMATIC_RETRIEVAL_RECENT_MESSAGES = 3;
12
+ export declare const AUTOMATIC_RETRIEVAL_QUERY_VARIANTS = 8;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Store-level constants used across the SQLite LCM store.
3
+ * These are configuration-like values that control store behavior.
4
+ */
5
+ // Summary DAG configuration
6
+ export const SUMMARY_LEAF_MESSAGES = 6;
7
+ export const SUMMARY_BRANCH_FACTOR = 3;
8
+ export const SUMMARY_NODE_CHAR_LIMIT = 260;
9
+ // Store schema
10
+ export const STORE_SCHEMA_VERSION = 1;
11
+ // Message retrieval limits
12
+ export const EXPAND_MESSAGE_LIMIT = 6;
13
+ // Automatic retrieval configuration
14
+ export const AUTOMATIC_RETRIEVAL_QUERY_TOKENS = 8;
15
+ export const AUTOMATIC_RETRIEVAL_RECENT_MESSAGES = 3;
16
+ export const AUTOMATIC_RETRIEVAL_QUERY_VARIANTS = 8;
@@ -0,0 +1,22 @@
1
+ export type DoctorSessionIssue = {
2
+ sessionID: string;
3
+ issues: string[];
4
+ };
5
+ export type DoctorCountCheck = {
6
+ expected: number;
7
+ actual: number;
8
+ };
9
+ export type DoctorReport = {
10
+ scope: string;
11
+ checkedSessions: number;
12
+ summarySessionsNeedingRebuild: DoctorSessionIssue[];
13
+ lineageSessionsNeedingRefresh: string[];
14
+ orphanSummaryEdges: number;
15
+ messageFts: DoctorCountCheck;
16
+ summaryFts: DoctorCountCheck;
17
+ artifactFts: DoctorCountCheck;
18
+ orphanArtifactBlobs: number;
19
+ status: 'clean' | 'issues-found' | 'repaired';
20
+ appliedActions?: string[];
21
+ };
22
+ export declare function formatDoctorReport(report: DoctorReport, limit: number): string;
package/dist/doctor.js ADDED
@@ -0,0 +1,44 @@
1
+ function formatCountCheck(label, value) {
2
+ return [
3
+ `${label}_expected=${value.expected}`,
4
+ `${label}_actual=${value.actual}`,
5
+ `${label}_delta=${value.expected - value.actual}`,
6
+ ];
7
+ }
8
+ export function formatDoctorReport(report, limit) {
9
+ const issueCount = report.summarySessionsNeedingRebuild.length +
10
+ report.lineageSessionsNeedingRefresh.length +
11
+ report.orphanSummaryEdges +
12
+ Math.abs(report.messageFts.expected - report.messageFts.actual) +
13
+ Math.abs(report.summaryFts.expected - report.summaryFts.actual) +
14
+ Math.abs(report.artifactFts.expected - report.artifactFts.actual) +
15
+ report.orphanArtifactBlobs;
16
+ const lines = [
17
+ `checked_scope=${report.scope}`,
18
+ `checked_sessions=${report.checkedSessions}`,
19
+ `summary_sessions_needing_rebuild=${report.summarySessionsNeedingRebuild.length}`,
20
+ `lineage_sessions_needing_refresh=${report.lineageSessionsNeedingRefresh.length}`,
21
+ `orphan_summary_edges=${report.orphanSummaryEdges}`,
22
+ ...formatCountCheck('message_fts', report.messageFts),
23
+ ...formatCountCheck('summary_fts', report.summaryFts),
24
+ ...formatCountCheck('artifact_fts', report.artifactFts),
25
+ `orphan_artifact_blobs=${report.orphanArtifactBlobs}`,
26
+ `issues=${issueCount}`,
27
+ `status=${report.status}`,
28
+ ];
29
+ if (report.summarySessionsNeedingRebuild.length > 0) {
30
+ lines.push('summary_session_preview:', ...report.summarySessionsNeedingRebuild
31
+ .slice(0, limit)
32
+ .map((issue) => `- ${issue.sessionID}: ${issue.issues.join(', ')}`));
33
+ }
34
+ if (report.lineageSessionsNeedingRefresh.length > 0) {
35
+ lines.push('lineage_session_preview:', ...report.lineageSessionsNeedingRefresh.slice(0, limit).map((sessionID) => `- ${sessionID}`));
36
+ }
37
+ if (report.appliedActions && report.appliedActions.length > 0) {
38
+ lines.push('applied_actions:', ...report.appliedActions.slice(0, limit).map((action) => `- ${action}`));
39
+ }
40
+ else if (report.status === 'issues-found') {
41
+ lines.push('Re-run with apply=true to repair the issues above.');
42
+ }
43
+ return lines.join('\n');
44
+ }
@@ -0,0 +1,4 @@
1
+ import { type Hooks, type PluginInput } from '@opencode-ai/plugin';
2
+ type PluginWithOptions = (ctx: PluginInput, rawOptions?: unknown) => Promise<Hooks>;
3
+ export declare const OpencodeLcmPlugin: PluginWithOptions;
4
+ export default OpencodeLcmPlugin;