@threadbase-sh/streamer 1.61.0 → 1.61.1
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/dist/cli.cjs +479 -102
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +10 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +10 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.cjs
CHANGED
|
@@ -140828,18 +140828,43 @@ function readGitBranch(projectPath) {
|
|
|
140828
140828
|
log15.trace({ projectPath }, "git: no .git found within depth");
|
|
140829
140829
|
return null;
|
|
140830
140830
|
}
|
|
140831
|
+
var FTS_HIT_OPEN = "";
|
|
140832
|
+
var FTS_HIT_CLOSE = "";
|
|
140833
|
+
var FTS_SNIPPET_TOKENS = 16;
|
|
140834
|
+
var FTS_ELLIPSIS = "\u2026";
|
|
140835
|
+
var CONTENT_FIELD = "content";
|
|
140836
|
+
function parseFtsSnippet(raw2) {
|
|
140837
|
+
if (!raw2?.includes(FTS_HIT_OPEN)) return null;
|
|
140838
|
+
const highlights = [];
|
|
140839
|
+
let snippet = "";
|
|
140840
|
+
let openAt = -1;
|
|
140841
|
+
for (const ch of raw2) {
|
|
140842
|
+
if (ch === FTS_HIT_OPEN) {
|
|
140843
|
+
openAt = snippet.length;
|
|
140844
|
+
continue;
|
|
140845
|
+
}
|
|
140846
|
+
if (ch === FTS_HIT_CLOSE) {
|
|
140847
|
+
if (openAt >= 0 && snippet.length > openAt) {
|
|
140848
|
+
highlights.push({ start: openAt, end: snippet.length });
|
|
140849
|
+
}
|
|
140850
|
+
openAt = -1;
|
|
140851
|
+
continue;
|
|
140852
|
+
}
|
|
140853
|
+
snippet += ch;
|
|
140854
|
+
}
|
|
140855
|
+
return highlights.length > 0 ? { snippet, highlights } : null;
|
|
140856
|
+
}
|
|
140831
140857
|
function generateMatches(meta3, query) {
|
|
140832
140858
|
const matches = [];
|
|
140833
140859
|
const lowerQuery = query.toLowerCase();
|
|
140834
140860
|
const fields = [
|
|
140835
|
-
["contentSnippet", meta3.contentSnippet],
|
|
140836
|
-
["projectName", meta3.projectName],
|
|
140837
|
-
["sessionId", meta3.sessionId],
|
|
140838
140861
|
["sessionName", meta3.sessionName],
|
|
140839
|
-
["
|
|
140840
|
-
["model", meta3.model || ""],
|
|
140862
|
+
["projectName", meta3.projectName],
|
|
140841
140863
|
["gitBranch", meta3.gitBranch || ""],
|
|
140842
|
-
["toolNames", meta3.toolNames.join(" ")]
|
|
140864
|
+
["toolNames", meta3.toolNames.join(" ")],
|
|
140865
|
+
["model", meta3.model || ""],
|
|
140866
|
+
["account", meta3.account],
|
|
140867
|
+
["sessionId", meta3.sessionId]
|
|
140843
140868
|
];
|
|
140844
140869
|
for (const [field, value] of fields) {
|
|
140845
140870
|
const idx = value.toLowerCase().indexOf(lowerQuery);
|
|
@@ -140854,11 +140879,29 @@ function generateMatches(meta3, query) {
|
|
|
140854
140879
|
}
|
|
140855
140880
|
return matches.length > 0 ? matches : [{ field: "preview", snippet: meta3.preview }];
|
|
140856
140881
|
}
|
|
140882
|
+
function buildContentMatch(searchContent, query) {
|
|
140883
|
+
if (!searchContent || !query.trim()) return null;
|
|
140884
|
+
const idx = searchContent.toLowerCase().indexOf(query.toLowerCase());
|
|
140885
|
+
if (idx === -1) return null;
|
|
140886
|
+
const start = Math.max(0, idx - 80);
|
|
140887
|
+
const end = Math.min(searchContent.length, idx + query.length + 120);
|
|
140888
|
+
const body = searchContent.slice(start, end).replace(/\s+/g, " ").trim();
|
|
140889
|
+
const hitAt = body.toLowerCase().indexOf(query.toLowerCase());
|
|
140890
|
+
const prefix = start > 0 ? FTS_ELLIPSIS : "";
|
|
140891
|
+
const suffix = end < searchContent.length ? FTS_ELLIPSIS : "";
|
|
140892
|
+
const snippet = `${prefix}${body}${suffix}`;
|
|
140893
|
+
const highlights = hitAt === -1 ? [] : [{ start: hitAt + prefix.length, end: hitAt + prefix.length + query.length }];
|
|
140894
|
+
return { field: CONTENT_FIELD, snippet, highlights };
|
|
140895
|
+
}
|
|
140857
140896
|
var FlexSearch = flexsearch_bundle_module_min_default.default ?? flexsearch_bundle_module_min_default;
|
|
140858
140897
|
var SearchIndexer = class {
|
|
140859
140898
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
140860
140899
|
index;
|
|
140861
140900
|
documents = /* @__PURE__ */ new Map();
|
|
140901
|
+
// The indexed body per conversation, kept so a hit can produce a real excerpt
|
|
140902
|
+
// instead of falling back to an unrelated preview. Sized by the caller (the
|
|
140903
|
+
// scanner tail-caps it to the content tier) — see the note on addDocument.
|
|
140904
|
+
searchContents = /* @__PURE__ */ new Map();
|
|
140862
140905
|
constructor() {
|
|
140863
140906
|
this.index = this.createIndex();
|
|
140864
140907
|
}
|
|
@@ -140884,25 +140927,25 @@ var SearchIndexer = class {
|
|
|
140884
140927
|
cache: 100
|
|
140885
140928
|
});
|
|
140886
140929
|
}
|
|
140887
|
-
|
|
140930
|
+
// `searchContent` is the combined search document (text + thinking + tools),
|
|
140931
|
+
// NOT meta.contentSnippet.
|
|
140932
|
+
//
|
|
140933
|
+
// It arrives pre-capped. This index is `tokenize: "forward"` at resolution 9,
|
|
140934
|
+
// which stores every prefix of every token, so it is a resident-memory
|
|
140935
|
+
// structure whose cost is very different from the on-disk FTS index. Feeding
|
|
140936
|
+
// it the full ~128 KB budget across a few hundred conversations would be a
|
|
140937
|
+
// multi-hundred-MB-to-GB index. The scanner therefore caps this to the active
|
|
140938
|
+
// tier's snippetMax and accepts that `persistent: false` has lower recall than
|
|
140939
|
+
// SQLite — an honest, documented divergence rather than a silent one.
|
|
140940
|
+
addDocument(meta3, searchContent = "") {
|
|
140888
140941
|
this.documents.set(meta3.id, meta3);
|
|
140889
|
-
this.
|
|
140890
|
-
|
|
140891
|
-
content: meta3.contentSnippet,
|
|
140892
|
-
projectName: meta3.projectName,
|
|
140893
|
-
projectPath: meta3.projectPath,
|
|
140894
|
-
sessionId: meta3.sessionId,
|
|
140895
|
-
sessionName: meta3.sessionName,
|
|
140896
|
-
account: meta3.account,
|
|
140897
|
-
model: meta3.model || "",
|
|
140898
|
-
gitBranch: meta3.gitBranch || "",
|
|
140899
|
-
toolNames: meta3.toolNames.join(" ")
|
|
140900
|
-
});
|
|
140942
|
+
this.searchContents.set(meta3.id, searchContent);
|
|
140943
|
+
this.index.add(toIndexDoc(meta3, searchContent));
|
|
140901
140944
|
}
|
|
140902
|
-
buildIndex(metas) {
|
|
140945
|
+
buildIndex(metas, searchContents) {
|
|
140903
140946
|
this.clear();
|
|
140904
140947
|
for (const meta3 of metas) {
|
|
140905
|
-
this.addDocument(meta3);
|
|
140948
|
+
this.addDocument(meta3, searchContents?.get(meta3.id) ?? "");
|
|
140906
140949
|
}
|
|
140907
140950
|
getLogger2().debug({ docCount: metas.length }, "indexer: built");
|
|
140908
140951
|
}
|
|
@@ -140922,14 +140965,26 @@ var SearchIndexer = class {
|
|
|
140922
140965
|
seen.add(id);
|
|
140923
140966
|
const meta3 = this.documents.get(id);
|
|
140924
140967
|
if (!meta3) continue;
|
|
140925
|
-
|
|
140926
|
-
|
|
140968
|
+
searchResults.push({
|
|
140969
|
+
meta: meta3,
|
|
140970
|
+
score: 1,
|
|
140971
|
+
matches: this.matchesFor(meta3, query)
|
|
140972
|
+
});
|
|
140927
140973
|
if (searchResults.length >= limit) break;
|
|
140928
140974
|
}
|
|
140929
140975
|
if (searchResults.length >= limit) break;
|
|
140930
140976
|
}
|
|
140931
140977
|
return searchResults;
|
|
140932
140978
|
}
|
|
140979
|
+
// Body context first (that is what explains why the result appeared), then any
|
|
140980
|
+
// metadata matches. Only when neither hits does generateMatches' preview
|
|
140981
|
+
// fallback stand in.
|
|
140982
|
+
matchesFor(meta3, query) {
|
|
140983
|
+
const contentMatch = buildContentMatch(this.searchContents.get(meta3.id) ?? "", query);
|
|
140984
|
+
const metaMatches = generateMatches(meta3, query);
|
|
140985
|
+
if (!contentMatch) return metaMatches;
|
|
140986
|
+
return [contentMatch, ...metaMatches.filter((m2) => m2.field !== "preview")];
|
|
140987
|
+
}
|
|
140933
140988
|
getRecent(limit) {
|
|
140934
140989
|
return Array.from(this.documents.values()).sort((a, b2) => b2.timestamp.localeCompare(a.timestamp)).slice(0, limit).map((meta3) => ({
|
|
140935
140990
|
meta: meta3,
|
|
@@ -140943,31 +140998,37 @@ var SearchIndexer = class {
|
|
|
140943
140998
|
// Replace an already-indexed document in place. FlexSearch's `add` does not
|
|
140944
140999
|
// overwrite an existing id, so a single-file refresh must go through
|
|
140945
141000
|
// `update` to avoid stale matches lingering in the index.
|
|
140946
|
-
updateDocument(meta3) {
|
|
141001
|
+
updateDocument(meta3, searchContent = "") {
|
|
140947
141002
|
this.documents.set(meta3.id, meta3);
|
|
140948
|
-
this.
|
|
140949
|
-
|
|
140950
|
-
content: meta3.contentSnippet,
|
|
140951
|
-
projectName: meta3.projectName,
|
|
140952
|
-
projectPath: meta3.projectPath,
|
|
140953
|
-
sessionId: meta3.sessionId,
|
|
140954
|
-
sessionName: meta3.sessionName,
|
|
140955
|
-
account: meta3.account,
|
|
140956
|
-
model: meta3.model || "",
|
|
140957
|
-
gitBranch: meta3.gitBranch || "",
|
|
140958
|
-
toolNames: meta3.toolNames.join(" ")
|
|
140959
|
-
});
|
|
141003
|
+
this.searchContents.set(meta3.id, searchContent);
|
|
141004
|
+
this.index.update(toIndexDoc(meta3, searchContent));
|
|
140960
141005
|
}
|
|
140961
141006
|
removeDocument(id) {
|
|
140962
141007
|
this.documents.delete(id);
|
|
141008
|
+
this.searchContents.delete(id);
|
|
140963
141009
|
this.index.remove(id);
|
|
140964
141010
|
}
|
|
140965
141011
|
clear() {
|
|
140966
141012
|
this.documents.clear();
|
|
141013
|
+
this.searchContents.clear();
|
|
140967
141014
|
this.index = this.createIndex();
|
|
140968
141015
|
getLogger2().trace("indexer: cleared");
|
|
140969
141016
|
}
|
|
140970
141017
|
};
|
|
141018
|
+
function toIndexDoc(meta3, searchContent) {
|
|
141019
|
+
return {
|
|
141020
|
+
id: meta3.id,
|
|
141021
|
+
content: searchContent,
|
|
141022
|
+
projectName: meta3.projectName,
|
|
141023
|
+
projectPath: meta3.projectPath,
|
|
141024
|
+
sessionId: meta3.sessionId,
|
|
141025
|
+
sessionName: meta3.sessionName,
|
|
141026
|
+
account: meta3.account,
|
|
141027
|
+
model: meta3.model || "",
|
|
141028
|
+
gitBranch: meta3.gitBranch || "",
|
|
141029
|
+
toolNames: meta3.toolNames.join(" ")
|
|
141030
|
+
};
|
|
141031
|
+
}
|
|
140971
141032
|
var CLAUDE_CODE_PROVIDER2 = "claude-code";
|
|
140972
141033
|
var CODEX_CLI_PROVIDER2 = "codex-cli";
|
|
140973
141034
|
function initialReducerState() {
|
|
@@ -141117,7 +141178,7 @@ var SYSTEM_TAG_RE = new RegExp(`<(${SYSTEM_TAGS.join("|")})[^>]*>[\\s\\S]*?<\\/\
|
|
|
141117
141178
|
function cleanSystemTags(text) {
|
|
141118
141179
|
return text.replace(SYSTEM_TAG_RE, "").replace(/[^\S\n]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
141119
141180
|
}
|
|
141120
|
-
async function parseMeta(filePath, account, tier) {
|
|
141181
|
+
async function parseMeta(filePath, account, tier, onEntry) {
|
|
141121
141182
|
const log15 = getLogger2();
|
|
141122
141183
|
log15.trace({ filePath, account, tier: tier.name }, "parseMeta: start");
|
|
141123
141184
|
const state = initialReducerState();
|
|
@@ -141134,6 +141195,7 @@ async function parseMeta(filePath, account, tier) {
|
|
|
141134
141195
|
continue;
|
|
141135
141196
|
}
|
|
141136
141197
|
reduceLine(state, entry, tier);
|
|
141198
|
+
onEntry?.(entry);
|
|
141137
141199
|
}
|
|
141138
141200
|
} catch (err) {
|
|
141139
141201
|
log15.warn({ filePath, err }, "parseMeta: read failed");
|
|
@@ -141828,7 +141890,7 @@ var LRUCache = class {
|
|
|
141828
141890
|
}
|
|
141829
141891
|
};
|
|
141830
141892
|
var YIELD_EVERY_LINES = 500;
|
|
141831
|
-
async function tailReduce(filePath, startOffset, startLine, state, tier) {
|
|
141893
|
+
async function tailReduce(filePath, startOffset, startLine, state, tier, onEntry) {
|
|
141832
141894
|
const stream = (0, import_fs11.createReadStream)(filePath, { start: startOffset, encoding: "utf8" });
|
|
141833
141895
|
let buffer = "";
|
|
141834
141896
|
let offset = startOffset;
|
|
@@ -141844,7 +141906,9 @@ async function tailReduce(filePath, startOffset, startLine, state, tier) {
|
|
|
141844
141906
|
buffer = buffer.slice(nl + 1);
|
|
141845
141907
|
if (text.length > 0) {
|
|
141846
141908
|
try {
|
|
141847
|
-
|
|
141909
|
+
const entry = JSON.parse(text);
|
|
141910
|
+
reduceLine(state, entry, tier);
|
|
141911
|
+
onEntry?.(entry);
|
|
141848
141912
|
} catch {
|
|
141849
141913
|
state.badJsonLines++;
|
|
141850
141914
|
}
|
|
@@ -142065,7 +142129,7 @@ function classify(filePath, existing) {
|
|
|
142065
142129
|
}
|
|
142066
142130
|
return { change: "reindex", stat: stat42 };
|
|
142067
142131
|
}
|
|
142068
|
-
async function parseMetaWithProvider(provider, filePath, account, tier) {
|
|
142132
|
+
async function parseMetaWithProvider(provider, filePath, account, tier, onEntry) {
|
|
142069
142133
|
const log15 = getLogger2();
|
|
142070
142134
|
const acc = provider.createEmptyAccumulator();
|
|
142071
142135
|
const rl = (0, import_readline3.createInterface)({
|
|
@@ -142083,6 +142147,7 @@ async function parseMetaWithProvider(provider, filePath, account, tier) {
|
|
|
142083
142147
|
}
|
|
142084
142148
|
try {
|
|
142085
142149
|
provider.reduceEntry(acc, entry, tier);
|
|
142150
|
+
onEntry?.(entry);
|
|
142086
142151
|
} catch (err) {
|
|
142087
142152
|
log15.warn({ filePath, provider: provider.name, err }, "provider reduce threw; line skipped");
|
|
142088
142153
|
}
|
|
@@ -142093,6 +142158,149 @@ async function parseMetaWithProvider(provider, filePath, account, tier) {
|
|
|
142093
142158
|
}
|
|
142094
142159
|
return provider.finalize(acc, filePath, account, tier);
|
|
142095
142160
|
}
|
|
142161
|
+
var SEARCH_BUDGET = {
|
|
142162
|
+
textMax: 64 * 1024,
|
|
142163
|
+
thinkingMax: 32 * 1024,
|
|
142164
|
+
toolsMax: 32 * 1024,
|
|
142165
|
+
toolPayloadMax: 4 * 1024
|
|
142166
|
+
};
|
|
142167
|
+
var SEP = "\n\n";
|
|
142168
|
+
function emptySearchDocument() {
|
|
142169
|
+
return { text: "", thinking: "", tools: "" };
|
|
142170
|
+
}
|
|
142171
|
+
function appendSearchDelta(doc, delta) {
|
|
142172
|
+
return {
|
|
142173
|
+
text: tailAppend(doc.text, delta.text, SEARCH_BUDGET.textMax),
|
|
142174
|
+
thinking: tailAppend(doc.thinking, delta.thinking, SEARCH_BUDGET.thinkingMax),
|
|
142175
|
+
tools: tailAppend(doc.tools, delta.tools, SEARCH_BUDGET.toolsMax)
|
|
142176
|
+
};
|
|
142177
|
+
}
|
|
142178
|
+
function tailAppend(current, incoming, max) {
|
|
142179
|
+
if (!incoming) return current;
|
|
142180
|
+
const joined = current ? current + SEP + incoming : incoming;
|
|
142181
|
+
return joined.length <= max ? joined : joined.slice(-max);
|
|
142182
|
+
}
|
|
142183
|
+
function combineSearchContent(doc) {
|
|
142184
|
+
return [doc.text, doc.thinking, doc.tools].filter(Boolean).join(SEP);
|
|
142185
|
+
}
|
|
142186
|
+
function capToolPayload(value) {
|
|
142187
|
+
const raw2 = stringifyPayload(value);
|
|
142188
|
+
if (!raw2) return "";
|
|
142189
|
+
return raw2.length > SEARCH_BUDGET.toolPayloadMax ? raw2.slice(0, SEARCH_BUDGET.toolPayloadMax) : raw2;
|
|
142190
|
+
}
|
|
142191
|
+
function stringifyPayload(value) {
|
|
142192
|
+
if (value === null || value === void 0) return "";
|
|
142193
|
+
if (typeof value === "string") return value;
|
|
142194
|
+
try {
|
|
142195
|
+
return JSON.stringify(value) ?? "";
|
|
142196
|
+
} catch {
|
|
142197
|
+
return "";
|
|
142198
|
+
}
|
|
142199
|
+
}
|
|
142200
|
+
function extractSearchDelta(entry) {
|
|
142201
|
+
if (entry.type === "response_item" || entry.type === "session_meta") {
|
|
142202
|
+
return extractCodexDelta(entry);
|
|
142203
|
+
}
|
|
142204
|
+
return extractClaudeDelta(entry);
|
|
142205
|
+
}
|
|
142206
|
+
function extractClaudeDelta(entry) {
|
|
142207
|
+
const type = entry.type;
|
|
142208
|
+
if (type !== "user" && type !== "assistant") return emptySearchDocument();
|
|
142209
|
+
if (entry.isMeta) return emptySearchDocument();
|
|
142210
|
+
const msg = entry.message;
|
|
142211
|
+
const content = msg?.content;
|
|
142212
|
+
const tools = [
|
|
142213
|
+
extractClaudeToolContent(content),
|
|
142214
|
+
// Claude stores the rich/structured tool result at the JSONL entry's top
|
|
142215
|
+
// level, not inside message.content — indexing only message.content would
|
|
142216
|
+
// miss most real tool output (file reads, command stdout).
|
|
142217
|
+
capToolPayload(entry.toolUseResult)
|
|
142218
|
+
].filter(Boolean).join(SEP);
|
|
142219
|
+
return {
|
|
142220
|
+
text: extractClaudeText(content),
|
|
142221
|
+
thinking: type === "assistant" ? extractThinking(content).content : "",
|
|
142222
|
+
tools
|
|
142223
|
+
};
|
|
142224
|
+
}
|
|
142225
|
+
function extractClaudeText(content) {
|
|
142226
|
+
if (typeof content === "string") return cleanSystemTags(content);
|
|
142227
|
+
if (!Array.isArray(content)) return "";
|
|
142228
|
+
const parts = [];
|
|
142229
|
+
for (const item of content) {
|
|
142230
|
+
if (typeof item === "string") {
|
|
142231
|
+
const cleaned = cleanSystemTags(item);
|
|
142232
|
+
if (cleaned) parts.push(cleaned);
|
|
142233
|
+
} else if (item?.type === "text" && typeof item.text === "string") {
|
|
142234
|
+
const cleaned = cleanSystemTags(item.text);
|
|
142235
|
+
if (cleaned) parts.push(cleaned);
|
|
142236
|
+
}
|
|
142237
|
+
}
|
|
142238
|
+
return parts.join(SEP);
|
|
142239
|
+
}
|
|
142240
|
+
function extractClaudeToolContent(content) {
|
|
142241
|
+
if (!Array.isArray(content)) return "";
|
|
142242
|
+
const parts = [];
|
|
142243
|
+
for (const item of content) {
|
|
142244
|
+
if (item?.type === "tool_use") {
|
|
142245
|
+
const capped = capToolPayload(item.input);
|
|
142246
|
+
if (capped) parts.push(capped);
|
|
142247
|
+
} else if (item?.type === "tool_result") {
|
|
142248
|
+
const capped = capToolPayload(item.content);
|
|
142249
|
+
if (capped) parts.push(capped);
|
|
142250
|
+
}
|
|
142251
|
+
}
|
|
142252
|
+
return parts.join(SEP);
|
|
142253
|
+
}
|
|
142254
|
+
function extractCodexDelta(entry) {
|
|
142255
|
+
const payload = entry.payload;
|
|
142256
|
+
if (!payload || typeof payload !== "object") return emptySearchDocument();
|
|
142257
|
+
const ptype = payload.type;
|
|
142258
|
+
if (ptype === "function_call" || ptype === "custom_tool_call") {
|
|
142259
|
+
return { text: "", thinking: "", tools: capToolPayload(payload.arguments) };
|
|
142260
|
+
}
|
|
142261
|
+
if (ptype === "function_call_output" || ptype === "custom_tool_call_output") {
|
|
142262
|
+
return { text: "", thinking: "", tools: capToolPayload(payload.output) };
|
|
142263
|
+
}
|
|
142264
|
+
if (ptype === "reasoning") {
|
|
142265
|
+
return { text: "", thinking: extractCodexReasoning(payload), tools: "" };
|
|
142266
|
+
}
|
|
142267
|
+
if (ptype === "message") {
|
|
142268
|
+
const role = payload.role;
|
|
142269
|
+
if (role !== "user" && role !== "assistant") return emptySearchDocument();
|
|
142270
|
+
return { text: extractCodexText2(payload.content), thinking: "", tools: "" };
|
|
142271
|
+
}
|
|
142272
|
+
return emptySearchDocument();
|
|
142273
|
+
}
|
|
142274
|
+
function extractCodexReasoning(payload) {
|
|
142275
|
+
const parts = [];
|
|
142276
|
+
for (const key of ["summary", "content"]) {
|
|
142277
|
+
const blocks = payload[key];
|
|
142278
|
+
if (!Array.isArray(blocks)) continue;
|
|
142279
|
+
for (const block of blocks) {
|
|
142280
|
+
if (typeof block === "string") parts.push(block);
|
|
142281
|
+
else if (typeof block?.text === "string") parts.push(block.text);
|
|
142282
|
+
}
|
|
142283
|
+
}
|
|
142284
|
+
return parts.filter(Boolean).join(SEP);
|
|
142285
|
+
}
|
|
142286
|
+
function extractCodexText2(content) {
|
|
142287
|
+
if (typeof content === "string") return cleanSystemTags(content);
|
|
142288
|
+
if (!Array.isArray(content)) return "";
|
|
142289
|
+
const parts = [];
|
|
142290
|
+
for (const item of content) {
|
|
142291
|
+
if (typeof item === "string") {
|
|
142292
|
+
const cleaned = cleanSystemTags(item);
|
|
142293
|
+
if (cleaned) parts.push(cleaned);
|
|
142294
|
+
continue;
|
|
142295
|
+
}
|
|
142296
|
+
const t = item?.type;
|
|
142297
|
+
if ((t === "input_text" || t === "output_text" || t === "text") && typeof item.text === "string") {
|
|
142298
|
+
const cleaned = cleanSystemTags(item.text);
|
|
142299
|
+
if (cleaned) parts.push(cleaned);
|
|
142300
|
+
}
|
|
142301
|
+
}
|
|
142302
|
+
return parts.join(SEP);
|
|
142303
|
+
}
|
|
142096
142304
|
var DEFAULT_TIERS = {
|
|
142097
142305
|
standard: { name: "standard", previewMax: 200, snippetMax: 5e3 },
|
|
142098
142306
|
full: { name: "full", previewMax: 1200, snippetMax: 5e4 }
|
|
@@ -142106,7 +142314,7 @@ function resolveTier(tierName, customTiers) {
|
|
|
142106
142314
|
}
|
|
142107
142315
|
return tier;
|
|
142108
142316
|
}
|
|
142109
|
-
var SCHEMA_VERSION =
|
|
142317
|
+
var SCHEMA_VERSION = 5;
|
|
142110
142318
|
var SCHEMA_SQL = `
|
|
142111
142319
|
CREATE TABLE IF NOT EXISTS conversation_files (
|
|
142112
142320
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -142206,13 +142414,27 @@ CREATE INDEX IF NOT EXISTS idx_conversations_account_recent ON conversations(acc
|
|
|
142206
142414
|
CREATE INDEX IF NOT EXISTS idx_conversations_subagent_recent ON conversations(is_subagent, timestamp DESC);
|
|
142207
142415
|
CREATE INDEX IF NOT EXISTS idx_conversations_team_recent ON conversations(team_name, timestamp DESC);
|
|
142208
142416
|
|
|
142209
|
-
-- Full-text search index over conversation
|
|
142210
|
-
--
|
|
142211
|
-
--
|
|
142212
|
-
--
|
|
142417
|
+
-- Full-text search index over conversation body + metadata. Kept separate from
|
|
142418
|
+
-- the metadata tables so list-screen queries stay small and fast. One FTS row
|
|
142419
|
+
-- per conversation, replaced on each upsert.
|
|
142420
|
+
--
|
|
142421
|
+
-- The row's rowid IS conversation_files.id. That is load-bearing, not cosmetic:
|
|
142422
|
+
-- FTS5's query planner only handles MATCH, rowid and rank, so any other
|
|
142423
|
+
-- constraint (including a source_path equality on an UNINDEXED column) has no
|
|
142424
|
+
-- index and linear-scans the whole table. At ~128 KB of body per row that would
|
|
142425
|
+
-- mean scanning the entire corpus on every append. Look rows up by rowid.
|
|
142426
|
+
-- source_path stays stored-but-UNINDEXED so a search hit can resolve back to a
|
|
142427
|
+
-- conversations row.
|
|
142428
|
+
--
|
|
142429
|
+
-- Body is three columns, not one: text > thinking > tools priority has to
|
|
142430
|
+
-- survive an append (a new user message belongs in the text column, not after
|
|
142431
|
+
-- the tool output already written). They are deliberately NOT concatenated into
|
|
142432
|
+
-- a fourth column - that would double-weight body hits and inflate bm25 length.
|
|
142213
142433
|
CREATE VIRTUAL TABLE IF NOT EXISTS conversation_messages_fts USING fts5(
|
|
142214
142434
|
source_path UNINDEXED,
|
|
142215
|
-
|
|
142435
|
+
text,
|
|
142436
|
+
thinking,
|
|
142437
|
+
tools,
|
|
142216
142438
|
project_name,
|
|
142217
142439
|
session_id,
|
|
142218
142440
|
session_name,
|
|
@@ -142280,6 +142502,24 @@ function runMigrations(db) {
|
|
|
142280
142502
|
if (current >= 1 && current < 3 && tableExists(db, "conversations")) {
|
|
142281
142503
|
db.exec("UPDATE conversations SET provider = 'claude-code' WHERE provider = 'threadbase'");
|
|
142282
142504
|
}
|
|
142505
|
+
if (current >= 1 && current < 5) {
|
|
142506
|
+
db.exec("DROP TABLE IF EXISTS conversation_messages_fts");
|
|
142507
|
+
if (tableExists(db, "conversation_files")) {
|
|
142508
|
+
const assignments = [];
|
|
142509
|
+
if (hasColumn(db, "conversation_files", "last_indexed_offset")) {
|
|
142510
|
+
assignments.push("last_indexed_offset = 0");
|
|
142511
|
+
}
|
|
142512
|
+
if (hasColumn(db, "conversation_files", "last_indexed_line")) {
|
|
142513
|
+
assignments.push("last_indexed_line = 0");
|
|
142514
|
+
}
|
|
142515
|
+
if (hasColumn(db, "conversation_files", "reducer_state")) {
|
|
142516
|
+
assignments.push("reducer_state = NULL");
|
|
142517
|
+
}
|
|
142518
|
+
if (assignments.length > 0) {
|
|
142519
|
+
db.exec(`UPDATE conversation_files SET ${assignments.join(", ")}`);
|
|
142520
|
+
}
|
|
142521
|
+
}
|
|
142522
|
+
}
|
|
142283
142523
|
db.exec(SCHEMA_SQL);
|
|
142284
142524
|
db.pragma(`user_version = ${SCHEMA_VERSION}`);
|
|
142285
142525
|
}
|
|
@@ -142299,6 +142539,7 @@ function openDatabase(dbPath) {
|
|
|
142299
142539
|
db.pragma("synchronous = NORMAL");
|
|
142300
142540
|
db.pragma("temp_store = MEMORY");
|
|
142301
142541
|
db.pragma("foreign_keys = ON");
|
|
142542
|
+
db.pragma("busy_timeout = 5000");
|
|
142302
142543
|
runMigrations(db);
|
|
142303
142544
|
getLogger2().debug({ dbPath }, "db: opened");
|
|
142304
142545
|
return db;
|
|
@@ -142733,22 +142974,31 @@ var ConversationsRepo = class {
|
|
|
142733
142974
|
return this.db.prepare("SELECT COUNT(*) AS n FROM conversations WHERE status = 'active'").get().n;
|
|
142734
142975
|
}
|
|
142735
142976
|
};
|
|
142977
|
+
var BODY_COLUMNS = [
|
|
142978
|
+
{ name: "text", index: 1 },
|
|
142979
|
+
{ name: "thinking", index: 2 },
|
|
142980
|
+
{ name: "tools", index: 3 }
|
|
142981
|
+
];
|
|
142736
142982
|
var FtsRepo = class {
|
|
142737
142983
|
constructor(db) {
|
|
142738
142984
|
this.db = db;
|
|
142739
142985
|
}
|
|
142740
142986
|
db;
|
|
142741
|
-
upsert(meta3) {
|
|
142987
|
+
upsert(rowId, meta3, doc) {
|
|
142742
142988
|
const sourcePath = canonicalPath(meta3.id);
|
|
142743
142989
|
const tx = this.db.transaction(() => {
|
|
142744
|
-
this.db.prepare("DELETE FROM conversation_messages_fts WHERE
|
|
142990
|
+
this.db.prepare("DELETE FROM conversation_messages_fts WHERE rowid = ?").run(rowId);
|
|
142745
142991
|
this.db.prepare(
|
|
142746
142992
|
`INSERT INTO conversation_messages_fts
|
|
142747
|
-
(
|
|
142748
|
-
|
|
142993
|
+
(rowid, source_path, text, thinking, tools,
|
|
142994
|
+
project_name, session_id, session_name, account, model, branch, tool_names)
|
|
142995
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
142749
142996
|
).run(
|
|
142997
|
+
rowId,
|
|
142750
142998
|
sourcePath,
|
|
142751
|
-
|
|
142999
|
+
doc.text,
|
|
143000
|
+
doc.thinking,
|
|
143001
|
+
doc.tools,
|
|
142752
143002
|
meta3.projectName ?? "",
|
|
142753
143003
|
meta3.sessionId ?? "",
|
|
142754
143004
|
meta3.sessionName ?? "",
|
|
@@ -142760,26 +143010,94 @@ var FtsRepo = class {
|
|
|
142760
143010
|
});
|
|
142761
143011
|
tx();
|
|
142762
143012
|
}
|
|
142763
|
-
|
|
142764
|
-
|
|
143013
|
+
// Current durable buckets for a conversation, so an append can tail-extend
|
|
143014
|
+
// them without reparsing the file. Returns null when no row exists yet.
|
|
143015
|
+
readDocument(rowId) {
|
|
143016
|
+
const row = this.db.prepare("SELECT text, thinking, tools FROM conversation_messages_fts WHERE rowid = ?").get(rowId);
|
|
143017
|
+
if (!row) return null;
|
|
143018
|
+
return { text: row.text ?? "", thinking: row.thinking ?? "", tools: row.tools ?? "" };
|
|
143019
|
+
}
|
|
143020
|
+
// True when the stored row already equals what we would write, so an append
|
|
143021
|
+
// can skip re-tokenizing ~128 KB of body for nothing.
|
|
143022
|
+
//
|
|
143023
|
+
// The check covers the metadata columns too, not just the buckets: a
|
|
143024
|
+
// newly-seen tool name or a late-resolved session_name changes metadata while
|
|
143025
|
+
// the body is byte-identical, and nothing else ever rewrites this row — so
|
|
143026
|
+
// skipping on "body unchanged" alone would strand that stale value forever.
|
|
143027
|
+
isCurrent(rowId, meta3, doc) {
|
|
143028
|
+
const row = this.db.prepare(
|
|
143029
|
+
`SELECT text, thinking, tools,
|
|
143030
|
+
project_name, session_id, session_name, account, model, branch, tool_names
|
|
143031
|
+
FROM conversation_messages_fts WHERE rowid = ?`
|
|
143032
|
+
).get(rowId);
|
|
143033
|
+
if (!row) return false;
|
|
143034
|
+
return row.text === doc.text && row.thinking === doc.thinking && row.tools === doc.tools && row.project_name === (meta3.projectName ?? "") && row.session_id === (meta3.sessionId ?? "") && row.session_name === (meta3.sessionName ?? "") && row.account === (meta3.account ?? "") && row.model === (meta3.model ?? "") && row.branch === (meta3.gitBranch ?? "") && row.tool_names === meta3.toolNames.join(" ");
|
|
142765
143035
|
}
|
|
142766
|
-
|
|
142767
|
-
|
|
142768
|
-
|
|
143036
|
+
remove(rowId) {
|
|
143037
|
+
this.db.prepare("DELETE FROM conversation_messages_fts WHERE rowid = ?").run(rowId);
|
|
143038
|
+
}
|
|
143039
|
+
// Ranked hits matching the query, best first. Filters run in SQL BEFORE LIMIT:
|
|
143040
|
+
// with a wide corpus a popular term matches far more conversations than one
|
|
143041
|
+
// page, so post-filtering an already-truncated list would report "no results"
|
|
143042
|
+
// for queries that do have them.
|
|
143043
|
+
search(query, limit, filters = {}) {
|
|
142769
143044
|
const match2 = toMatchQuery(query);
|
|
142770
143045
|
if (!match2) return [];
|
|
143046
|
+
const snippetSelects = BODY_COLUMNS.map(
|
|
143047
|
+
(c) => `snippet(conversation_messages_fts, ${c.index}, ?, ?, ?, ?) AS snippet_${c.name}`
|
|
143048
|
+
).join(", ");
|
|
143049
|
+
const params = [];
|
|
143050
|
+
for (const _col of BODY_COLUMNS) {
|
|
143051
|
+
params.push(FTS_HIT_OPEN, FTS_HIT_CLOSE, FTS_ELLIPSIS, FTS_SNIPPET_TOKENS);
|
|
143052
|
+
}
|
|
143053
|
+
params.push(match2);
|
|
143054
|
+
const where = ["conversation_messages_fts MATCH ?", "c.status = 'active'"];
|
|
143055
|
+
if (filters.account) {
|
|
143056
|
+
where.push("c.account = ?");
|
|
143057
|
+
params.push(filters.account);
|
|
143058
|
+
}
|
|
143059
|
+
if (filters.provider) {
|
|
143060
|
+
where.push("c.provider = ?");
|
|
143061
|
+
params.push(filters.provider);
|
|
143062
|
+
}
|
|
143063
|
+
if (filters.project) {
|
|
143064
|
+
where.push("(lower(c.project_path) LIKE ? OR lower(c.project_name) LIKE ?)");
|
|
143065
|
+
const like = `%${filters.project.toLowerCase()}%`;
|
|
143066
|
+
params.push(like, like);
|
|
143067
|
+
}
|
|
143068
|
+
if (filters.since) {
|
|
143069
|
+
where.push("c.timestamp >= ?");
|
|
143070
|
+
params.push(filters.since.toISOString());
|
|
143071
|
+
}
|
|
143072
|
+
if (filters.include === "conversations") {
|
|
143073
|
+
where.push("c.is_subagent = 0 AND c.is_teammate = 0");
|
|
143074
|
+
} else if (filters.include === "subagents") {
|
|
143075
|
+
where.push("c.is_subagent = 1");
|
|
143076
|
+
} else if (filters.include === "teammates") {
|
|
143077
|
+
where.push("c.is_teammate = 1");
|
|
143078
|
+
}
|
|
143079
|
+
params.push(limit);
|
|
142771
143080
|
const rows = this.db.prepare(
|
|
142772
|
-
`SELECT source_path
|
|
142773
|
-
|
|
143081
|
+
`SELECT conversation_messages_fts.source_path AS source_path, ${snippetSelects}
|
|
143082
|
+
FROM conversation_messages_fts
|
|
143083
|
+
JOIN conversations c ON c.source_path = conversation_messages_fts.source_path
|
|
143084
|
+
WHERE ${where.join(" AND ")}
|
|
142774
143085
|
ORDER BY rank
|
|
142775
143086
|
LIMIT ?`
|
|
142776
|
-
).all(
|
|
142777
|
-
return rows.map((
|
|
143087
|
+
).all(...params);
|
|
143088
|
+
return rows.map((row) => ({ sourcePath: row.source_path, body: pickBodySnippet(row) }));
|
|
142778
143089
|
}
|
|
142779
143090
|
count() {
|
|
142780
143091
|
return this.db.prepare("SELECT COUNT(*) AS n FROM conversation_messages_fts").get().n;
|
|
142781
143092
|
}
|
|
142782
143093
|
};
|
|
143094
|
+
function pickBodySnippet(row) {
|
|
143095
|
+
for (const col of BODY_COLUMNS) {
|
|
143096
|
+
const parsed = parseFtsSnippet(row[`snippet_${col.name}`]);
|
|
143097
|
+
if (parsed) return parsed;
|
|
143098
|
+
}
|
|
143099
|
+
return null;
|
|
143100
|
+
}
|
|
142783
143101
|
function toMatchQuery(query) {
|
|
142784
143102
|
const terms = query.trim().split(/\s+/).map((t) => t.replace(/"/g, "").trim()).filter(Boolean);
|
|
142785
143103
|
if (terms.length === 0) return "";
|
|
@@ -142955,9 +143273,13 @@ var PersistentEngine = class {
|
|
|
142955
143273
|
const state = resume ? JSON.parse(existing.reducer_state) : initialReducerState();
|
|
142956
143274
|
const startOffset = resume ? existing.last_indexed_offset : 0;
|
|
142957
143275
|
const startLine = resume ? existing.last_indexed_line : 0;
|
|
143276
|
+
let searchDelta = emptySearchDocument();
|
|
143277
|
+
const collectSearch = (entry) => {
|
|
143278
|
+
searchDelta = appendSearchDelta(searchDelta, extractSearchDelta(entry));
|
|
143279
|
+
};
|
|
142958
143280
|
let result;
|
|
142959
143281
|
try {
|
|
142960
|
-
result = await tailReduce(filePath, startOffset, startLine, state, tier);
|
|
143282
|
+
result = await tailReduce(filePath, startOffset, startLine, state, tier, collectSearch);
|
|
142961
143283
|
} catch (err) {
|
|
142962
143284
|
log15.warn({ filePath, err }, "persistent: tail read failed");
|
|
142963
143285
|
return { meta: null, change };
|
|
@@ -142970,9 +143292,13 @@ var PersistentEngine = class {
|
|
|
142970
143292
|
meta3.gitBranch = resolveGitBranch(meta3.projectPath);
|
|
142971
143293
|
const fp = stat42.size > 0 ? fingerprint(filePath, stat42.size) : null;
|
|
142972
143294
|
const fileId = this.files.ensure(filePath, account);
|
|
143295
|
+
const base = resume ? this.fts.readDocument(fileId) ?? emptySearchDocument() : emptySearchDocument();
|
|
143296
|
+
const searchDoc = appendSearchDelta(base, searchDelta);
|
|
142973
143297
|
const upsert = this.db.transaction(() => {
|
|
142974
143298
|
this.conversations.upsert(fileId, meta3, state.pageMessageCount);
|
|
142975
|
-
this.fts.
|
|
143299
|
+
if (!this.fts.isCurrent(fileId, meta3, searchDoc)) {
|
|
143300
|
+
this.fts.upsert(fileId, meta3, searchDoc);
|
|
143301
|
+
}
|
|
142976
143302
|
if (!resume) this.checkpoints.remove(filePath);
|
|
142977
143303
|
this.files.updateCursor(fileId, {
|
|
142978
143304
|
sizeBytes: stat42.size,
|
|
@@ -143015,7 +143341,10 @@ var PersistentEngine = class {
|
|
|
143015
143341
|
// any change reparses from 0 again. No reducer_state is persisted.
|
|
143016
143342
|
async indexFileWithProvider(provider, filePath, account, tier, stat42, resolveGitBranch) {
|
|
143017
143343
|
const log15 = getLogger2();
|
|
143018
|
-
|
|
143344
|
+
let searchDoc = emptySearchDocument();
|
|
143345
|
+
const meta3 = await parseMetaWithProvider(provider, filePath, account, tier, (entry) => {
|
|
143346
|
+
searchDoc = appendSearchDelta(searchDoc, extractSearchDelta(entry));
|
|
143347
|
+
});
|
|
143019
143348
|
if (!meta3) {
|
|
143020
143349
|
this.markDeleted(filePath);
|
|
143021
143350
|
return null;
|
|
@@ -143027,7 +143356,9 @@ var PersistentEngine = class {
|
|
|
143027
143356
|
const fileId = this.files.ensure(filePath, account);
|
|
143028
143357
|
const upsert = this.db.transaction(() => {
|
|
143029
143358
|
this.conversations.upsert(fileId, meta3, meta3.messageCount);
|
|
143030
|
-
this.fts.
|
|
143359
|
+
if (!this.fts.isCurrent(fileId, meta3, searchDoc)) {
|
|
143360
|
+
this.fts.upsert(fileId, meta3, searchDoc);
|
|
143361
|
+
}
|
|
143031
143362
|
this.checkpoints.remove(filePath);
|
|
143032
143363
|
this.files.updateCursor(fileId, {
|
|
143033
143364
|
sizeBytes: stat42.size,
|
|
@@ -143053,7 +143384,7 @@ var PersistentEngine = class {
|
|
|
143053
143384
|
if (!existing) return;
|
|
143054
143385
|
const tx = this.db.transaction(() => {
|
|
143055
143386
|
this.conversations.deleteByFileId(existing.id);
|
|
143056
|
-
this.fts.remove(
|
|
143387
|
+
this.fts.remove(existing.id);
|
|
143057
143388
|
this.checkpoints.remove(filePath);
|
|
143058
143389
|
this.files.setStatus(existing.id, "deleted");
|
|
143059
143390
|
});
|
|
@@ -143069,20 +143400,24 @@ var PersistentEngine = class {
|
|
|
143069
143400
|
getAllBySessionId(sessionId) {
|
|
143070
143401
|
return this.conversations.getAllBySessionId(sessionId);
|
|
143071
143402
|
}
|
|
143072
|
-
// Ranked
|
|
143073
|
-
//
|
|
143074
|
-
//
|
|
143075
|
-
|
|
143076
|
-
|
|
143077
|
-
|
|
143078
|
-
|
|
143079
|
-
|
|
143080
|
-
const
|
|
143081
|
-
for (const
|
|
143082
|
-
const meta3 = this.conversations.getBySourcePath(
|
|
143083
|
-
if (meta3)
|
|
143403
|
+
// Ranked hits matching the FTS query, best first, each already resolved to its
|
|
143404
|
+
// active conversation row and carrying the body excerpt when the match was in
|
|
143405
|
+
// the conversation body.
|
|
143406
|
+
//
|
|
143407
|
+
// Filters are passed down into SQL rather than applied to the result: with a
|
|
143408
|
+
// wide corpus, filtering an already-LIMITed list drops conversations that
|
|
143409
|
+
// would have matched.
|
|
143410
|
+
searchHits(query, limit, filters = {}) {
|
|
143411
|
+
const hits = [];
|
|
143412
|
+
for (const hit of this.fts.search(query, limit, filters)) {
|
|
143413
|
+
const meta3 = this.conversations.getBySourcePath(hit.sourcePath);
|
|
143414
|
+
if (meta3) hits.push({ meta: meta3, body: hit.body });
|
|
143084
143415
|
}
|
|
143085
|
-
return
|
|
143416
|
+
return hits;
|
|
143417
|
+
}
|
|
143418
|
+
// Empty-query listing, mirroring the in-memory indexer's behavior.
|
|
143419
|
+
recentMetas(limit) {
|
|
143420
|
+
return this.conversations.recent(limit);
|
|
143086
143421
|
}
|
|
143087
143422
|
getProjects() {
|
|
143088
143423
|
return this.conversations.distinctProjects();
|
|
@@ -143236,6 +143571,10 @@ var DEFAULT_CONFIG_PATH2 = "~/.config/threadbase-scanner";
|
|
|
143236
143571
|
function defaultDbPath() {
|
|
143237
143572
|
return process.env.TB_SCANNER_DB ?? (0, import_path10.join)((0, import_os8.homedir)(), ".config", "threadbase-scanner", "index.db");
|
|
143238
143573
|
}
|
|
143574
|
+
function capForMemory(doc, max) {
|
|
143575
|
+
const combined = combineSearchContent(doc);
|
|
143576
|
+
return combined.length > max ? combined.slice(-max) : combined;
|
|
143577
|
+
}
|
|
143239
143578
|
var ConversationScanner = class {
|
|
143240
143579
|
metadataCache = /* @__PURE__ */ new Map();
|
|
143241
143580
|
// Parsed conversations plus (persistent claude-code entries only) the resume
|
|
@@ -143247,6 +143586,10 @@ var ConversationScanner = class {
|
|
|
143247
143586
|
sessionIdIndex = /* @__PURE__ */ new Map();
|
|
143248
143587
|
projects = /* @__PURE__ */ new Set();
|
|
143249
143588
|
indexer = new SearchIndexer();
|
|
143589
|
+
// Search body per conversation for the in-memory path, already tail-capped to
|
|
143590
|
+
// the content tier. Survives scan() so a statCache hit — which skips the parse
|
|
143591
|
+
// entirely — can still index a body rather than an empty string.
|
|
143592
|
+
searchContents = /* @__PURE__ */ new Map();
|
|
143250
143593
|
// Tier the most recent scan() ran with, so refreshFile() re-parses a single
|
|
143251
143594
|
// file at the same content depth. Defaults to the standard tier.
|
|
143252
143595
|
lastTier = resolveTier("standard");
|
|
@@ -143407,34 +143750,42 @@ var ConversationScanner = class {
|
|
|
143407
143750
|
try {
|
|
143408
143751
|
const s3 = (0, import_fs9.statSync)(filePath);
|
|
143409
143752
|
if (s3.mtimeMs === cached4.stat.mtimeMs && s3.size === cached4.stat.size) {
|
|
143410
|
-
return
|
|
143753
|
+
return {
|
|
143754
|
+
meta: cached4.meta,
|
|
143755
|
+
searchContent: this.searchContents.get(cached4.meta.id)
|
|
143756
|
+
};
|
|
143411
143757
|
}
|
|
143412
143758
|
} catch {
|
|
143413
143759
|
}
|
|
143414
143760
|
}
|
|
143415
143761
|
}
|
|
143416
143762
|
try {
|
|
143417
|
-
|
|
143763
|
+
let doc = emptySearchDocument();
|
|
143764
|
+
const meta3 = await parseMetaWithProvider(provider, filePath, account, tier, (entry) => {
|
|
143765
|
+
doc = appendSearchDelta(doc, extractSearchDelta(entry));
|
|
143766
|
+
});
|
|
143418
143767
|
if (meta3 && meta3.gitBranch === null && meta3.projectPath) {
|
|
143419
143768
|
meta3.gitBranch = resolveGitBranch(meta3.projectPath);
|
|
143420
143769
|
}
|
|
143421
|
-
return meta3;
|
|
143770
|
+
return { meta: meta3, searchContent: capForMemory(doc, tier.snippetMax) };
|
|
143422
143771
|
} catch (err) {
|
|
143423
143772
|
parseFailures++;
|
|
143424
143773
|
log15.warn({ filePath, account, provider: provider.name, err }, "scan: parse threw");
|
|
143425
|
-
return null;
|
|
143774
|
+
return { meta: null, searchContent: void 0 };
|
|
143426
143775
|
}
|
|
143427
143776
|
})
|
|
143428
143777
|
);
|
|
143429
143778
|
const batchMetas = [];
|
|
143430
|
-
for (const meta3 of results) {
|
|
143779
|
+
for (const { meta: meta3, searchContent } of results) {
|
|
143431
143780
|
if (meta3 && meta3.messageCount > 0) {
|
|
143432
143781
|
this.metadataCache.set(meta3.id, meta3);
|
|
143433
143782
|
this.addToSessionIndex(meta3);
|
|
143434
143783
|
this.projects.add(meta3.projectPath);
|
|
143435
143784
|
allMetas.push(meta3);
|
|
143436
143785
|
batchMetas.push(meta3);
|
|
143437
|
-
this.
|
|
143786
|
+
const content = searchContent ?? this.searchContents.get(meta3.id) ?? "";
|
|
143787
|
+
this.searchContents.set(meta3.id, content);
|
|
143788
|
+
this.indexer.addDocument(meta3, content);
|
|
143438
143789
|
}
|
|
143439
143790
|
}
|
|
143440
143791
|
if (batchMetas.length > 0) {
|
|
@@ -143471,12 +143822,29 @@ var ConversationScanner = class {
|
|
|
143471
143822
|
const activeProfiles = profiles.filter((p2) => p2.enabled && p2.scanHistory !== false);
|
|
143472
143823
|
await engine.indexAll(activeProfiles, { ...options, limit: void 0, offset: void 0 });
|
|
143473
143824
|
}
|
|
143474
|
-
|
|
143475
|
-
|
|
143476
|
-
|
|
143477
|
-
|
|
143478
|
-
|
|
143479
|
-
|
|
143825
|
+
if (query.trim()) {
|
|
143826
|
+
const want = (options.limit ?? 50) + (options.offset ?? 0);
|
|
143827
|
+
results = engine.searchHits(query, want, {
|
|
143828
|
+
account: options.account,
|
|
143829
|
+
provider: options.provider,
|
|
143830
|
+
project: options.project,
|
|
143831
|
+
since: options.since ? parseSinceCutoff(options.since) : void 0,
|
|
143832
|
+
include: options.include
|
|
143833
|
+
}).map(({ meta: meta3, body }) => ({
|
|
143834
|
+
meta: meta3,
|
|
143835
|
+
score: 1,
|
|
143836
|
+
matches: body ? [
|
|
143837
|
+
{ field: CONTENT_FIELD, snippet: body.snippet, highlights: body.highlights },
|
|
143838
|
+
...generateMatches(meta3, query).filter((m2) => m2.field !== "preview")
|
|
143839
|
+
] : generateMatches(meta3, query)
|
|
143840
|
+
}));
|
|
143841
|
+
} else {
|
|
143842
|
+
results = engine.recentMetas((options.limit ?? 50) + (options.offset ?? 0)).map((meta3) => ({
|
|
143843
|
+
meta: meta3,
|
|
143844
|
+
score: 1,
|
|
143845
|
+
matches: [{ field: "timestamp", snippet: meta3.preview }]
|
|
143846
|
+
}));
|
|
143847
|
+
}
|
|
143480
143848
|
} else {
|
|
143481
143849
|
if (this.indexer.getDocumentCount() === 0) {
|
|
143482
143850
|
log15.debug("search: index empty, triggering scan");
|
|
@@ -143670,8 +144038,11 @@ var ConversationScanner = class {
|
|
|
143670
144038
|
const previous = this.metadataCache.get(filePath);
|
|
143671
144039
|
const resolvedAccount = account ?? previous?.account ?? "default";
|
|
143672
144040
|
let meta3 = null;
|
|
144041
|
+
let searchDoc = emptySearchDocument();
|
|
143673
144042
|
try {
|
|
143674
|
-
meta3 = await parseMeta(filePath, resolvedAccount, this.lastTier)
|
|
144043
|
+
meta3 = await parseMeta(filePath, resolvedAccount, this.lastTier, (entry) => {
|
|
144044
|
+
searchDoc = appendSearchDelta(searchDoc, extractSearchDelta(entry));
|
|
144045
|
+
});
|
|
143675
144046
|
} catch (err) {
|
|
143676
144047
|
log15.warn({ filePath, err }, "refreshFile: parseMeta threw");
|
|
143677
144048
|
meta3 = null;
|
|
@@ -143688,6 +144059,7 @@ var ConversationScanner = class {
|
|
|
143688
144059
|
this.metadataCache.delete(previous.id);
|
|
143689
144060
|
this.removeFromSessionIndex(previous);
|
|
143690
144061
|
this.indexer.removeDocument(previous.id);
|
|
144062
|
+
this.searchContents.delete(previous.id);
|
|
143691
144063
|
}
|
|
143692
144064
|
log15.debug({ filePath }, "refreshFile: dropped (no parseable messages)");
|
|
143693
144065
|
return null;
|
|
@@ -143697,10 +144069,12 @@ var ConversationScanner = class {
|
|
|
143697
144069
|
this.metadataCache.set(meta3.id, meta3);
|
|
143698
144070
|
this.addToSessionIndex(meta3);
|
|
143699
144071
|
this.projects.add(meta3.projectPath);
|
|
144072
|
+
const searchContent = capForMemory(searchDoc, this.lastTier.snippetMax);
|
|
144073
|
+
this.searchContents.set(meta3.id, searchContent);
|
|
143700
144074
|
if (previous) {
|
|
143701
|
-
this.indexer.updateDocument(meta3);
|
|
144075
|
+
this.indexer.updateDocument(meta3, searchContent);
|
|
143702
144076
|
} else {
|
|
143703
|
-
this.indexer.addDocument(meta3);
|
|
144077
|
+
this.indexer.addDocument(meta3, searchContent);
|
|
143704
144078
|
}
|
|
143705
144079
|
log15.debug(
|
|
143706
144080
|
{ filePath, messageCount: meta3.messageCount },
|
|
@@ -145550,7 +145924,7 @@ function paginate(results, offset, limit) {
|
|
|
145550
145924
|
}
|
|
145551
145925
|
|
|
145552
145926
|
// src/utils/codexConversationLine.ts
|
|
145553
|
-
function
|
|
145927
|
+
function extractCodexText3(content) {
|
|
145554
145928
|
if (typeof content === "string") return content.trim();
|
|
145555
145929
|
if (!Array.isArray(content)) return "";
|
|
145556
145930
|
return content.map((item) => {
|
|
@@ -145589,7 +145963,7 @@ function classifyCodexLine(line) {
|
|
|
145589
145963
|
if (role !== "user" && role !== "assistant") {
|
|
145590
145964
|
return { kind: "ignored", reason: `role ${String(role)} is not rendered` };
|
|
145591
145965
|
}
|
|
145592
|
-
const text =
|
|
145966
|
+
const text = extractCodexText3(payload.content);
|
|
145593
145967
|
if (!text) {
|
|
145594
145968
|
return { kind: "ignored", reason: "message has no extractable text" };
|
|
145595
145969
|
}
|
|
@@ -154529,13 +154903,16 @@ var StreamerServer = class {
|
|
|
154529
154903
|
json2(res, 400, { error: "Missing token or clientPublicKey" });
|
|
154530
154904
|
return;
|
|
154531
154905
|
}
|
|
154532
|
-
|
|
154533
|
-
|
|
154534
|
-
|
|
154535
|
-
|
|
154536
|
-
|
|
154537
|
-
|
|
154538
|
-
|
|
154906
|
+
const e2eeEnabled = describeE2eeCapability(this.featureFlags.e2ee).enabled;
|
|
154907
|
+
let e2eeRequest = null;
|
|
154908
|
+
if (e2eeEnabled) {
|
|
154909
|
+
try {
|
|
154910
|
+
e2eeRequest = parseE2eeRequest(body?.e2ee);
|
|
154911
|
+
} catch (err) {
|
|
154912
|
+
const e = err;
|
|
154913
|
+
json2(res, 400, { error: e.message, code: e.code });
|
|
154914
|
+
return;
|
|
154915
|
+
}
|
|
154539
154916
|
}
|
|
154540
154917
|
const precheck = this.pairTokens.wouldConsume(token);
|
|
154541
154918
|
if (!precheck.ok) {
|