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.
- package/CHANGELOG.md +83 -0
- package/LICENSE +21 -0
- package/README.md +207 -0
- package/dist/archive-transform.d.ts +45 -0
- package/dist/archive-transform.js +81 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +16 -0
- package/dist/doctor.d.ts +22 -0
- package/dist/doctor.js +44 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +306 -0
- package/dist/logging.d.ts +14 -0
- package/dist/logging.js +28 -0
- package/dist/options.d.ts +3 -0
- package/dist/options.js +217 -0
- package/dist/preview-providers.d.ts +20 -0
- package/dist/preview-providers.js +246 -0
- package/dist/privacy.d.ts +16 -0
- package/dist/privacy.js +92 -0
- package/dist/search-ranking.d.ts +12 -0
- package/dist/search-ranking.js +98 -0
- package/dist/sql-utils.d.ts +31 -0
- package/dist/sql-utils.js +80 -0
- package/dist/store-artifacts.d.ts +50 -0
- package/dist/store-artifacts.js +374 -0
- package/dist/store-retention.d.ts +39 -0
- package/dist/store-retention.js +90 -0
- package/dist/store-search.d.ts +37 -0
- package/dist/store-search.js +298 -0
- package/dist/store-snapshot.d.ts +133 -0
- package/dist/store-snapshot.js +325 -0
- package/dist/store-types.d.ts +14 -0
- package/dist/store-types.js +5 -0
- package/dist/store.d.ts +316 -0
- package/dist/store.js +3673 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +35 -0
- package/dist/utils.js +414 -0
- package/dist/workspace-path.d.ts +1 -0
- package/dist/workspace-path.js +15 -0
- package/dist/worktree-key.d.ts +1 -0
- package/dist/worktree-key.js +6 -0
- package/package.json +61 -0
- package/src/archive-transform.ts +147 -0
- package/src/bun-sqlite.d.ts +18 -0
- package/src/constants.ts +20 -0
- package/src/doctor.ts +83 -0
- package/src/index.ts +330 -0
- package/src/logging.ts +41 -0
- package/src/options.ts +297 -0
- package/src/preview-providers.ts +298 -0
- package/src/privacy.ts +122 -0
- package/src/search-ranking.ts +145 -0
- package/src/sql-utils.ts +107 -0
- package/src/store-artifacts.ts +666 -0
- package/src/store-retention.ts +152 -0
- package/src/store-search.ts +440 -0
- package/src/store-snapshot.ts +582 -0
- package/src/store-types.ts +16 -0
- package/src/store.ts +4926 -0
- package/src/types.ts +132 -0
- package/src/utils.ts +444 -0
- package/src/workspace-path.ts +20 -0
- 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
|
+
[](https://github.com/Plutarch01/opencode-lcm/actions/workflows/ci.yml)
|
|
4
|
+
[](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
|
+
<!--  -->
|
|
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;
|
package/dist/doctor.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED