Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/main/ai-vault/session-scanner-opencode-sqlite-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { canReadOpenCodeMessageParts } from './session-scanner-opencode-sqlite-s
import type { TranscriptMessage, TranscriptMessageRole } from './session-transcript-consumers'
import { boundedText, toolCallText } from './session-transcript-message-content'
import { zcodeVisibleMessageFilter } from './session-scanner-zcode-visibility'
import { zcodeTranscriptOrder } from './session-scanner-zcode-order'
import type SyncDatabase from '../sqlite/sync-database'

// Why: the session list needs the newest few messages, and the search index
Expand Down Expand Up @@ -120,7 +121,7 @@ function captureRole(role: string | null): TranscriptMessageRole | null {
return role === 'user' || role === 'assistant' ? role : null
}

function buildCaptureQuery(agent: 'opencode' | 'zcode'): string {
function buildCaptureQuery(db: SyncDatabase, agent: 'opencode' | 'zcode'): string {
// Message order, then part order within a message: the same key the preview
// read uses, run forwards and without the newest-N window.
return `SELECT m.id AS message_id,
Expand All @@ -134,7 +135,8 @@ function buildCaptureQuery(agent: 'opencode' | 'zcode'): string {
${zcodeVisibleMessageFilter(agent)}
AND json_extract(m.data, '$.role') IN ('user','assistant')
AND json_extract(p.data, '$.type') IN ${OPENCODE_CAPTURE_PART_TYPES}
ORDER BY m.time_created ASC, m.id ASC, p.time_created ASC, p.rowid ASC
ORDER BY ${zcodeTranscriptOrder(db, agent, 'message', 'm', 'ASC')},
${zcodeTranscriptOrder(db, agent, 'part', 'p', 'ASC')}
LIMIT ?`
}

Expand Down Expand Up @@ -164,7 +166,7 @@ export function readOpenCodeSessionMessages(
)
}
const rows = db
.prepare(buildCaptureQuery(agent))
.prepare(buildCaptureQuery(db, agent))
.all(sessionId, OPENCODE_CAPTURE_RECORD_LIMIT + 1)
if (rows.length > OPENCODE_CAPTURE_RECORD_LIMIT) {
throw new Error(
Expand Down
19 changes: 12 additions & 7 deletions src/main/ai-vault/session-scanner-opencode-sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from './session-scanner-opencode-sqlite-schema'
import { normalizeTitleText } from './session-scanner-values'
import { zcodeVisibleMessageFilter } from './session-scanner-zcode-visibility'
import { zcodeTranscriptOrder } from './session-scanner-zcode-order'
import type SyncDatabase from '../sqlite/sync-database'
import { columnExists, tableExists } from '../opencode-usage/schema-helpers'

Expand Down Expand Up @@ -162,11 +163,11 @@ function readFirstUserPromptFromOpenCodeDb(
${zcodeVisibleMessageFilter(agent)}
AND json_extract(m.data, '$.role') = 'user'
AND json_extract(fp.data, '$.type') = 'text'
ORDER BY m.time_created ASC, m.id ASC
ORDER BY ${zcodeTranscriptOrder(db, agent, 'message', 'm', 'ASC')}
LIMIT 1
)
AND json_extract(p.data, '$.type') = 'text'
ORDER BY p.time_created ASC, p.rowid ASC
ORDER BY ${zcodeTranscriptOrder(db, agent, 'part', 'p', 'ASC')}
LIMIT ${FIRST_USER_PROMPT_PART_LIMIT}`
)
.all(sessionId)
Expand Down Expand Up @@ -198,15 +199,19 @@ function buildPreviewQuery(db: SyncDatabase, agent: 'opencode' | 'zcode'): strin
p.time_created,
json_extract(m.data, '$.summary.title') AS summary_title,
json_extract(m.data, '$.summary.body') AS summary_body
FROM (SELECT id, data FROM message
WHERE session_id = ?
${zcodeVisibleMessageFilter(agent, 'data')}
ORDER BY time_created DESC, id DESC
FROM (SELECT ${agent === 'zcode' ? 'm.rowid AS rowid, m.*' : 'm.id, m.data'} FROM message m
WHERE m.session_id = ?
${zcodeVisibleMessageFilter(agent)}
ORDER BY ${zcodeTranscriptOrder(db, agent, 'message', 'm', 'DESC')}
LIMIT ${OPENCODE_SQLITE_PREVIEW_MESSAGE_WINDOW}) m
JOIN part p ON p.message_id = m.id
WHERE json_extract(m.data, '$.role') IN ('user','assistant')
AND json_extract(p.data, '$.type') = 'text'
ORDER BY p.time_created DESC
ORDER BY ${
agent === 'zcode'
? `${zcodeTranscriptOrder(db, agent, 'message', 'm', 'DESC')}, ${zcodeTranscriptOrder(db, agent, 'part', 'p', 'DESC')}`
: 'p.time_created DESC'
}
LIMIT ?`
}

Expand Down
18 changes: 18 additions & 0 deletions src/main/ai-vault/session-scanner-zcode-order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { columnExists } from '../opencode-usage/schema-helpers'
import type SyncDatabase from '../sqlite/sync-database'

export function zcodeTranscriptOrder(
db: SyncDatabase,
agent: 'opencode' | 'zcode',
table: 'message' | 'part',
alias: 'm' | 'p',
direction: 'ASC' | 'DESC'
): string {
const sequenced = agent === 'zcode' && columnExists(db, table, 'sequence')
const sequence = sequenced
? `${alias}.sequence IS NULL ${direction}, ${alias}.sequence ${direction}, `
: ''
// ZCode orders imported/forked transcripts by sequence, with legacy NULL rows last.
const tieBreaker = table === 'message' ? (sequenced ? 'rowid' : 'id') : sequenced ? 'id' : 'rowid'
return `${sequence}${alias}.time_created ${direction}, ${alias}.${tieBreaker} ${direction}`
}
116 changes: 116 additions & 0 deletions src/main/ai-vault/session-scanner-zcode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,122 @@ afterEach(() => {
})

describe('ZCode AI Vault SQLite worker', () => {
it.each(['message', 'part', 'both'] as const)(
'reads partially migrated %s sequence columns and keeps legacy NULL rows last',
async (tables) => {
const directory = mkdtempSync(join(tmpdir(), 'orca-zcode-legacy-sequence-'))
directories.push(directory)
const dbPath = join(directory, 'db.sqlite')
writeOpenCodeSqliteDatabase(dbPath, [
{
id: 'legacy-session',
turns: [
{ role: 'user', parts: ['first', 'second', 'legacy part'] },
{ role: 'user', parts: ['legacy message'] }
]
}
])
const db = new SyncDatabase(dbPath)
try {
if (tables !== 'part') {
db.exec(`ALTER TABLE message ADD COLUMN sequence INTEGER;
UPDATE message SET sequence = 0 WHERE rowid = 1;
UPDATE message SET time_created = 1 WHERE rowid = 2;`)
}
if (tables !== 'message') {
db.exec(`ALTER TABLE part ADD COLUMN sequence INTEGER;
UPDATE part SET sequence = rowid WHERE rowid <= 2;
UPDATE part SET time_created = 1 WHERE rowid = 3;`)
}
} finally {
db.close()
}
const captured = await handleOpenCodeSqliteRequest({
id: 8,
kind: 'capture',
agent: 'zcode',
dbPath,
sessionId: 'legacy-session',
platform: 'win32'
})
expect(captured.ok).toBe(true)
if (!captured.ok) {
return
}
expect(readMessageTexts(captured.value)).toEqual([
{ text: 'first\nsecond\nlegacy part' },
{ text: 'legacy message' }
])
}
)

it('uses stored message and part sequence for first prompt, preview, and search', async () => {
const directory = mkdtempSync(join(tmpdir(), 'orca-zcode-sequence-'))
directories.push(directory)
const dbPath = join(directory, 'db.sqlite')
writeOpenCodeSqliteDatabase(dbPath, [
{
id: 'ordered-session',
turns: [
{ role: 'user', parts: ['first prompt start', 'first prompt end'] },
{ role: 'assistant', parts: ['first response'] },
{ role: 'user', parts: ['second prompt'] },
{ role: 'assistant', parts: ['second response start', 'second response end'] }
]
}
])
const db = new SyncDatabase(dbPath)
try {
db.exec(`ALTER TABLE message ADD COLUMN sequence INTEGER;
ALTER TABLE part ADD COLUMN sequence INTEGER;
UPDATE message SET sequence = rowid, time_created = time_created - rowid * 120000;
UPDATE part SET sequence = rowid, time_created = time_created - rowid * 120000;`)
} finally {
db.close()
}
const parsed = await handleOpenCodeSqliteRequest({
id: 6,
kind: 'parse',
agent: 'zcode',
dbPath,
sessionId: 'ordered-session',
platform: 'darwin',
fullFirstUserPrompt: true
})
expect(parsed).toMatchObject({
ok: true,
value: {
firstUserPrompt: 'first prompt start\nfirst prompt end',
previewMessagesTruncated: true,
previewMessages: [
{ text: 'first prompt end' },
{ text: 'first response' },
{ text: 'second prompt' },
{ text: 'second response start' },
{ text: 'second response end' }
]
}
})
const captured = await handleOpenCodeSqliteRequest({
id: 7,
kind: 'capture',
agent: 'zcode',
dbPath,
sessionId: 'ordered-session',
platform: 'darwin'
})
expect(captured.ok).toBe(true)
if (!captured.ok) {
return
}
expect(readMessageTexts(captured.value)).toEqual([
{ text: 'first prompt start\nfirst prompt end' },
{ text: 'first response' },
{ text: 'second prompt' },
{ text: 'second response start\nsecond response end' }
])
})

it('lists, parses, and captures ZCode sessions without labelling them OpenCode', async () => {
const directory = mkdtempSync(join(tmpdir(), 'orca-zcode-ai-vault-'))
directories.push(directory)
Expand Down
Loading