opencode-lcm 0.13.1 → 0.13.2

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 CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.13.2] - 2026-04-08
11
+
12
+ ### Fixed
13
+ - Republish of 0.13.1 malformed-message hardening through CI with provenance
14
+
10
15
  ## [0.13.1] - 2026-04-07
11
16
 
12
17
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-lcm",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "description": "Long-memory plugin for OpenCode with context-mode interop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,73 +0,0 @@
1
- import type { SqlDatabaseLike } from './store-types.js';
2
- /**
3
- * Doctor diagnostics operations.
4
- * Analyzes store health: summary graph integrity, FTS index consistency, orphan detection.
5
- */
6
- export type DoctorSessionIssue = {
7
- sessionID: string;
8
- issues: string[];
9
- };
10
- export type DoctorReport = {
11
- scope: string;
12
- checkedSessions: number;
13
- summarySessionsNeedingRebuild: DoctorSessionIssue[];
14
- lineageSessionsNeedingRefresh: string[];
15
- orphanSummaryEdges: number;
16
- messageFts: {
17
- expected: number;
18
- actual: number;
19
- };
20
- summaryFts: {
21
- expected: number;
22
- actual: number;
23
- };
24
- artifactFts: {
25
- expected: number;
26
- actual: number;
27
- };
28
- orphanArtifactBlobs: number;
29
- status: 'clean' | 'issues-found';
30
- };
31
- type SessionSnapshot = {
32
- sessionID: string;
33
- messages: {
34
- info: {
35
- id: string;
36
- time: {
37
- created: number;
38
- };
39
- };
40
- parts: unknown[];
41
- }[];
42
- rootSessionID?: string;
43
- lineageDepth?: number;
44
- };
45
- type SummaryNodeRow = {
46
- node_id: string;
47
- session_id: string;
48
- level: number;
49
- slot: number;
50
- archived_message_ids_json: string;
51
- summary_text: string;
52
- created_at: number;
53
- };
54
- type DoctorDeps = {
55
- db: SqlDatabaseLike;
56
- getArchivedMessages: (messages: SessionSnapshot['messages']) => SessionSnapshot['messages'];
57
- buildArchivedSignature: (messages: SessionSnapshot['messages']) => string;
58
- readSummaryNode: (nodeID: string) => SummaryNodeRow | undefined;
59
- canReuseSummaryGraph: (sessionID: string, archived: SessionSnapshot['messages'], roots: SummaryNodeRow[]) => boolean;
60
- readScopedSummaryRows: (sessionIDs?: string[]) => unknown[];
61
- readScopedArtifactRows: (sessionIDs?: string[]) => unknown[];
62
- readOrphanArtifactBlobRows: () => unknown[];
63
- countScopedFtsRows: (table: 'message_fts' | 'summary_fts' | 'artifact_fts', sessionIDs?: string[]) => number;
64
- countScopedOrphanSummaryEdges: (sessionIDs?: string[]) => number;
65
- guessMessageText: (message: SessionSnapshot['messages'][number], ignorePrefixes: string[]) => string;
66
- ignoreToolPrefixes: string[];
67
- parseJson: <T>(value: string) => T;
68
- };
69
- export declare function collectDoctorReport(sessions: SessionSnapshot[], sessionID: string | undefined, deps: DoctorDeps, readLineageChain: (sessionID: string) => {
70
- sessionID: string;
71
- }[]): DoctorReport;
72
- export declare function hasDoctorIssues(report: DoctorReport): boolean;
73
- export {};
@@ -1,106 +0,0 @@
1
- function countFtsExpected(sessions, deps) {
2
- return sessions.reduce((count, session) => {
3
- return (count +
4
- session.messages.filter((message) => deps.guessMessageText(message, deps.ignoreToolPrefixes).length > 0).length);
5
- }, 0);
6
- }
7
- function diagnoseSummarySession(session, deps) {
8
- const issues = [];
9
- const archived = deps.getArchivedMessages(session.messages);
10
- const state = deps.db
11
- .prepare('SELECT * FROM summary_state WHERE session_id = ?')
12
- .get(session.sessionID);
13
- const summaryNodeCount = deps.db
14
- .prepare('SELECT COUNT(*) AS count FROM summary_nodes WHERE session_id = ?')
15
- .get(session.sessionID);
16
- const summaryEdgeCount = deps.db
17
- .prepare('SELECT COUNT(*) AS count FROM summary_edges WHERE session_id = ?')
18
- .get(session.sessionID);
19
- if (archived.length === 0) {
20
- if (state)
21
- issues.push('unexpected-summary-state');
22
- if (summaryNodeCount.count > 0)
23
- issues.push('unexpected-summary-nodes');
24
- if (summaryEdgeCount.count > 0)
25
- issues.push('unexpected-summary-edges');
26
- return issues.length > 0 ? { sessionID: session.sessionID, issues } : undefined;
27
- }
28
- const latestMessageCreated = archived.at(-1)?.info.time.created ?? 0;
29
- const archivedSignature = deps.buildArchivedSignature(archived);
30
- const rootIDs = state ? deps.parseJson(state.root_node_ids_json) : [];
31
- const roots = rootIDs
32
- .map((nodeID) => deps.readSummaryNode(nodeID))
33
- .filter((node) => Boolean(node));
34
- if (!state) {
35
- issues.push('missing-summary-state');
36
- }
37
- else {
38
- if (state.archived_count !== archived.length)
39
- issues.push('archived-count-mismatch');
40
- if (state.latest_message_created !== latestMessageCreated)
41
- issues.push('latest-message-mismatch');
42
- if (state.archived_signature !== archivedSignature)
43
- issues.push('archived-signature-mismatch');
44
- if (rootIDs.length === 0)
45
- issues.push('missing-root-node-ids');
46
- if (roots.length !== rootIDs.length) {
47
- issues.push('missing-root-node-record');
48
- }
49
- else if (rootIDs.length > 0 &&
50
- !deps.canReuseSummaryGraph(session.sessionID, archived, roots)) {
51
- issues.push('invalid-summary-graph');
52
- }
53
- }
54
- if (summaryNodeCount.count === 0)
55
- issues.push('missing-summary-nodes');
56
- return issues.length > 0 ? { sessionID: session.sessionID, issues } : undefined;
57
- }
58
- function needsLineageRefresh(session, readLineageChain) {
59
- const chain = readLineageChain(session.sessionID);
60
- const expectedRoot = chain[0]?.sessionID ?? session.sessionID;
61
- const expectedDepth = Math.max(0, chain.length - 1);
62
- return ((session.rootSessionID ?? session.sessionID) !== expectedRoot ||
63
- (session.lineageDepth ?? 0) !== expectedDepth);
64
- }
65
- export function collectDoctorReport(sessions, sessionID, deps, readLineageChain) {
66
- const sessionIDs = sessions.map((session) => session.sessionID);
67
- const summarySessionsNeedingRebuild = sessions
68
- .map((session) => diagnoseSummarySession(session, deps))
69
- .filter((issue) => Boolean(issue));
70
- const lineageSessionsNeedingRefresh = sessions
71
- .filter((session) => needsLineageRefresh(session, readLineageChain))
72
- .map((session) => session.sessionID);
73
- const messageFtsExpected = countFtsExpected(sessions, deps);
74
- const report = {
75
- scope: sessionID ? `session:${sessionID}` : 'all',
76
- checkedSessions: sessions.length,
77
- summarySessionsNeedingRebuild,
78
- lineageSessionsNeedingRefresh,
79
- orphanSummaryEdges: deps.countScopedOrphanSummaryEdges(sessionIDs),
80
- messageFts: {
81
- expected: messageFtsExpected,
82
- actual: deps.countScopedFtsRows('message_fts', sessionIDs),
83
- },
84
- summaryFts: {
85
- expected: deps.readScopedSummaryRows(sessionIDs).length,
86
- actual: deps.countScopedFtsRows('summary_fts', sessionIDs),
87
- },
88
- artifactFts: {
89
- expected: deps.readScopedArtifactRows(sessionIDs).length,
90
- actual: deps.countScopedFtsRows('artifact_fts', sessionIDs),
91
- },
92
- orphanArtifactBlobs: deps.readOrphanArtifactBlobRows().length,
93
- status: 'clean',
94
- };
95
- report.status = hasDoctorIssues(report) ? 'issues-found' : 'clean';
96
- return report;
97
- }
98
- export function hasDoctorIssues(report) {
99
- return (report.summarySessionsNeedingRebuild.length > 0 ||
100
- report.lineageSessionsNeedingRefresh.length > 0 ||
101
- report.orphanSummaryEdges > 0 ||
102
- report.messageFts.expected !== report.messageFts.actual ||
103
- report.summaryFts.expected !== report.summaryFts.actual ||
104
- report.artifactFts.expected !== report.artifactFts.actual ||
105
- report.orphanArtifactBlobs > 0);
106
- }
@@ -1,8 +0,0 @@
1
- import type { SqlDatabaseLike } from './store-types.js';
2
- /**
3
- * Schema version management operations.
4
- * Handles reading, writing, and validating the SQLite store schema version.
5
- */
6
- export declare function readSchemaVersionSync(db: SqlDatabaseLike): number;
7
- export declare function assertSupportedSchemaVersionSync(db: SqlDatabaseLike, maxVersion: number): void;
8
- export declare function writeSchemaVersionSync(db: SqlDatabaseLike, version: number): void;
@@ -1,23 +0,0 @@
1
- /**
2
- * Schema version management operations.
3
- * Handles reading, writing, and validating the SQLite store schema version.
4
- */
5
- export function readSchemaVersionSync(db) {
6
- const result = db.prepare('PRAGMA user_version').get();
7
- if (!result || typeof result !== 'object')
8
- return 0;
9
- for (const value of Object.values(result)) {
10
- if (typeof value === 'number' && Number.isFinite(value))
11
- return value;
12
- }
13
- return 0;
14
- }
15
- export function assertSupportedSchemaVersionSync(db, maxVersion) {
16
- const schemaVersion = readSchemaVersionSync(db);
17
- if (schemaVersion <= maxVersion)
18
- return;
19
- throw new Error(`Unsupported store schema version: ${schemaVersion}. This build supports up to ${maxVersion}.`);
20
- }
21
- export function writeSchemaVersionSync(db, version) {
22
- db.exec(`PRAGMA user_version = ${Math.max(0, Math.trunc(version))}`);
23
- }
@@ -1,97 +0,0 @@
1
- import type { SqlDatabaseLike } from './store-types.js';
2
- /**
3
- * Session read operations.
4
- * Handles reading sessions, messages, parts, artifacts from the store.
5
- */
6
- export type SessionRow = {
7
- session_id: string;
8
- title: string | null;
9
- parent_session_id: string | null;
10
- root_session_id: string | null;
11
- lineage_depth: number | null;
12
- session_directory: string | null;
13
- worktree_key: string | null;
14
- pinned: number;
15
- pin_reason: string | null;
16
- deleted: number;
17
- updated_at: number;
18
- created_at: number;
19
- event_count: number;
20
- };
21
- export type MessageRow = {
22
- session_id: string;
23
- message_id: string;
24
- role: string;
25
- created_at: number;
26
- };
27
- export type PartRow = {
28
- session_id: string;
29
- message_id: string;
30
- part_id: string;
31
- part_type: string;
32
- sort_key: number;
33
- state_json: string;
34
- created_at: number;
35
- };
36
- export type ArtifactRow = {
37
- artifact_id: string;
38
- session_id: string;
39
- message_id: string;
40
- part_id: string;
41
- artifact_kind: string;
42
- field_name: string;
43
- content_hash: string | null;
44
- preview_text: string;
45
- metadata_json: string;
46
- char_count: number;
47
- created_at: number;
48
- };
49
- export type ArtifactBlobRow = {
50
- content_hash: string;
51
- content_text: string;
52
- char_count: number;
53
- created_at: number;
54
- };
55
- export type SummaryNodeRow = {
56
- node_id: string;
57
- session_id: string;
58
- level: number;
59
- slot: number;
60
- archived_message_ids_json: string;
61
- summary_text: string;
62
- created_at: number;
63
- };
64
- export type SummaryEdgeRow = {
65
- session_id: string;
66
- parent_id: string;
67
- child_id: string;
68
- child_position: number;
69
- };
70
- export type SummaryStateRow = {
71
- session_id: string;
72
- archived_count: number;
73
- latest_message_created: number;
74
- archived_signature: string;
75
- root_node_ids_json: string;
76
- updated_at: number;
77
- };
78
- export declare function readSessionHeader(db: SqlDatabaseLike, sessionID: string): SessionRow | undefined;
79
- export declare function readAllSessions(db: SqlDatabaseLike): SessionRow[];
80
- export declare function readChildSessions(db: SqlDatabaseLike, parentSessionID: string): SessionRow[];
81
- export declare function readLineageChain(db: SqlDatabaseLike, sessionID: string): SessionRow[];
82
- export declare function readMessagesForSession(db: SqlDatabaseLike, sessionID: string): MessageRow[];
83
- export declare function readPartsForSession(db: SqlDatabaseLike, sessionID: string): PartRow[];
84
- export declare function readArtifactsForSession(db: SqlDatabaseLike, sessionID: string): ArtifactRow[];
85
- export declare function readArtifact(db: SqlDatabaseLike, artifactID: string): ArtifactRow | undefined;
86
- export declare function readArtifactBlob(db: SqlDatabaseLike, contentHash: string): ArtifactBlobRow | undefined;
87
- export declare function readOrphanArtifactBlobRows(db: SqlDatabaseLike): ArtifactBlobRow[];
88
- export declare function readLatestSessionID(db: SqlDatabaseLike): string | undefined;
89
- export declare function readSessionStats(db: SqlDatabaseLike): {
90
- sessionCount: number;
91
- messageCount: number;
92
- artifactCount: number;
93
- summaryNodeCount: number;
94
- blobCount: number;
95
- orphanBlobCount: number;
96
- orphanBlobChars: number;
97
- };
@@ -1,80 +0,0 @@
1
- export function readSessionHeader(db, sessionID) {
2
- return db.prepare('SELECT * FROM sessions WHERE session_id = ?').get(sessionID);
3
- }
4
- export function readAllSessions(db) {
5
- return db.prepare('SELECT * FROM sessions ORDER BY updated_at DESC').all();
6
- }
7
- export function readChildSessions(db, parentSessionID) {
8
- return db
9
- .prepare('SELECT * FROM sessions WHERE parent_session_id = ? ORDER BY updated_at DESC')
10
- .all(parentSessionID);
11
- }
12
- export function readLineageChain(db, sessionID) {
13
- const chain = [];
14
- let current = readSessionHeader(db, sessionID);
15
- while (current) {
16
- chain.unshift(current);
17
- if (!current.parent_session_id)
18
- break;
19
- current = readSessionHeader(db, current.parent_session_id);
20
- }
21
- return chain;
22
- }
23
- export function readMessagesForSession(db, sessionID) {
24
- return db
25
- .prepare('SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC')
26
- .all(sessionID);
27
- }
28
- export function readPartsForSession(db, sessionID) {
29
- return db
30
- .prepare('SELECT * FROM parts WHERE session_id = ? ORDER BY message_id ASC, sort_key ASC')
31
- .all(sessionID);
32
- }
33
- export function readArtifactsForSession(db, sessionID) {
34
- return db
35
- .prepare('SELECT * FROM artifacts WHERE session_id = ? ORDER BY created_at DESC')
36
- .all(sessionID);
37
- }
38
- export function readArtifact(db, artifactID) {
39
- return db.prepare('SELECT * FROM artifacts WHERE artifact_id = ?').get(artifactID);
40
- }
41
- export function readArtifactBlob(db, contentHash) {
42
- return db.prepare('SELECT * FROM artifact_blobs WHERE content_hash = ?').get(contentHash);
43
- }
44
- export function readOrphanArtifactBlobRows(db) {
45
- return db
46
- .prepare(`SELECT b.* FROM artifact_blobs b
47
- WHERE NOT EXISTS (
48
- SELECT 1 FROM artifacts a WHERE a.content_hash = b.content_hash
49
- )
50
- ORDER BY b.created_at ASC`)
51
- .all();
52
- }
53
- export function readLatestSessionID(db) {
54
- const row = db
55
- .prepare('SELECT session_id FROM sessions ORDER BY updated_at DESC LIMIT 1')
56
- .get();
57
- return row?.session_id;
58
- }
59
- export function readSessionStats(db) {
60
- const sessions = db.prepare('SELECT COUNT(*) AS count FROM sessions').get();
61
- const messages = db.prepare('SELECT COUNT(*) AS count FROM messages').get();
62
- const artifacts = db.prepare('SELECT COUNT(*) AS count FROM artifacts').get();
63
- const summaryNodes = db.prepare('SELECT COUNT(*) AS count FROM summary_nodes').get();
64
- const blobs = db
65
- .prepare(`SELECT COUNT(*) AS count, COALESCE(SUM(char_count), 0) AS chars
66
- FROM artifact_blobs b
67
- WHERE NOT EXISTS (
68
- SELECT 1 FROM artifacts a WHERE a.content_hash = b.content_hash
69
- )`)
70
- .get();
71
- return {
72
- sessionCount: sessions.count,
73
- messageCount: messages.count,
74
- artifactCount: artifacts.count,
75
- summaryNodeCount: summaryNodes.count,
76
- blobCount: blobs.count,
77
- orphanBlobCount: blobs.count,
78
- orphanBlobChars: blobs.chars,
79
- };
80
- }