Skip to content

Commit 867fe31

Browse files
committed
fix(sessions): read KiloCode VSCode state storage
1 parent 03294fd commit 867fe31

2 files changed

Lines changed: 304 additions & 3 deletions

File tree

cli.js

Lines changed: 256 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6066,6 +6066,241 @@ function readKiloCodeDatabaseSessionDetail(dbPath, sessionId, messageLimit = DEF
60666066
};
60676067
}
60686068

6069+
6070+
function makeKiloCodeStateVscdbRef(dbPath, sessionId) {
6071+
return `${dbPath}#${encodeURIComponent(sessionId)}`;
6072+
}
6073+
6074+
function parseKiloCodeStateVscdbRef(value) {
6075+
if (typeof value !== 'string') return null;
6076+
const marker = value.lastIndexOf('#');
6077+
if (marker <= 0) return null;
6078+
const dbPath = value.slice(0, marker);
6079+
if (!/state\.vscdb$/i.test(dbPath)) return null;
6080+
return { dbPath, sessionId: decodeURIComponent(value.slice(marker + 1)) };
6081+
}
6082+
6083+
function decodeVscodeStateValue(value) {
6084+
if (value == null) return null;
6085+
let text = '';
6086+
if (Buffer.isBuffer(value)) text = value.toString('utf8');
6087+
else if (typeof value === 'string') text = value;
6088+
else return value;
6089+
text = stripUtf8Bom(text).trim();
6090+
if (!text) return null;
6091+
let parsed = parseKiloCodeJson(text, undefined);
6092+
if (typeof parsed === 'string') {
6093+
const nested = parseKiloCodeJson(parsed, undefined);
6094+
if (nested !== undefined) parsed = nested;
6095+
}
6096+
return parsed === undefined ? text : parsed;
6097+
}
6098+
6099+
function getKiloCodeStateVscdbFiles() {
6100+
const files = [];
6101+
const seen = new Set();
6102+
const pushFile = (value) => {
6103+
if (typeof value !== 'string' || !value.trim()) return;
6104+
const full = expandHomePath(value.trim());
6105+
if (!full || seen.has(full) || !fs.existsSync(full)) return;
6106+
seen.add(full);
6107+
files.push(full);
6108+
};
6109+
const pushDir = (value) => {
6110+
if (typeof value !== 'string' || !value.trim()) return;
6111+
const root = expandHomePath(value.trim());
6112+
if (!root || !fs.existsSync(root)) return;
6113+
const direct = path.join(root, 'state.vscdb');
6114+
pushFile(direct);
6115+
let entries = [];
6116+
try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch (_) { entries = []; }
6117+
for (const entry of entries) {
6118+
if (entry.isDirectory()) pushFile(path.join(root, entry.name, 'state.vscdb'));
6119+
}
6120+
};
6121+
pushFile(process.env.KILOCODE_STATE_VSCDB_FILE);
6122+
pushDir(process.env.KILOCODE_STATE_VSCDB_DIR);
6123+
const home = os.homedir();
6124+
const xdgConfig = process.env.XDG_CONFIG_HOME || (home ? path.join(home, '.config') : '');
6125+
const appData = process.env.APPDATA;
6126+
const appBases = [];
6127+
for (const app of ['Code', 'Code - Insiders', 'VSCodium', 'Cursor', 'Windsurf']) {
6128+
if (xdgConfig) appBases.push(path.join(xdgConfig, app, 'User'));
6129+
if (home) appBases.push(path.join(home, 'Library', 'Application Support', app, 'User'));
6130+
if (appData) appBases.push(path.join(appData, app, 'User'));
6131+
}
6132+
for (const base of appBases) {
6133+
pushDir(path.join(base, 'workspaceStorage'));
6134+
pushFile(path.join(base, 'globalStorage', 'state.vscdb'));
6135+
}
6136+
return files;
6137+
}
6138+
6139+
function readVscodeStateVscdbRows(dbPath) {
6140+
if (!dbPath || !fs.existsSync(dbPath)) return [];
6141+
const Database = loadKiloCodeSqliteBinding();
6142+
if (!Database) return [];
6143+
let db;
6144+
try {
6145+
db = new Database(dbPath, { readonly: true, fileMustExist: true });
6146+
const tables = new Set(db.prepare("select name from sqlite_master where type='table'").all().map(row => row.name));
6147+
if (!tables.has('ItemTable')) return [];
6148+
const columns = new Set(db.prepare("pragma table_info(ItemTable)").all().map(row => row.name));
6149+
if (!columns.has('key') || !columns.has('value')) return [];
6150+
return db.prepare('select key, value from ItemTable').all().map(row => ({ key: String(row.key || ''), value: decodeVscodeStateValue(row.value) }));
6151+
} catch (_) {
6152+
return [];
6153+
} finally {
6154+
if (db) { try { db.close(); } catch (_) { } }
6155+
}
6156+
}
6157+
6158+
function looksLikeKiloCodeLegacyHistoryItem(value) {
6159+
return value && typeof value === 'object'
6160+
&& typeof value.id === 'string'
6161+
&& (typeof value.task === 'string' || typeof value.workspace === 'string' || Number.isFinite(Number(value.ts)));
6162+
}
6163+
6164+
function collectKiloCodeStateHistoryItems(rows) {
6165+
const items = [];
6166+
const pushArray = (arr) => {
6167+
if (!Array.isArray(arr)) return;
6168+
for (const item of arr) {
6169+
if (looksLikeKiloCodeLegacyHistoryItem(item)) items.push(item);
6170+
}
6171+
};
6172+
for (const row of rows) {
6173+
const key = String(row.key || '').toLowerCase();
6174+
const value = row.value;
6175+
if (key.includes('taskhistory')) pushArray(value);
6176+
if (value && typeof value === 'object' && !Array.isArray(value)) {
6177+
pushArray(value.taskHistory);
6178+
pushArray(value.history);
6179+
}
6180+
if (Array.isArray(value) && value.some(looksLikeKiloCodeLegacyHistoryItem)) pushArray(value);
6181+
}
6182+
return items;
6183+
}
6184+
6185+
function collectKiloCodeStateMessagesForSession(rows, sessionId) {
6186+
const target = String(sessionId || '').toLowerCase();
6187+
if (!target) return [];
6188+
const candidates = [];
6189+
const pushMessages = (value) => {
6190+
if (Array.isArray(value)) {
6191+
const normalized = value.map((entry, index) => normalizeKiloCodeLegacyConversationMessage(entry, index, {})).filter(Boolean);
6192+
if (normalized.length) candidates.push(value);
6193+
}
6194+
};
6195+
for (const row of rows) {
6196+
const key = String(row.key || '').toLowerCase();
6197+
const value = row.value;
6198+
if (key.includes(target)) {
6199+
pushMessages(value);
6200+
if (value && typeof value === 'object' && !Array.isArray(value)) {
6201+
pushMessages(value.api_conversation_history);
6202+
pushMessages(value.apiConversationHistory);
6203+
pushMessages(value.messages);
6204+
}
6205+
}
6206+
if (value && typeof value === 'object' && !Array.isArray(value)) {
6207+
const id = String(value.id || value.taskId || value.sessionId || '').toLowerCase();
6208+
if (id === target) {
6209+
pushMessages(value.api_conversation_history);
6210+
pushMessages(value.apiConversationHistory);
6211+
pushMessages(value.messages);
6212+
}
6213+
}
6214+
}
6215+
return candidates[0] || [];
6216+
}
6217+
6218+
function toKiloCodeStateVscdbSummary(dbPath, item, rows) {
6219+
const parsed = parseKiloCodeLegacyHistoryItem(item, item?.id);
6220+
if (!parsed || !parsed.id) return null;
6221+
const embeddedMessages = collectKiloCodeStateMessagesForSession(rows, parsed.id);
6222+
const normalized = removeLeadingSystemMessage(embeddedMessages.map((entry, index) => normalizeKiloCodeLegacyConversationMessage(entry, index, parsed)).filter(Boolean));
6223+
return {
6224+
source: 'kilocode',
6225+
sourceLabel: 'KiloCode VSCode',
6226+
provider: 'kilocode',
6227+
model: '',
6228+
models: [],
6229+
sessionId: parsed.id,
6230+
title: parsed.task || titleFromKiloCodeLegacyMessages(embeddedMessages) || parsed.id,
6231+
cwd: parsed.workspace || '',
6232+
createdAt: toIsoTime(parsed.ts, ''),
6233+
updatedAt: toIsoTime(parsed.ts, ''),
6234+
messageCount: normalized.length,
6235+
totalTokens: 0,
6236+
contextWindow: 0,
6237+
inputTokens: 0,
6238+
cachedInputTokens: 0,
6239+
cacheCreationInputTokens: 0,
6240+
outputTokens: 0,
6241+
reasoningOutputTokens: 0,
6242+
__messageCountExact: Boolean(normalized.length),
6243+
filePath: makeKiloCodeStateVscdbRef(dbPath, parsed.id),
6244+
keywords: [],
6245+
capabilities: { code: true, vscode: true, stateVscdb: true }
6246+
};
6247+
}
6248+
6249+
function listKiloCodeStateVscdbSessions(limit) {
6250+
const sessions = [];
6251+
const lookupStore = g_sessionFileLookupCache.kilocode;
6252+
for (const dbPath of getKiloCodeStateVscdbFiles()) {
6253+
const rows = readVscodeStateVscdbRows(dbPath);
6254+
const seen = new Set();
6255+
for (const item of collectKiloCodeStateHistoryItems(rows)) {
6256+
const summary = toKiloCodeStateVscdbSummary(dbPath, item, rows);
6257+
if (!summary) continue;
6258+
const idKey = summary.sessionId.toLowerCase();
6259+
if (seen.has(idKey)) continue;
6260+
seen.add(idKey);
6261+
if (lookupStore instanceof Map) lookupStore.set(idKey, summary.filePath);
6262+
sessions.push(summary);
6263+
if (sessions.length >= limit) break;
6264+
}
6265+
if (sessions.length >= limit) break;
6266+
}
6267+
return sessions;
6268+
}
6269+
6270+
function readKiloCodeStateVscdbSessionDetail(refOrPath, sessionId, messageLimit = DEFAULT_SESSION_DETAIL_MESSAGES) {
6271+
const parsedRef = parseKiloCodeStateVscdbRef(refOrPath);
6272+
const dbPath = parsedRef ? parsedRef.dbPath : refOrPath;
6273+
const id = sessionId || parsedRef?.sessionId || '';
6274+
if (!dbPath || !id || !fs.existsSync(dbPath)) return null;
6275+
const rows = readVscodeStateVscdbRows(dbPath);
6276+
const item = collectKiloCodeStateHistoryItems(rows).find(entry => String(entry.id || '').toLowerCase() === String(id).toLowerCase()) || {};
6277+
const embeddedMessages = collectKiloCodeStateMessagesForSession(rows, id);
6278+
if (embeddedMessages.length) {
6279+
const parsed = parseKiloCodeLegacyHistoryItem(item, id) || { id, workspace: '', ts: 0 };
6280+
const filtered = removeLeadingSystemMessage(embeddedMessages.map((entry, index) => normalizeKiloCodeLegacyConversationMessage(entry, index, parsed)).filter(Boolean));
6281+
const clipped = filtered.length > messageLimit;
6282+
return {
6283+
sessionId: id,
6284+
cwd: parsed.workspace || '',
6285+
updatedAt: toIsoTime(parsed.ts, ''),
6286+
totalMessages: filtered.length,
6287+
clipped,
6288+
truncated: clipped,
6289+
messages: clipped ? filtered.slice(-messageLimit) : filtered
6290+
};
6291+
}
6292+
for (const taskRoot of getKiloCodeLegacyTaskRoots()) {
6293+
const apiPath = path.join(taskRoot, id, 'api_conversation_history.json');
6294+
const detail = readKiloCodeLegacySessionDetail(apiPath, id, messageLimit);
6295+
if (detail) return detail;
6296+
}
6297+
if (looksLikeKiloCodeLegacyHistoryItem(item)) {
6298+
const parsed = parseKiloCodeLegacyHistoryItem(item, id) || {};
6299+
return { sessionId: id, cwd: parsed.workspace || '', updatedAt: toIsoTime(parsed.ts, ''), totalMessages: 0, clipped: false, truncated: false, messages: [] };
6300+
}
6301+
return null;
6302+
}
6303+
60696304
function extractKiloCodeMessageFromRecord(record, state, lineIndex = -1) {
60706305
if (!record || typeof record !== 'object') return;
60716306
if (record.timestamp || record.updatedAt || record.time) {
@@ -6178,7 +6413,7 @@ function collectKiloCodeSessionFiles(rootDir, maxFiles = 5000) {
61786413

61796414
function listKiloCodeSessions(limit, options = {}) {
61806415
const targetCount = Math.max(limit * (Number(options.scanFactor) || SESSION_SCAN_FACTOR), Number(options.minFiles) || SESSION_SCAN_MIN_FILES);
6181-
const sessions = listKiloCodeDatabaseSessions(targetCount).concat(listKiloCodeLegacyTaskSessions(targetCount));
6416+
const sessions = listKiloCodeDatabaseSessions(targetCount).concat(listKiloCodeLegacyTaskSessions(targetCount), listKiloCodeStateVscdbSessions(targetCount));
61826417
const seenIds = new Set(sessions.map(item => String(item.sessionId || '').toLowerCase()).filter(Boolean));
61836418
const filesMeta = [];
61846419
for (const root of getKiloCodeSessionRoots()) {
@@ -7043,6 +7278,10 @@ function resolveSessionFilePath(source, filePath, sessionId) {
70437278
const lookupStore = g_sessionFileLookupCache[normalizedSource];
70447279
if (lookupStore instanceof Map && lookupStore.has(targetId)) {
70457280
const cachedPath = lookupStore.get(targetId);
7281+
const cachedStateRef = normalizedSource === 'kilocode' ? parseKiloCodeStateVscdbRef(cachedPath) : null;
7282+
if (cachedStateRef && fs.existsSync(cachedStateRef.dbPath)) {
7283+
return cachedPath;
7284+
}
70467285
if (cachedPath && fs.existsSync(cachedPath) && availableRoots.some((rootPath) => isPathInside(cachedPath, rootPath))) {
70477286
return cachedPath;
70487287
}
@@ -7094,6 +7333,15 @@ function resolveSessionFilePath(source, filePath, sessionId) {
70947333
}
70957334
}
70967335
}
7336+
if (!matchedFile) {
7337+
for (const dbPath of getKiloCodeStateVscdbFiles()) {
7338+
const detail = readKiloCodeStateVscdbSessionDetail(dbPath, sessionId.trim(), 1);
7339+
if (detail) {
7340+
matchedFile = makeKiloCodeStateVscdbRef(dbPath, sessionId.trim());
7341+
break;
7342+
}
7343+
}
7344+
}
70977345
if (!matchedFile) {
70987346
const files = [];
70997347
for (const rootPath of availableRoots) {
@@ -7110,8 +7358,10 @@ function resolveSessionFilePath(source, filePath, sessionId) {
71107358
}
71117359
matchedFile = files.find(item => path.basename(item, '.jsonl').toLowerCase() === targetId) || '';
71127360
}
7113-
if (matchedFile && fs.existsSync(matchedFile)) {
7114-
return matchedFile;
7361+
if (matchedFile) {
7362+
const stateRef = normalizedSource === 'kilocode' ? parseKiloCodeStateVscdbRef(matchedFile) : null;
7363+
if (stateRef && fs.existsSync(stateRef.dbPath)) return matchedFile;
7364+
if (fs.existsSync(matchedFile)) return matchedFile;
71157365
}
71167366
}
71177367

@@ -8940,6 +9190,7 @@ async function readSessionDetail(params = {}) {
89409190
if (source === 'kilocode') {
89419191
extracted = readKiloCodeDatabaseSessionDetail(filePath, params.sessionId, messageLimit)
89429192
|| readKiloCodeLegacySessionDetail(filePath, params.sessionId, messageLimit)
9193+
|| readKiloCodeStateVscdbSessionDetail(filePath, params.sessionId, messageLimit)
89439194
|| await extractSessionDetailPreviewFromFile(filePath, source, messageLimit, { preview });
89449195
} else if (source === 'gemini') {
89459196
let json;
@@ -9075,6 +9326,7 @@ async function readSessionPlain(params = {}) {
90759326
if (source === 'kilocode') {
90769327
extracted = readKiloCodeDatabaseSessionDetail(filePath, params.sessionId, maxMessages === Infinity ? MAX_EXPORT_MESSAGES : maxMessages)
90779328
|| readKiloCodeLegacySessionDetail(filePath, params.sessionId, maxMessages === Infinity ? MAX_EXPORT_MESSAGES : maxMessages)
9329+
|| readKiloCodeStateVscdbSessionDetail(filePath, params.sessionId, maxMessages === Infinity ? MAX_EXPORT_MESSAGES : maxMessages)
90789330
|| await extractMessagesFromFile(filePath, source, { maxMessages });
90799331
} else if (source === 'gemini') {
90809332
let json;
@@ -9167,6 +9419,7 @@ async function exportSessionData(params = {}) {
91679419
if (source === 'kilocode') {
91689420
extracted = readKiloCodeDatabaseSessionDetail(filePath, params.sessionId, maxMessages === Infinity ? MAX_EXPORT_MESSAGES : maxMessages)
91699421
|| readKiloCodeLegacySessionDetail(filePath, params.sessionId, maxMessages === Infinity ? MAX_EXPORT_MESSAGES : maxMessages)
9422+
|| readKiloCodeStateVscdbSessionDetail(filePath, params.sessionId, maxMessages === Infinity ? MAX_EXPORT_MESSAGES : maxMessages)
91709423
|| await extractMessagesFromFile(filePath, source, { maxMessages });
91719424
} else if (source === 'gemini') {
91729425
let json;

tests/unit/kilocode-session-browser.test.mjs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,51 @@ test('list-sessions reads KiloCode SQLite history when session table has no path
236236
fs.rmSync(tmpDir, { recursive: true, force: true });
237237
}
238238
});
239+
240+
function createKiloStateVscdb(dbPath) {
241+
const db = new Database(dbPath);
242+
try {
243+
db.exec('create table ItemTable (key text primary key, value blob);');
244+
const taskId = 'state-task-1';
245+
const put = db.prepare('insert into ItemTable (key, value) values (?, ?)');
246+
put.run('taskHistory', JSON.stringify([
247+
{ id: taskId, task: 'Kilo state.vscdb session', workspace: '/tmp/kilo-state-workspace', ts: 1750004000000, mode: 'code' }
248+
]));
249+
put.run(`${taskId}:api_conversation_history`, JSON.stringify([
250+
{ role: 'user', content: [{ type: 'text', text: 'hello from state vscdb' }], ts: 1750004001000 },
251+
{ role: 'assistant', content: [{ type: 'text', text: 'answer from state vscdb' }], ts: 1750004002000 }
252+
]));
253+
} finally {
254+
db.close();
255+
}
256+
}
257+
258+
test('export-session reads KiloCode VSCode workspace state.vscdb task history', () => {
259+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codexmate-kilo-statevscdb-'));
260+
const dbPath = path.join(tmpDir, 'Code', 'User', 'workspaceStorage', '4206e49917515a1d83fb5980c6a520f6', 'state.vscdb');
261+
const outputPath = path.join(tmpDir, 'state.md');
262+
263+
try {
264+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
265+
createKiloStateVscdb(dbPath);
266+
execFileSync(process.execPath, [
267+
path.join(repoRoot, 'cli.js'),
268+
'export-session',
269+
'--source', 'kilocode',
270+
'--session-id', 'state-task-1',
271+
'--output', outputPath,
272+
'--max-messages', 'all'
273+
], {
274+
cwd: repoRoot,
275+
env: { ...process.env, KILOCODE_STATE_VSCDB_FILE: dbPath },
276+
encoding: 'utf8',
277+
stdio: 'pipe'
278+
});
279+
280+
const md = fs.readFileSync(outputPath, 'utf8');
281+
assert(md.includes('hello from state vscdb'));
282+
assert(md.includes('answer from state vscdb'));
283+
} finally {
284+
fs.rmSync(tmpDir, { recursive: true, force: true });
285+
}
286+
});

0 commit comments

Comments
 (0)