openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
79 lines (78 loc) • 4.14 kB
JavaScript
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import "./utils-P__uGsPB.js";
import { b as toAgentStoreSessionKey } from "./session-key-BnWWjqNc.js";
import { n as withOpenClawAgentDatabaseReadOnly } from "./openclaw-agent-db-readonly-CHjf8FxN.js";
import { m as toDatabaseOptions, s as resolveSqliteReadScope } from "./session-accessor.sqlite-scope-2KfMzb44.js";
import { o as listSessionsNeedingTranscriptIndexReconcile } from "./session-transcript-index-DmCW_GtW.js";
import { r as startSessionTranscriptIndexReconcile, t as isSessionTranscriptIndexReconcileRunning } from "./session-transcript-reconcile-D3LiCrAp.js";
//#region src/config/sessions/session-transcript-search.ts
const SEARCH_SNIPPET_MAX_CHARS = 500;
const SEARCH_LIMIT_MAX = 25;
const SEARCH_QUERY_MAX_CHARS = 4096;
function toFtsQuery(query) {
return query.trim().split(/\s+/u).map((token) => `"${token.replaceAll("\"", "\"\"")}"`).join(" AND ");
}
/** Search the per-agent FTS index; kicks off one background reconcile when the index lags. */
function searchSessionTranscripts(params) {
const query = params.query.trim();
if (!query) throw new Error("query must not be empty");
if (query.length > SEARCH_QUERY_MAX_CHARS) throw new Error(`query must not exceed ${SEARCH_QUERY_MAX_CHARS} characters`);
const scope = resolveSqliteReadScope(params);
const databaseOptions = toDatabaseOptions(scope);
const result = withOpenClawAgentDatabaseReadOnly((database) => {
const dirtySessions = listSessionsNeedingTranscriptIndexReconcile(database.db);
if (dirtySessions.length > 0) startSessionTranscriptIndexReconcile(databaseOptions);
const indexing = dirtySessions.length > 0 || isSessionTranscriptIndexReconcileRunning(databaseOptions);
const limit = Math.min(Math.max(1, params.limit ?? 10), SEARCH_LIMIT_MAX);
const sessionFilterValues = params.sessionKeys ?? [toAgentStoreSessionKey({
agentId: scope.agentId,
requestKey: "*"
})];
const whereSession = params.sessionKeys === void 0 ? " AND (session_windows.session_key GLOB ? OR session_windows.session_key IN ('global', 'unknown'))" : sessionFilterValues.length > 0 ? ` AND session_windows.session_key IN (${sessionFilterValues.map(() => "?").join(", ")})` : "";
const statement = database.db.prepare(`
SELECT session_windows.session_key AS session_key, session_transcript_fts.session_id AS session_id,
message_id, role, timestamp,
snippet(session_transcript_fts, 0, '', '', ' … ', 48) AS snippet,
bm25(session_transcript_fts) AS rank
FROM session_transcript_fts
JOIN session_windows ON session_windows.session_id = session_transcript_fts.session_id
WHERE session_transcript_fts MATCH ?${whereSession}
AND session_transcript_fts.session_id NOT IN (
SELECT session_id FROM session_transcript_index_state WHERE needs_rebuild != 0
)
ORDER BY rank ASC, timestamp DESC, message_id ASC
LIMIT ?
`);
const values = [
toFtsQuery(query),
...sessionFilterValues,
limit + 1
];
const hits = statement.all(...values).flatMap((row) => {
if (typeof row.session_key !== "string" || typeof row.session_id !== "string" || typeof row.message_id !== "string" || row.role !== "user" && row.role !== "assistant" || typeof row.snippet !== "string") return [];
const timestamp = typeof row.timestamp === "number" ? row.timestamp : Number(row.timestamp);
const rank = typeof row.rank === "number" ? row.rank : Number(row.rank);
return [{
sessionKey: row.session_key,
sessionId: row.session_id,
messageId: row.message_id,
role: row.role,
timestamp: Number.isFinite(timestamp) ? timestamp : 0,
snippet: row.snippet.length > SEARCH_SNIPPET_MAX_CHARS ? `${truncateUtf16Safe(row.snippet, SEARCH_SNIPPET_MAX_CHARS)}…` : row.snippet,
score: Number.isFinite(rank) ? -rank : 0
}];
});
return {
hits: hits.slice(0, limit),
indexing,
truncated: hits.length > limit
};
}, databaseOptions, { throwOnMissingTable: true });
return result.found ? result.value : {
hits: [],
indexing: false,
truncated: false
};
}
//#endregion
export { searchSessionTranscripts as t };