[codex] fix kanban session matching (#538)

* fix kanban session matching

* tighten kanban task session lookup

* remove favicon svg
This commit is contained in:
ekko
2026-05-08 13:53:40 +08:00
committed by GitHub
parent 9edb76ac64
commit e55792acbb
4 changed files with 259 additions and 11 deletions
@@ -3,7 +3,12 @@ import { readFile } from 'fs/promises'
import { resolve, normalize } from 'path'
import { homedir } from 'os'
import * as kanbanCli from '../../services/hermes/hermes-kanban'
import { searchSessionSummariesWithProfile, getSessionDetailFromDbWithProfile } from '../../db/hermes/sessions-db'
import {
searchSessionSummariesWithProfile,
getSessionDetailFromDbWithProfile,
getExactSessionDetailFromDbWithProfile,
findLatestExactSessionIdWithProfile,
} from '../../db/hermes/sessions-db'
function getLatestRunProfile(detail: { runs: Array<{ profile: string | null }> }): string | null {
return [...detail.runs].reverse().find(run => run.profile)?.profile || null
@@ -34,13 +39,12 @@ export async function get(ctx: Context) {
const profile = getLatestRunProfile(detail)
if (profile) {
try {
const results = await searchSessionSummariesWithProfile(detail.task.id, profile, undefined, 5)
if (results.length > 0) {
const sessionId = results[0].id
const sessionDetail = await getSessionDetailFromDbWithProfile(sessionId, profile)
const exactSessionId = await findLatestExactSessionIdWithProfile(detail.task.id, profile)
if (exactSessionId) {
const sessionDetail = await getExactSessionDetailFromDbWithProfile(exactSessionId, profile)
if (sessionDetail) {
;(detail as any).session = {
id: sessionId,
id: exactSessionId,
title: sessionDetail.title,
source: sessionDetail.source,
model: sessionDetail.model,
@@ -49,6 +53,23 @@ export async function get(ctx: Context) {
messages: sessionDetail.messages,
}
}
} else {
const results = await searchSessionSummariesWithProfile(detail.task.id, profile, undefined, 5)
if (results.length > 0) {
const sessionId = results[0].id
const sessionDetail = await getSessionDetailFromDbWithProfile(sessionId, profile)
if (sessionDetail) {
;(detail as any).session = {
id: sessionId,
title: sessionDetail.title,
source: sessionDetail.source,
model: sessionDetail.model,
started_at: sessionDetail.started_at,
ended_at: sessionDetail.ended_at,
messages: sessionDetail.messages,
}
}
}
}
} catch {
// Session lookup is best-effort, don't fail the whole request
@@ -215,6 +236,42 @@ export async function searchSessions(ctx: Context) {
return
}
try {
if (!q) {
const exactSessionId = await findLatestExactSessionIdWithProfile(task_id, profile)
if (exactSessionId) {
const sessionDetail = await getExactSessionDetailFromDbWithProfile(exactSessionId, profile)
if (sessionDetail) {
ctx.body = {
results: [{
id: exactSessionId,
source: sessionDetail.source,
title: sessionDetail.title,
preview: sessionDetail.preview,
model: sessionDetail.model,
started_at: sessionDetail.started_at,
ended_at: sessionDetail.ended_at,
last_active: sessionDetail.last_active,
message_count: sessionDetail.message_count,
tool_call_count: sessionDetail.tool_call_count,
input_tokens: sessionDetail.input_tokens,
output_tokens: sessionDetail.output_tokens,
cache_read_tokens: sessionDetail.cache_read_tokens,
cache_write_tokens: sessionDetail.cache_write_tokens,
reasoning_tokens: sessionDetail.reasoning_tokens,
billing_provider: sessionDetail.billing_provider,
estimated_cost_usd: sessionDetail.estimated_cost_usd,
actual_cost_usd: sessionDetail.actual_cost_usd,
cost_status: sessionDetail.cost_status,
matched_message_id: null,
snippet: sessionDetail.preview,
rank: 0,
}],
}
return
}
}
}
const searchQuery = q || task_id
const results = await searchSessionSummariesWithProfile(searchQuery, profile, undefined, 10)
ctx.body = { results }
@@ -696,6 +696,139 @@ export async function getSessionDetailFromDbWithProfile(sessionId: string, profi
}
}
export async function getExactSessionDetailFromDbWithProfile(sessionId: string, profile: string): Promise<HermesSessionDetailRow | null> {
const { DatabaseSync } = await import('node:sqlite')
const dbPath = `${getProfileDir(profile)}/state.db`
const db = new DatabaseSync(dbPath, { open: true, readOnly: true })
try {
const idx = loadAllSessions(db)
const requested = idx.byId.get(sessionId) || null
if (!requested) return null
const messageRows = db.prepare(`
SELECT
id,
session_id,
role,
content,
tool_call_id,
tool_calls,
tool_name,
timestamp,
token_count,
finish_reason,
reasoning,
reasoning_details,
codex_reasoning_items,
reasoning_content
FROM messages
WHERE session_id = ?
ORDER BY timestamp, id
`).all(sessionId) as Record<string, unknown>[]
const messages = messageRows.map(mapMessageRow)
return aggregateSessionDetail([requested], messages, sessionId)
} finally {
db.close()
}
}
export async function findLatestExactSessionIdWithProfile(
query: string,
profile: string,
source?: string,
): Promise<string | null> {
if (!SQLITE_AVAILABLE) {
throw new Error(`node:sqlite requires Node >= 22.5, current: ${process.versions.node}`)
}
const trimmed = query.trim()
if (!trimmed) return null
const { DatabaseSync } = await import('node:sqlite')
const dbPath = `${getProfileDir(profile)}/state.db`
const db = new DatabaseSync(dbPath, { open: true, readOnly: true })
const loweredQuery = trimmed.toLowerCase()
const likePattern = buildLikePattern(loweredQuery)
const kanbanPrompt = `work kanban task ${trimmed}`.toLowerCase()
const taskJsonNeedle = `"task_id": "${trimmed}"`.toLowerCase()
try {
const sourceClause = source ? 'AND s.source = ?' : ''
const sourceParams = source ? [source] : []
const exactPromptSql = `
WITH base AS (
SELECT
${SESSION_SELECT},
s.parent_session_id AS parent_session_id
FROM sessions s
WHERE s.source != 'tool' AND s.id NOT LIKE 'compress_%'
${sourceClause}
)
SELECT base.id
FROM base
JOIN messages m ON m.session_id = base.id
WHERE m.role = 'user'
AND LOWER(TRIM(m.content)) = ?
ORDER BY base.last_active DESC, m.timestamp DESC
LIMIT 1
`
const exactPromptMatch = db.prepare(exactPromptSql).get(...sourceParams, kanbanPrompt) as Record<string, unknown> | undefined
if (exactPromptMatch?.id) return String(exactPromptMatch.id)
const taskJsonSql = `
WITH base AS (
SELECT
${SESSION_SELECT},
s.parent_session_id AS parent_session_id
FROM sessions s
WHERE s.source != 'tool' AND s.id NOT LIKE 'compress_%'
${sourceClause}
)
SELECT base.id
FROM base
JOIN messages m ON m.session_id = base.id
WHERE LOWER(m.content) LIKE ? ESCAPE '\\'
ORDER BY base.last_active DESC, m.timestamp DESC
LIMIT 1
`
const taskJsonMatch = db.prepare(taskJsonSql).get(...sourceParams, buildLikePattern(taskJsonNeedle)) as Record<string, unknown> | undefined
if (taskJsonMatch?.id) return String(taskJsonMatch.id)
const contentSql = `
WITH base AS (
SELECT
${SESSION_SELECT},
s.parent_session_id AS parent_session_id
FROM sessions s
WHERE s.source != 'tool' AND s.id NOT LIKE 'compress_%'
${sourceClause}
)
SELECT base.id
FROM base
JOIN messages m ON m.session_id = base.id
WHERE LOWER(m.content) LIKE ? ESCAPE '\\'
ORDER BY base.last_active DESC, m.timestamp DESC
LIMIT 1
`
const contentMatch = db.prepare(contentSql).get(...sourceParams, likePattern) as Record<string, unknown> | undefined
if (contentMatch?.id) return String(contentMatch.id)
const titleSql = `
SELECT s.id
FROM sessions s
WHERE s.source != 'tool' AND s.id NOT LIKE 'compress_%'
${sourceClause}
AND LOWER(COALESCE(s.title, '')) LIKE ? ESCAPE '\\'
ORDER BY s.started_at DESC
LIMIT 1
`
const titleMatch = db.prepare(titleSql).get(...sourceParams, likePattern) as Record<string, unknown> | undefined
return titleMatch?.id ? String(titleMatch.id) : null
} finally {
db.close()
}
}
export interface HermesUsageStats extends LocalUsageStats {
cost: number
total_api_calls: number