@threadbase-sh/streamer 1.61.0 → 1.61.2
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/README.md +8 -0
- package/dist/cli.cjs +604 -134
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +101 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -32
- package/dist/index.d.ts +59 -32
- package/dist/index.js +101 -35
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/cli.cjs
CHANGED
|
@@ -6146,8 +6146,15 @@ var init_logger = __esm({
|
|
|
6146
6146
|
});
|
|
6147
6147
|
|
|
6148
6148
|
// src/feature-flags.ts
|
|
6149
|
+
function isFeatureFlagId(id) {
|
|
6150
|
+
return Object.hasOwn(FEATURE_FLAGS, id);
|
|
6151
|
+
}
|
|
6152
|
+
function getFeatureFlag(id) {
|
|
6153
|
+
return { id, ...FEATURE_FLAGS[id] };
|
|
6154
|
+
}
|
|
6149
6155
|
function findFeatureFlag(id) {
|
|
6150
|
-
|
|
6156
|
+
if (!isFeatureFlagId(id)) return void 0;
|
|
6157
|
+
return getFeatureFlag(id);
|
|
6151
6158
|
}
|
|
6152
6159
|
function parseBooleanEnv(raw2) {
|
|
6153
6160
|
if (raw2 === void 0) return void 0;
|
|
@@ -6169,10 +6176,11 @@ function validateFeatureFlagValues(raw2) {
|
|
|
6169
6176
|
}
|
|
6170
6177
|
if (dropped.length > 0) {
|
|
6171
6178
|
getLogger("feature-flags").warn(
|
|
6172
|
-
`Ignoring unknown or non-boolean feature flags: ${dropped.join(", ")}`,
|
|
6179
|
+
`Ignoring unknown or non-boolean feature flags: ${dropped.join(", ")}. Known ids (FEATURE_FLAGS keys, not env names): ${FEATURE_FLAG_IDS.join(", ")}`,
|
|
6173
6180
|
{
|
|
6174
6181
|
event: "config.feature_flags_dropped",
|
|
6175
|
-
dropped
|
|
6182
|
+
dropped,
|
|
6183
|
+
known: FEATURE_FLAG_IDS
|
|
6176
6184
|
}
|
|
6177
6185
|
);
|
|
6178
6186
|
}
|
|
@@ -6188,7 +6196,7 @@ function parseFeatureFlagArgs(entries) {
|
|
|
6188
6196
|
const def = findFeatureFlag(id);
|
|
6189
6197
|
if (!def) {
|
|
6190
6198
|
errors.push(
|
|
6191
|
-
`Unknown feature flag "${id}". Known flags
|
|
6199
|
+
`Unknown feature flag "${id}". Known flags (FEATURE_FLAGS keys, not env names): ${FEATURE_FLAG_IDS.join(", ")}`
|
|
6192
6200
|
);
|
|
6193
6201
|
continue;
|
|
6194
6202
|
}
|
|
@@ -6204,7 +6212,7 @@ function resolveFeatureFlags(opts) {
|
|
|
6204
6212
|
const env = opts?.env ?? process.env;
|
|
6205
6213
|
const values = {};
|
|
6206
6214
|
const sources = {};
|
|
6207
|
-
for (const def of
|
|
6215
|
+
for (const def of FEATURE_FLAG_LIST) {
|
|
6208
6216
|
const rungs = [
|
|
6209
6217
|
["override", opts?.override?.[def.id]],
|
|
6210
6218
|
["env", parseBooleanEnv(env[def.env])],
|
|
@@ -6218,50 +6226,50 @@ function resolveFeatureFlags(opts) {
|
|
|
6218
6226
|
return { values, sources };
|
|
6219
6227
|
}
|
|
6220
6228
|
function nonDefaultFeatureFlags(values) {
|
|
6221
|
-
return
|
|
6229
|
+
return FEATURE_FLAG_LIST.filter((f2) => values[f2.id] !== f2.default).map((f2) => f2.id);
|
|
6222
6230
|
}
|
|
6223
6231
|
function describeFeatureFlags(resolution) {
|
|
6224
|
-
return
|
|
6232
|
+
return FEATURE_FLAG_LIST.map(
|
|
6225
6233
|
(f2) => `${f2.id}=${resolution.values[f2.id]}(${resolution.sources[f2.id]})`
|
|
6226
6234
|
).join(" ");
|
|
6227
6235
|
}
|
|
6228
|
-
var FEATURE_FLAGS;
|
|
6236
|
+
var FEATURE_FLAGS, FEATURE_FLAG_IDS, FEATURE_FLAG_LIST;
|
|
6229
6237
|
var init_feature_flags = __esm({
|
|
6230
6238
|
"src/feature-flags.ts"() {
|
|
6231
6239
|
"use strict";
|
|
6232
6240
|
init_logger();
|
|
6233
|
-
FEATURE_FLAGS =
|
|
6234
|
-
{
|
|
6235
|
-
id: "codexSystemPrompt",
|
|
6241
|
+
FEATURE_FLAGS = {
|
|
6242
|
+
codexSystemPrompt: {
|
|
6236
6243
|
description: "Send the built system prompt to fresh Codex sessions. Off by default: Codex has no --system-prompt flag, so the prompt goes in the positional [PROMPT] argument, which Codex treats as the user's opening turn rather than a system-level instruction.",
|
|
6237
6244
|
default: false,
|
|
6238
6245
|
env: "THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT"
|
|
6239
6246
|
},
|
|
6240
|
-
{
|
|
6241
|
-
id: "sessionRehydration",
|
|
6247
|
+
sessionRehydration: {
|
|
6242
6248
|
description: "Seed the session list at boot with sessions a previous streamer run left behind, so a restart leaves them one tap from resuming instead of silently gone. On by default, with a kill switch: it changes what GET /api/sessions contains.",
|
|
6243
6249
|
default: true,
|
|
6244
6250
|
env: "THREADBASE_FEATURE_SESSION_REHYDRATION"
|
|
6245
6251
|
},
|
|
6246
|
-
{
|
|
6247
|
-
id: "liveActivityPush",
|
|
6252
|
+
liveActivityPush: {
|
|
6248
6253
|
description: "Drive iOS Live Activity surfaces for running sessions. Off by default: the streamer half needs an APNs p8 and a registered push-to-start token, and without both, mobile falls back to starting the activity locally \u2014 which freezes the moment the app backgrounds and expires silently after ~8h. Mobile reads this flag and skips its local path too, so one switch turns the whole surface off.",
|
|
6249
6254
|
default: false,
|
|
6250
6255
|
env: "THREADBASE_FEATURE_LIVE_ACTIVITY_PUSH"
|
|
6251
6256
|
},
|
|
6252
|
-
{
|
|
6253
|
-
id: "e2ee",
|
|
6257
|
+
e2ee: {
|
|
6254
6258
|
description: "Application-layer encryption between a paired device and this server, independent of TLS, so a tunnel or a LAN observer on the path carries ciphertext. Off by default: it is negotiated per device, never forced, because released mobile builds cannot be force-updated and a server that demanded it would break every one of them.",
|
|
6255
6259
|
default: false,
|
|
6256
6260
|
env: "THREADBASE_FEATURE_E2EE"
|
|
6257
6261
|
},
|
|
6258
|
-
{
|
|
6259
|
-
id: "ptyHost",
|
|
6262
|
+
ptyHost: {
|
|
6260
6263
|
description: "Keep live PTYs in a separate host process so a streamer restart can reconnect without restarting the agents. Off by default until cross-platform behavior is qualified.",
|
|
6261
6264
|
default: false,
|
|
6262
6265
|
env: "THREADBASE_FEATURE_PTY_HOST"
|
|
6263
6266
|
}
|
|
6264
|
-
|
|
6267
|
+
};
|
|
6268
|
+
FEATURE_FLAG_IDS = Object.keys(FEATURE_FLAGS);
|
|
6269
|
+
FEATURE_FLAG_LIST = FEATURE_FLAG_IDS.map((id) => ({
|
|
6270
|
+
id,
|
|
6271
|
+
...FEATURE_FLAGS[id]
|
|
6272
|
+
}));
|
|
6265
6273
|
}
|
|
6266
6274
|
});
|
|
6267
6275
|
|
|
@@ -140828,18 +140836,43 @@ function readGitBranch(projectPath) {
|
|
|
140828
140836
|
log15.trace({ projectPath }, "git: no .git found within depth");
|
|
140829
140837
|
return null;
|
|
140830
140838
|
}
|
|
140839
|
+
var FTS_HIT_OPEN = "";
|
|
140840
|
+
var FTS_HIT_CLOSE = "";
|
|
140841
|
+
var FTS_SNIPPET_TOKENS = 16;
|
|
140842
|
+
var FTS_ELLIPSIS = "\u2026";
|
|
140843
|
+
var CONTENT_FIELD = "content";
|
|
140844
|
+
function parseFtsSnippet(raw2) {
|
|
140845
|
+
if (!raw2?.includes(FTS_HIT_OPEN)) return null;
|
|
140846
|
+
const highlights = [];
|
|
140847
|
+
let snippet = "";
|
|
140848
|
+
let openAt = -1;
|
|
140849
|
+
for (const ch of raw2) {
|
|
140850
|
+
if (ch === FTS_HIT_OPEN) {
|
|
140851
|
+
openAt = snippet.length;
|
|
140852
|
+
continue;
|
|
140853
|
+
}
|
|
140854
|
+
if (ch === FTS_HIT_CLOSE) {
|
|
140855
|
+
if (openAt >= 0 && snippet.length > openAt) {
|
|
140856
|
+
highlights.push({ start: openAt, end: snippet.length });
|
|
140857
|
+
}
|
|
140858
|
+
openAt = -1;
|
|
140859
|
+
continue;
|
|
140860
|
+
}
|
|
140861
|
+
snippet += ch;
|
|
140862
|
+
}
|
|
140863
|
+
return highlights.length > 0 ? { snippet, highlights } : null;
|
|
140864
|
+
}
|
|
140831
140865
|
function generateMatches(meta3, query) {
|
|
140832
140866
|
const matches = [];
|
|
140833
140867
|
const lowerQuery = query.toLowerCase();
|
|
140834
140868
|
const fields = [
|
|
140835
|
-
["contentSnippet", meta3.contentSnippet],
|
|
140836
|
-
["projectName", meta3.projectName],
|
|
140837
|
-
["sessionId", meta3.sessionId],
|
|
140838
140869
|
["sessionName", meta3.sessionName],
|
|
140839
|
-
["
|
|
140840
|
-
["model", meta3.model || ""],
|
|
140870
|
+
["projectName", meta3.projectName],
|
|
140841
140871
|
["gitBranch", meta3.gitBranch || ""],
|
|
140842
|
-
["toolNames", meta3.toolNames.join(" ")]
|
|
140872
|
+
["toolNames", meta3.toolNames.join(" ")],
|
|
140873
|
+
["model", meta3.model || ""],
|
|
140874
|
+
["account", meta3.account],
|
|
140875
|
+
["sessionId", meta3.sessionId]
|
|
140843
140876
|
];
|
|
140844
140877
|
for (const [field, value] of fields) {
|
|
140845
140878
|
const idx = value.toLowerCase().indexOf(lowerQuery);
|
|
@@ -140854,11 +140887,29 @@ function generateMatches(meta3, query) {
|
|
|
140854
140887
|
}
|
|
140855
140888
|
return matches.length > 0 ? matches : [{ field: "preview", snippet: meta3.preview }];
|
|
140856
140889
|
}
|
|
140890
|
+
function buildContentMatch(searchContent, query) {
|
|
140891
|
+
if (!searchContent || !query.trim()) return null;
|
|
140892
|
+
const idx = searchContent.toLowerCase().indexOf(query.toLowerCase());
|
|
140893
|
+
if (idx === -1) return null;
|
|
140894
|
+
const start = Math.max(0, idx - 80);
|
|
140895
|
+
const end = Math.min(searchContent.length, idx + query.length + 120);
|
|
140896
|
+
const body = searchContent.slice(start, end).replace(/\s+/g, " ").trim();
|
|
140897
|
+
const hitAt = body.toLowerCase().indexOf(query.toLowerCase());
|
|
140898
|
+
const prefix = start > 0 ? FTS_ELLIPSIS : "";
|
|
140899
|
+
const suffix = end < searchContent.length ? FTS_ELLIPSIS : "";
|
|
140900
|
+
const snippet = `${prefix}${body}${suffix}`;
|
|
140901
|
+
const highlights = hitAt === -1 ? [] : [{ start: hitAt + prefix.length, end: hitAt + prefix.length + query.length }];
|
|
140902
|
+
return { field: CONTENT_FIELD, snippet, highlights };
|
|
140903
|
+
}
|
|
140857
140904
|
var FlexSearch = flexsearch_bundle_module_min_default.default ?? flexsearch_bundle_module_min_default;
|
|
140858
140905
|
var SearchIndexer = class {
|
|
140859
140906
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
140860
140907
|
index;
|
|
140861
140908
|
documents = /* @__PURE__ */ new Map();
|
|
140909
|
+
// The indexed body per conversation, kept so a hit can produce a real excerpt
|
|
140910
|
+
// instead of falling back to an unrelated preview. Sized by the caller (the
|
|
140911
|
+
// scanner tail-caps it to the content tier) — see the note on addDocument.
|
|
140912
|
+
searchContents = /* @__PURE__ */ new Map();
|
|
140862
140913
|
constructor() {
|
|
140863
140914
|
this.index = this.createIndex();
|
|
140864
140915
|
}
|
|
@@ -140884,25 +140935,25 @@ var SearchIndexer = class {
|
|
|
140884
140935
|
cache: 100
|
|
140885
140936
|
});
|
|
140886
140937
|
}
|
|
140887
|
-
|
|
140938
|
+
// `searchContent` is the combined search document (text + thinking + tools),
|
|
140939
|
+
// NOT meta.contentSnippet.
|
|
140940
|
+
//
|
|
140941
|
+
// It arrives pre-capped. This index is `tokenize: "forward"` at resolution 9,
|
|
140942
|
+
// which stores every prefix of every token, so it is a resident-memory
|
|
140943
|
+
// structure whose cost is very different from the on-disk FTS index. Feeding
|
|
140944
|
+
// it the full ~128 KB budget across a few hundred conversations would be a
|
|
140945
|
+
// multi-hundred-MB-to-GB index. The scanner therefore caps this to the active
|
|
140946
|
+
// tier's snippetMax and accepts that `persistent: false` has lower recall than
|
|
140947
|
+
// SQLite — an honest, documented divergence rather than a silent one.
|
|
140948
|
+
addDocument(meta3, searchContent = "") {
|
|
140888
140949
|
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
|
-
});
|
|
140950
|
+
this.searchContents.set(meta3.id, searchContent);
|
|
140951
|
+
this.index.add(toIndexDoc(meta3, searchContent));
|
|
140901
140952
|
}
|
|
140902
|
-
buildIndex(metas) {
|
|
140953
|
+
buildIndex(metas, searchContents) {
|
|
140903
140954
|
this.clear();
|
|
140904
140955
|
for (const meta3 of metas) {
|
|
140905
|
-
this.addDocument(meta3);
|
|
140956
|
+
this.addDocument(meta3, searchContents?.get(meta3.id) ?? "");
|
|
140906
140957
|
}
|
|
140907
140958
|
getLogger2().debug({ docCount: metas.length }, "indexer: built");
|
|
140908
140959
|
}
|
|
@@ -140922,14 +140973,26 @@ var SearchIndexer = class {
|
|
|
140922
140973
|
seen.add(id);
|
|
140923
140974
|
const meta3 = this.documents.get(id);
|
|
140924
140975
|
if (!meta3) continue;
|
|
140925
|
-
|
|
140926
|
-
|
|
140976
|
+
searchResults.push({
|
|
140977
|
+
meta: meta3,
|
|
140978
|
+
score: 1,
|
|
140979
|
+
matches: this.matchesFor(meta3, query)
|
|
140980
|
+
});
|
|
140927
140981
|
if (searchResults.length >= limit) break;
|
|
140928
140982
|
}
|
|
140929
140983
|
if (searchResults.length >= limit) break;
|
|
140930
140984
|
}
|
|
140931
140985
|
return searchResults;
|
|
140932
140986
|
}
|
|
140987
|
+
// Body context first (that is what explains why the result appeared), then any
|
|
140988
|
+
// metadata matches. Only when neither hits does generateMatches' preview
|
|
140989
|
+
// fallback stand in.
|
|
140990
|
+
matchesFor(meta3, query) {
|
|
140991
|
+
const contentMatch = buildContentMatch(this.searchContents.get(meta3.id) ?? "", query);
|
|
140992
|
+
const metaMatches = generateMatches(meta3, query);
|
|
140993
|
+
if (!contentMatch) return metaMatches;
|
|
140994
|
+
return [contentMatch, ...metaMatches.filter((m2) => m2.field !== "preview")];
|
|
140995
|
+
}
|
|
140933
140996
|
getRecent(limit) {
|
|
140934
140997
|
return Array.from(this.documents.values()).sort((a, b2) => b2.timestamp.localeCompare(a.timestamp)).slice(0, limit).map((meta3) => ({
|
|
140935
140998
|
meta: meta3,
|
|
@@ -140943,31 +141006,37 @@ var SearchIndexer = class {
|
|
|
140943
141006
|
// Replace an already-indexed document in place. FlexSearch's `add` does not
|
|
140944
141007
|
// overwrite an existing id, so a single-file refresh must go through
|
|
140945
141008
|
// `update` to avoid stale matches lingering in the index.
|
|
140946
|
-
updateDocument(meta3) {
|
|
141009
|
+
updateDocument(meta3, searchContent = "") {
|
|
140947
141010
|
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
|
-
});
|
|
141011
|
+
this.searchContents.set(meta3.id, searchContent);
|
|
141012
|
+
this.index.update(toIndexDoc(meta3, searchContent));
|
|
140960
141013
|
}
|
|
140961
141014
|
removeDocument(id) {
|
|
140962
141015
|
this.documents.delete(id);
|
|
141016
|
+
this.searchContents.delete(id);
|
|
140963
141017
|
this.index.remove(id);
|
|
140964
141018
|
}
|
|
140965
141019
|
clear() {
|
|
140966
141020
|
this.documents.clear();
|
|
141021
|
+
this.searchContents.clear();
|
|
140967
141022
|
this.index = this.createIndex();
|
|
140968
141023
|
getLogger2().trace("indexer: cleared");
|
|
140969
141024
|
}
|
|
140970
141025
|
};
|
|
141026
|
+
function toIndexDoc(meta3, searchContent) {
|
|
141027
|
+
return {
|
|
141028
|
+
id: meta3.id,
|
|
141029
|
+
content: searchContent,
|
|
141030
|
+
projectName: meta3.projectName,
|
|
141031
|
+
projectPath: meta3.projectPath,
|
|
141032
|
+
sessionId: meta3.sessionId,
|
|
141033
|
+
sessionName: meta3.sessionName,
|
|
141034
|
+
account: meta3.account,
|
|
141035
|
+
model: meta3.model || "",
|
|
141036
|
+
gitBranch: meta3.gitBranch || "",
|
|
141037
|
+
toolNames: meta3.toolNames.join(" ")
|
|
141038
|
+
};
|
|
141039
|
+
}
|
|
140971
141040
|
var CLAUDE_CODE_PROVIDER2 = "claude-code";
|
|
140972
141041
|
var CODEX_CLI_PROVIDER2 = "codex-cli";
|
|
140973
141042
|
function initialReducerState() {
|
|
@@ -141117,7 +141186,7 @@ var SYSTEM_TAG_RE = new RegExp(`<(${SYSTEM_TAGS.join("|")})[^>]*>[\\s\\S]*?<\\/\
|
|
|
141117
141186
|
function cleanSystemTags(text) {
|
|
141118
141187
|
return text.replace(SYSTEM_TAG_RE, "").replace(/[^\S\n]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
141119
141188
|
}
|
|
141120
|
-
async function parseMeta(filePath, account, tier) {
|
|
141189
|
+
async function parseMeta(filePath, account, tier, onEntry) {
|
|
141121
141190
|
const log15 = getLogger2();
|
|
141122
141191
|
log15.trace({ filePath, account, tier: tier.name }, "parseMeta: start");
|
|
141123
141192
|
const state = initialReducerState();
|
|
@@ -141134,6 +141203,7 @@ async function parseMeta(filePath, account, tier) {
|
|
|
141134
141203
|
continue;
|
|
141135
141204
|
}
|
|
141136
141205
|
reduceLine(state, entry, tier);
|
|
141206
|
+
onEntry?.(entry);
|
|
141137
141207
|
}
|
|
141138
141208
|
} catch (err) {
|
|
141139
141209
|
log15.warn({ filePath, err }, "parseMeta: read failed");
|
|
@@ -141828,7 +141898,7 @@ var LRUCache = class {
|
|
|
141828
141898
|
}
|
|
141829
141899
|
};
|
|
141830
141900
|
var YIELD_EVERY_LINES = 500;
|
|
141831
|
-
async function tailReduce(filePath, startOffset, startLine, state, tier) {
|
|
141901
|
+
async function tailReduce(filePath, startOffset, startLine, state, tier, onEntry) {
|
|
141832
141902
|
const stream = (0, import_fs11.createReadStream)(filePath, { start: startOffset, encoding: "utf8" });
|
|
141833
141903
|
let buffer = "";
|
|
141834
141904
|
let offset = startOffset;
|
|
@@ -141844,7 +141914,9 @@ async function tailReduce(filePath, startOffset, startLine, state, tier) {
|
|
|
141844
141914
|
buffer = buffer.slice(nl + 1);
|
|
141845
141915
|
if (text.length > 0) {
|
|
141846
141916
|
try {
|
|
141847
|
-
|
|
141917
|
+
const entry = JSON.parse(text);
|
|
141918
|
+
reduceLine(state, entry, tier);
|
|
141919
|
+
onEntry?.(entry);
|
|
141848
141920
|
} catch {
|
|
141849
141921
|
state.badJsonLines++;
|
|
141850
141922
|
}
|
|
@@ -142065,7 +142137,7 @@ function classify(filePath, existing) {
|
|
|
142065
142137
|
}
|
|
142066
142138
|
return { change: "reindex", stat: stat42 };
|
|
142067
142139
|
}
|
|
142068
|
-
async function parseMetaWithProvider(provider, filePath, account, tier) {
|
|
142140
|
+
async function parseMetaWithProvider(provider, filePath, account, tier, onEntry) {
|
|
142069
142141
|
const log15 = getLogger2();
|
|
142070
142142
|
const acc = provider.createEmptyAccumulator();
|
|
142071
142143
|
const rl = (0, import_readline3.createInterface)({
|
|
@@ -142083,6 +142155,7 @@ async function parseMetaWithProvider(provider, filePath, account, tier) {
|
|
|
142083
142155
|
}
|
|
142084
142156
|
try {
|
|
142085
142157
|
provider.reduceEntry(acc, entry, tier);
|
|
142158
|
+
onEntry?.(entry);
|
|
142086
142159
|
} catch (err) {
|
|
142087
142160
|
log15.warn({ filePath, provider: provider.name, err }, "provider reduce threw; line skipped");
|
|
142088
142161
|
}
|
|
@@ -142093,6 +142166,149 @@ async function parseMetaWithProvider(provider, filePath, account, tier) {
|
|
|
142093
142166
|
}
|
|
142094
142167
|
return provider.finalize(acc, filePath, account, tier);
|
|
142095
142168
|
}
|
|
142169
|
+
var SEARCH_BUDGET = {
|
|
142170
|
+
textMax: 64 * 1024,
|
|
142171
|
+
thinkingMax: 32 * 1024,
|
|
142172
|
+
toolsMax: 32 * 1024,
|
|
142173
|
+
toolPayloadMax: 4 * 1024
|
|
142174
|
+
};
|
|
142175
|
+
var SEP = "\n\n";
|
|
142176
|
+
function emptySearchDocument() {
|
|
142177
|
+
return { text: "", thinking: "", tools: "" };
|
|
142178
|
+
}
|
|
142179
|
+
function appendSearchDelta(doc, delta) {
|
|
142180
|
+
return {
|
|
142181
|
+
text: tailAppend(doc.text, delta.text, SEARCH_BUDGET.textMax),
|
|
142182
|
+
thinking: tailAppend(doc.thinking, delta.thinking, SEARCH_BUDGET.thinkingMax),
|
|
142183
|
+
tools: tailAppend(doc.tools, delta.tools, SEARCH_BUDGET.toolsMax)
|
|
142184
|
+
};
|
|
142185
|
+
}
|
|
142186
|
+
function tailAppend(current, incoming, max) {
|
|
142187
|
+
if (!incoming) return current;
|
|
142188
|
+
const joined = current ? current + SEP + incoming : incoming;
|
|
142189
|
+
return joined.length <= max ? joined : joined.slice(-max);
|
|
142190
|
+
}
|
|
142191
|
+
function combineSearchContent(doc) {
|
|
142192
|
+
return [doc.text, doc.thinking, doc.tools].filter(Boolean).join(SEP);
|
|
142193
|
+
}
|
|
142194
|
+
function capToolPayload(value) {
|
|
142195
|
+
const raw2 = stringifyPayload(value);
|
|
142196
|
+
if (!raw2) return "";
|
|
142197
|
+
return raw2.length > SEARCH_BUDGET.toolPayloadMax ? raw2.slice(0, SEARCH_BUDGET.toolPayloadMax) : raw2;
|
|
142198
|
+
}
|
|
142199
|
+
function stringifyPayload(value) {
|
|
142200
|
+
if (value === null || value === void 0) return "";
|
|
142201
|
+
if (typeof value === "string") return value;
|
|
142202
|
+
try {
|
|
142203
|
+
return JSON.stringify(value) ?? "";
|
|
142204
|
+
} catch {
|
|
142205
|
+
return "";
|
|
142206
|
+
}
|
|
142207
|
+
}
|
|
142208
|
+
function extractSearchDelta(entry) {
|
|
142209
|
+
if (entry.type === "response_item" || entry.type === "session_meta") {
|
|
142210
|
+
return extractCodexDelta(entry);
|
|
142211
|
+
}
|
|
142212
|
+
return extractClaudeDelta(entry);
|
|
142213
|
+
}
|
|
142214
|
+
function extractClaudeDelta(entry) {
|
|
142215
|
+
const type = entry.type;
|
|
142216
|
+
if (type !== "user" && type !== "assistant") return emptySearchDocument();
|
|
142217
|
+
if (entry.isMeta) return emptySearchDocument();
|
|
142218
|
+
const msg = entry.message;
|
|
142219
|
+
const content = msg?.content;
|
|
142220
|
+
const tools = [
|
|
142221
|
+
extractClaudeToolContent(content),
|
|
142222
|
+
// Claude stores the rich/structured tool result at the JSONL entry's top
|
|
142223
|
+
// level, not inside message.content — indexing only message.content would
|
|
142224
|
+
// miss most real tool output (file reads, command stdout).
|
|
142225
|
+
capToolPayload(entry.toolUseResult)
|
|
142226
|
+
].filter(Boolean).join(SEP);
|
|
142227
|
+
return {
|
|
142228
|
+
text: extractClaudeText(content),
|
|
142229
|
+
thinking: type === "assistant" ? extractThinking(content).content : "",
|
|
142230
|
+
tools
|
|
142231
|
+
};
|
|
142232
|
+
}
|
|
142233
|
+
function extractClaudeText(content) {
|
|
142234
|
+
if (typeof content === "string") return cleanSystemTags(content);
|
|
142235
|
+
if (!Array.isArray(content)) return "";
|
|
142236
|
+
const parts = [];
|
|
142237
|
+
for (const item of content) {
|
|
142238
|
+
if (typeof item === "string") {
|
|
142239
|
+
const cleaned = cleanSystemTags(item);
|
|
142240
|
+
if (cleaned) parts.push(cleaned);
|
|
142241
|
+
} else if (item?.type === "text" && typeof item.text === "string") {
|
|
142242
|
+
const cleaned = cleanSystemTags(item.text);
|
|
142243
|
+
if (cleaned) parts.push(cleaned);
|
|
142244
|
+
}
|
|
142245
|
+
}
|
|
142246
|
+
return parts.join(SEP);
|
|
142247
|
+
}
|
|
142248
|
+
function extractClaudeToolContent(content) {
|
|
142249
|
+
if (!Array.isArray(content)) return "";
|
|
142250
|
+
const parts = [];
|
|
142251
|
+
for (const item of content) {
|
|
142252
|
+
if (item?.type === "tool_use") {
|
|
142253
|
+
const capped = capToolPayload(item.input);
|
|
142254
|
+
if (capped) parts.push(capped);
|
|
142255
|
+
} else if (item?.type === "tool_result") {
|
|
142256
|
+
const capped = capToolPayload(item.content);
|
|
142257
|
+
if (capped) parts.push(capped);
|
|
142258
|
+
}
|
|
142259
|
+
}
|
|
142260
|
+
return parts.join(SEP);
|
|
142261
|
+
}
|
|
142262
|
+
function extractCodexDelta(entry) {
|
|
142263
|
+
const payload = entry.payload;
|
|
142264
|
+
if (!payload || typeof payload !== "object") return emptySearchDocument();
|
|
142265
|
+
const ptype = payload.type;
|
|
142266
|
+
if (ptype === "function_call" || ptype === "custom_tool_call") {
|
|
142267
|
+
return { text: "", thinking: "", tools: capToolPayload(payload.arguments) };
|
|
142268
|
+
}
|
|
142269
|
+
if (ptype === "function_call_output" || ptype === "custom_tool_call_output") {
|
|
142270
|
+
return { text: "", thinking: "", tools: capToolPayload(payload.output) };
|
|
142271
|
+
}
|
|
142272
|
+
if (ptype === "reasoning") {
|
|
142273
|
+
return { text: "", thinking: extractCodexReasoning(payload), tools: "" };
|
|
142274
|
+
}
|
|
142275
|
+
if (ptype === "message") {
|
|
142276
|
+
const role = payload.role;
|
|
142277
|
+
if (role !== "user" && role !== "assistant") return emptySearchDocument();
|
|
142278
|
+
return { text: extractCodexText2(payload.content), thinking: "", tools: "" };
|
|
142279
|
+
}
|
|
142280
|
+
return emptySearchDocument();
|
|
142281
|
+
}
|
|
142282
|
+
function extractCodexReasoning(payload) {
|
|
142283
|
+
const parts = [];
|
|
142284
|
+
for (const key of ["summary", "content"]) {
|
|
142285
|
+
const blocks = payload[key];
|
|
142286
|
+
if (!Array.isArray(blocks)) continue;
|
|
142287
|
+
for (const block of blocks) {
|
|
142288
|
+
if (typeof block === "string") parts.push(block);
|
|
142289
|
+
else if (typeof block?.text === "string") parts.push(block.text);
|
|
142290
|
+
}
|
|
142291
|
+
}
|
|
142292
|
+
return parts.filter(Boolean).join(SEP);
|
|
142293
|
+
}
|
|
142294
|
+
function extractCodexText2(content) {
|
|
142295
|
+
if (typeof content === "string") return cleanSystemTags(content);
|
|
142296
|
+
if (!Array.isArray(content)) return "";
|
|
142297
|
+
const parts = [];
|
|
142298
|
+
for (const item of content) {
|
|
142299
|
+
if (typeof item === "string") {
|
|
142300
|
+
const cleaned = cleanSystemTags(item);
|
|
142301
|
+
if (cleaned) parts.push(cleaned);
|
|
142302
|
+
continue;
|
|
142303
|
+
}
|
|
142304
|
+
const t = item?.type;
|
|
142305
|
+
if ((t === "input_text" || t === "output_text" || t === "text") && typeof item.text === "string") {
|
|
142306
|
+
const cleaned = cleanSystemTags(item.text);
|
|
142307
|
+
if (cleaned) parts.push(cleaned);
|
|
142308
|
+
}
|
|
142309
|
+
}
|
|
142310
|
+
return parts.join(SEP);
|
|
142311
|
+
}
|
|
142096
142312
|
var DEFAULT_TIERS = {
|
|
142097
142313
|
standard: { name: "standard", previewMax: 200, snippetMax: 5e3 },
|
|
142098
142314
|
full: { name: "full", previewMax: 1200, snippetMax: 5e4 }
|
|
@@ -142106,7 +142322,7 @@ function resolveTier(tierName, customTiers) {
|
|
|
142106
142322
|
}
|
|
142107
142323
|
return tier;
|
|
142108
142324
|
}
|
|
142109
|
-
var SCHEMA_VERSION =
|
|
142325
|
+
var SCHEMA_VERSION = 5;
|
|
142110
142326
|
var SCHEMA_SQL = `
|
|
142111
142327
|
CREATE TABLE IF NOT EXISTS conversation_files (
|
|
142112
142328
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -142206,13 +142422,27 @@ CREATE INDEX IF NOT EXISTS idx_conversations_account_recent ON conversations(acc
|
|
|
142206
142422
|
CREATE INDEX IF NOT EXISTS idx_conversations_subagent_recent ON conversations(is_subagent, timestamp DESC);
|
|
142207
142423
|
CREATE INDEX IF NOT EXISTS idx_conversations_team_recent ON conversations(team_name, timestamp DESC);
|
|
142208
142424
|
|
|
142209
|
-
-- Full-text search index over conversation
|
|
142210
|
-
--
|
|
142211
|
-
--
|
|
142212
|
-
--
|
|
142425
|
+
-- Full-text search index over conversation body + metadata. Kept separate from
|
|
142426
|
+
-- the metadata tables so list-screen queries stay small and fast. One FTS row
|
|
142427
|
+
-- per conversation, replaced on each upsert.
|
|
142428
|
+
--
|
|
142429
|
+
-- The row's rowid IS conversation_files.id. That is load-bearing, not cosmetic:
|
|
142430
|
+
-- FTS5's query planner only handles MATCH, rowid and rank, so any other
|
|
142431
|
+
-- constraint (including a source_path equality on an UNINDEXED column) has no
|
|
142432
|
+
-- index and linear-scans the whole table. At ~128 KB of body per row that would
|
|
142433
|
+
-- mean scanning the entire corpus on every append. Look rows up by rowid.
|
|
142434
|
+
-- source_path stays stored-but-UNINDEXED so a search hit can resolve back to a
|
|
142435
|
+
-- conversations row.
|
|
142436
|
+
--
|
|
142437
|
+
-- Body is three columns, not one: text > thinking > tools priority has to
|
|
142438
|
+
-- survive an append (a new user message belongs in the text column, not after
|
|
142439
|
+
-- the tool output already written). They are deliberately NOT concatenated into
|
|
142440
|
+
-- a fourth column - that would double-weight body hits and inflate bm25 length.
|
|
142213
142441
|
CREATE VIRTUAL TABLE IF NOT EXISTS conversation_messages_fts USING fts5(
|
|
142214
142442
|
source_path UNINDEXED,
|
|
142215
|
-
|
|
142443
|
+
text,
|
|
142444
|
+
thinking,
|
|
142445
|
+
tools,
|
|
142216
142446
|
project_name,
|
|
142217
142447
|
session_id,
|
|
142218
142448
|
session_name,
|
|
@@ -142280,6 +142510,24 @@ function runMigrations(db) {
|
|
|
142280
142510
|
if (current >= 1 && current < 3 && tableExists(db, "conversations")) {
|
|
142281
142511
|
db.exec("UPDATE conversations SET provider = 'claude-code' WHERE provider = 'threadbase'");
|
|
142282
142512
|
}
|
|
142513
|
+
if (current >= 1 && current < 5) {
|
|
142514
|
+
db.exec("DROP TABLE IF EXISTS conversation_messages_fts");
|
|
142515
|
+
if (tableExists(db, "conversation_files")) {
|
|
142516
|
+
const assignments = [];
|
|
142517
|
+
if (hasColumn(db, "conversation_files", "last_indexed_offset")) {
|
|
142518
|
+
assignments.push("last_indexed_offset = 0");
|
|
142519
|
+
}
|
|
142520
|
+
if (hasColumn(db, "conversation_files", "last_indexed_line")) {
|
|
142521
|
+
assignments.push("last_indexed_line = 0");
|
|
142522
|
+
}
|
|
142523
|
+
if (hasColumn(db, "conversation_files", "reducer_state")) {
|
|
142524
|
+
assignments.push("reducer_state = NULL");
|
|
142525
|
+
}
|
|
142526
|
+
if (assignments.length > 0) {
|
|
142527
|
+
db.exec(`UPDATE conversation_files SET ${assignments.join(", ")}`);
|
|
142528
|
+
}
|
|
142529
|
+
}
|
|
142530
|
+
}
|
|
142283
142531
|
db.exec(SCHEMA_SQL);
|
|
142284
142532
|
db.pragma(`user_version = ${SCHEMA_VERSION}`);
|
|
142285
142533
|
}
|
|
@@ -142294,15 +142542,46 @@ function openDatabase(dbPath) {
|
|
|
142294
142542
|
if (dbPath !== ":memory:") {
|
|
142295
142543
|
(0, import_fs15.mkdirSync)((0, import_path12.dirname)(dbPath), { recursive: true });
|
|
142296
142544
|
}
|
|
142297
|
-
|
|
142545
|
+
let db;
|
|
142546
|
+
try {
|
|
142547
|
+
db = new import_better_sqlite3.default(dbPath);
|
|
142548
|
+
} catch (err) {
|
|
142549
|
+
if (isNativeBindingFailure(err)) throw nativeBindingError(err, dbPath);
|
|
142550
|
+
throw err;
|
|
142551
|
+
}
|
|
142298
142552
|
db.pragma("journal_mode = WAL");
|
|
142299
142553
|
db.pragma("synchronous = NORMAL");
|
|
142300
142554
|
db.pragma("temp_store = MEMORY");
|
|
142301
142555
|
db.pragma("foreign_keys = ON");
|
|
142556
|
+
db.pragma("busy_timeout = 5000");
|
|
142302
142557
|
runMigrations(db);
|
|
142303
142558
|
getLogger2().debug({ dbPath }, "db: opened");
|
|
142304
142559
|
return db;
|
|
142305
142560
|
}
|
|
142561
|
+
function isNativeBindingFailure(err) {
|
|
142562
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
142563
|
+
return /could not locate the bindings file|NODE_MODULE_VERSION|was compiled against|\.node['"\s]/i.test(
|
|
142564
|
+
message
|
|
142565
|
+
);
|
|
142566
|
+
}
|
|
142567
|
+
function nativeBindingError(err, dbPath) {
|
|
142568
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
142569
|
+
return new Error(
|
|
142570
|
+
[
|
|
142571
|
+
`Could not load better-sqlite3's native binary, so the persistent index at ${dbPath} cannot be opened.`,
|
|
142572
|
+
"This usually means the binary was never built or downloaded \u2014 not that your Node version is wrong.",
|
|
142573
|
+
"",
|
|
142574
|
+
"Try, in order:",
|
|
142575
|
+
" 1. npm rebuild better-sqlite3",
|
|
142576
|
+
" 2. rm -rf node_modules && npm install",
|
|
142577
|
+
" (npm 12 blocks package install scripts by default; approve better-sqlite3 if asked)",
|
|
142578
|
+
" 3. Run without SQLite entirely: new ConversationScanner({ persistent: false })",
|
|
142579
|
+
" or pass --no-persist on the CLI. Search falls back to the in-memory index.",
|
|
142580
|
+
"",
|
|
142581
|
+
`Original error: ${detail}`
|
|
142582
|
+
].join("\n")
|
|
142583
|
+
);
|
|
142584
|
+
}
|
|
142306
142585
|
var FULL_RECONCILE_EVERY_N_SCANS = 20;
|
|
142307
142586
|
async function discoverJsonlFilesGated(dirs, files, scannedDirs, options = {}) {
|
|
142308
142587
|
const log15 = getLogger2();
|
|
@@ -142733,22 +143012,31 @@ var ConversationsRepo = class {
|
|
|
142733
143012
|
return this.db.prepare("SELECT COUNT(*) AS n FROM conversations WHERE status = 'active'").get().n;
|
|
142734
143013
|
}
|
|
142735
143014
|
};
|
|
143015
|
+
var BODY_COLUMNS = [
|
|
143016
|
+
{ name: "text", index: 1 },
|
|
143017
|
+
{ name: "thinking", index: 2 },
|
|
143018
|
+
{ name: "tools", index: 3 }
|
|
143019
|
+
];
|
|
142736
143020
|
var FtsRepo = class {
|
|
142737
143021
|
constructor(db) {
|
|
142738
143022
|
this.db = db;
|
|
142739
143023
|
}
|
|
142740
143024
|
db;
|
|
142741
|
-
upsert(meta3) {
|
|
143025
|
+
upsert(rowId, meta3, doc) {
|
|
142742
143026
|
const sourcePath = canonicalPath(meta3.id);
|
|
142743
143027
|
const tx = this.db.transaction(() => {
|
|
142744
|
-
this.db.prepare("DELETE FROM conversation_messages_fts WHERE
|
|
143028
|
+
this.db.prepare("DELETE FROM conversation_messages_fts WHERE rowid = ?").run(rowId);
|
|
142745
143029
|
this.db.prepare(
|
|
142746
143030
|
`INSERT INTO conversation_messages_fts
|
|
142747
|
-
(
|
|
142748
|
-
|
|
143031
|
+
(rowid, source_path, text, thinking, tools,
|
|
143032
|
+
project_name, session_id, session_name, account, model, branch, tool_names)
|
|
143033
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
142749
143034
|
).run(
|
|
143035
|
+
rowId,
|
|
142750
143036
|
sourcePath,
|
|
142751
|
-
|
|
143037
|
+
doc.text,
|
|
143038
|
+
doc.thinking,
|
|
143039
|
+
doc.tools,
|
|
142752
143040
|
meta3.projectName ?? "",
|
|
142753
143041
|
meta3.sessionId ?? "",
|
|
142754
143042
|
meta3.sessionName ?? "",
|
|
@@ -142760,26 +143048,94 @@ var FtsRepo = class {
|
|
|
142760
143048
|
});
|
|
142761
143049
|
tx();
|
|
142762
143050
|
}
|
|
142763
|
-
|
|
142764
|
-
|
|
143051
|
+
// Current durable buckets for a conversation, so an append can tail-extend
|
|
143052
|
+
// them without reparsing the file. Returns null when no row exists yet.
|
|
143053
|
+
readDocument(rowId) {
|
|
143054
|
+
const row = this.db.prepare("SELECT text, thinking, tools FROM conversation_messages_fts WHERE rowid = ?").get(rowId);
|
|
143055
|
+
if (!row) return null;
|
|
143056
|
+
return { text: row.text ?? "", thinking: row.thinking ?? "", tools: row.tools ?? "" };
|
|
143057
|
+
}
|
|
143058
|
+
// True when the stored row already equals what we would write, so an append
|
|
143059
|
+
// can skip re-tokenizing ~128 KB of body for nothing.
|
|
143060
|
+
//
|
|
143061
|
+
// The check covers the metadata columns too, not just the buckets: a
|
|
143062
|
+
// newly-seen tool name or a late-resolved session_name changes metadata while
|
|
143063
|
+
// the body is byte-identical, and nothing else ever rewrites this row — so
|
|
143064
|
+
// skipping on "body unchanged" alone would strand that stale value forever.
|
|
143065
|
+
isCurrent(rowId, meta3, doc) {
|
|
143066
|
+
const row = this.db.prepare(
|
|
143067
|
+
`SELECT text, thinking, tools,
|
|
143068
|
+
project_name, session_id, session_name, account, model, branch, tool_names
|
|
143069
|
+
FROM conversation_messages_fts WHERE rowid = ?`
|
|
143070
|
+
).get(rowId);
|
|
143071
|
+
if (!row) return false;
|
|
143072
|
+
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(" ");
|
|
143073
|
+
}
|
|
143074
|
+
remove(rowId) {
|
|
143075
|
+
this.db.prepare("DELETE FROM conversation_messages_fts WHERE rowid = ?").run(rowId);
|
|
142765
143076
|
}
|
|
142766
|
-
// Ranked
|
|
142767
|
-
//
|
|
142768
|
-
|
|
143077
|
+
// Ranked hits matching the query, best first. Filters run in SQL BEFORE LIMIT:
|
|
143078
|
+
// with a wide corpus a popular term matches far more conversations than one
|
|
143079
|
+
// page, so post-filtering an already-truncated list would report "no results"
|
|
143080
|
+
// for queries that do have them.
|
|
143081
|
+
search(query, limit, filters = {}) {
|
|
142769
143082
|
const match2 = toMatchQuery(query);
|
|
142770
143083
|
if (!match2) return [];
|
|
143084
|
+
const snippetSelects = BODY_COLUMNS.map(
|
|
143085
|
+
(c) => `snippet(conversation_messages_fts, ${c.index}, ?, ?, ?, ?) AS snippet_${c.name}`
|
|
143086
|
+
).join(", ");
|
|
143087
|
+
const params = [];
|
|
143088
|
+
for (const _col of BODY_COLUMNS) {
|
|
143089
|
+
params.push(FTS_HIT_OPEN, FTS_HIT_CLOSE, FTS_ELLIPSIS, FTS_SNIPPET_TOKENS);
|
|
143090
|
+
}
|
|
143091
|
+
params.push(match2);
|
|
143092
|
+
const where = ["conversation_messages_fts MATCH ?", "c.status = 'active'"];
|
|
143093
|
+
if (filters.account) {
|
|
143094
|
+
where.push("c.account = ?");
|
|
143095
|
+
params.push(filters.account);
|
|
143096
|
+
}
|
|
143097
|
+
if (filters.provider) {
|
|
143098
|
+
where.push("c.provider = ?");
|
|
143099
|
+
params.push(filters.provider);
|
|
143100
|
+
}
|
|
143101
|
+
if (filters.project) {
|
|
143102
|
+
where.push("(lower(c.project_path) LIKE ? OR lower(c.project_name) LIKE ?)");
|
|
143103
|
+
const like = `%${filters.project.toLowerCase()}%`;
|
|
143104
|
+
params.push(like, like);
|
|
143105
|
+
}
|
|
143106
|
+
if (filters.since) {
|
|
143107
|
+
where.push("c.timestamp >= ?");
|
|
143108
|
+
params.push(filters.since.toISOString());
|
|
143109
|
+
}
|
|
143110
|
+
if (filters.include === "conversations") {
|
|
143111
|
+
where.push("c.is_subagent = 0 AND c.is_teammate = 0");
|
|
143112
|
+
} else if (filters.include === "subagents") {
|
|
143113
|
+
where.push("c.is_subagent = 1");
|
|
143114
|
+
} else if (filters.include === "teammates") {
|
|
143115
|
+
where.push("c.is_teammate = 1");
|
|
143116
|
+
}
|
|
143117
|
+
params.push(limit);
|
|
142771
143118
|
const rows = this.db.prepare(
|
|
142772
|
-
`SELECT source_path
|
|
142773
|
-
|
|
143119
|
+
`SELECT conversation_messages_fts.source_path AS source_path, ${snippetSelects}
|
|
143120
|
+
FROM conversation_messages_fts
|
|
143121
|
+
JOIN conversations c ON c.source_path = conversation_messages_fts.source_path
|
|
143122
|
+
WHERE ${where.join(" AND ")}
|
|
142774
143123
|
ORDER BY rank
|
|
142775
143124
|
LIMIT ?`
|
|
142776
|
-
).all(
|
|
142777
|
-
return rows.map((
|
|
143125
|
+
).all(...params);
|
|
143126
|
+
return rows.map((row) => ({ sourcePath: row.source_path, body: pickBodySnippet(row) }));
|
|
142778
143127
|
}
|
|
142779
143128
|
count() {
|
|
142780
143129
|
return this.db.prepare("SELECT COUNT(*) AS n FROM conversation_messages_fts").get().n;
|
|
142781
143130
|
}
|
|
142782
143131
|
};
|
|
143132
|
+
function pickBodySnippet(row) {
|
|
143133
|
+
for (const col of BODY_COLUMNS) {
|
|
143134
|
+
const parsed = parseFtsSnippet(row[`snippet_${col.name}`]);
|
|
143135
|
+
if (parsed) return parsed;
|
|
143136
|
+
}
|
|
143137
|
+
return null;
|
|
143138
|
+
}
|
|
142783
143139
|
function toMatchQuery(query) {
|
|
142784
143140
|
const terms = query.trim().split(/\s+/).map((t) => t.replace(/"/g, "").trim()).filter(Boolean);
|
|
142785
143141
|
if (terms.length === 0) return "";
|
|
@@ -142955,9 +143311,13 @@ var PersistentEngine = class {
|
|
|
142955
143311
|
const state = resume ? JSON.parse(existing.reducer_state) : initialReducerState();
|
|
142956
143312
|
const startOffset = resume ? existing.last_indexed_offset : 0;
|
|
142957
143313
|
const startLine = resume ? existing.last_indexed_line : 0;
|
|
143314
|
+
let searchDelta = emptySearchDocument();
|
|
143315
|
+
const collectSearch = (entry) => {
|
|
143316
|
+
searchDelta = appendSearchDelta(searchDelta, extractSearchDelta(entry));
|
|
143317
|
+
};
|
|
142958
143318
|
let result;
|
|
142959
143319
|
try {
|
|
142960
|
-
result = await tailReduce(filePath, startOffset, startLine, state, tier);
|
|
143320
|
+
result = await tailReduce(filePath, startOffset, startLine, state, tier, collectSearch);
|
|
142961
143321
|
} catch (err) {
|
|
142962
143322
|
log15.warn({ filePath, err }, "persistent: tail read failed");
|
|
142963
143323
|
return { meta: null, change };
|
|
@@ -142970,9 +143330,13 @@ var PersistentEngine = class {
|
|
|
142970
143330
|
meta3.gitBranch = resolveGitBranch(meta3.projectPath);
|
|
142971
143331
|
const fp = stat42.size > 0 ? fingerprint(filePath, stat42.size) : null;
|
|
142972
143332
|
const fileId = this.files.ensure(filePath, account);
|
|
143333
|
+
const base = resume ? this.fts.readDocument(fileId) ?? emptySearchDocument() : emptySearchDocument();
|
|
143334
|
+
const searchDoc = appendSearchDelta(base, searchDelta);
|
|
142973
143335
|
const upsert = this.db.transaction(() => {
|
|
142974
143336
|
this.conversations.upsert(fileId, meta3, state.pageMessageCount);
|
|
142975
|
-
this.fts.
|
|
143337
|
+
if (!this.fts.isCurrent(fileId, meta3, searchDoc)) {
|
|
143338
|
+
this.fts.upsert(fileId, meta3, searchDoc);
|
|
143339
|
+
}
|
|
142976
143340
|
if (!resume) this.checkpoints.remove(filePath);
|
|
142977
143341
|
this.files.updateCursor(fileId, {
|
|
142978
143342
|
sizeBytes: stat42.size,
|
|
@@ -143015,7 +143379,10 @@ var PersistentEngine = class {
|
|
|
143015
143379
|
// any change reparses from 0 again. No reducer_state is persisted.
|
|
143016
143380
|
async indexFileWithProvider(provider, filePath, account, tier, stat42, resolveGitBranch) {
|
|
143017
143381
|
const log15 = getLogger2();
|
|
143018
|
-
|
|
143382
|
+
let searchDoc = emptySearchDocument();
|
|
143383
|
+
const meta3 = await parseMetaWithProvider(provider, filePath, account, tier, (entry) => {
|
|
143384
|
+
searchDoc = appendSearchDelta(searchDoc, extractSearchDelta(entry));
|
|
143385
|
+
});
|
|
143019
143386
|
if (!meta3) {
|
|
143020
143387
|
this.markDeleted(filePath);
|
|
143021
143388
|
return null;
|
|
@@ -143027,7 +143394,9 @@ var PersistentEngine = class {
|
|
|
143027
143394
|
const fileId = this.files.ensure(filePath, account);
|
|
143028
143395
|
const upsert = this.db.transaction(() => {
|
|
143029
143396
|
this.conversations.upsert(fileId, meta3, meta3.messageCount);
|
|
143030
|
-
this.fts.
|
|
143397
|
+
if (!this.fts.isCurrent(fileId, meta3, searchDoc)) {
|
|
143398
|
+
this.fts.upsert(fileId, meta3, searchDoc);
|
|
143399
|
+
}
|
|
143031
143400
|
this.checkpoints.remove(filePath);
|
|
143032
143401
|
this.files.updateCursor(fileId, {
|
|
143033
143402
|
sizeBytes: stat42.size,
|
|
@@ -143053,7 +143422,7 @@ var PersistentEngine = class {
|
|
|
143053
143422
|
if (!existing) return;
|
|
143054
143423
|
const tx = this.db.transaction(() => {
|
|
143055
143424
|
this.conversations.deleteByFileId(existing.id);
|
|
143056
|
-
this.fts.remove(
|
|
143425
|
+
this.fts.remove(existing.id);
|
|
143057
143426
|
this.checkpoints.remove(filePath);
|
|
143058
143427
|
this.files.setStatus(existing.id, "deleted");
|
|
143059
143428
|
});
|
|
@@ -143069,20 +143438,24 @@ var PersistentEngine = class {
|
|
|
143069
143438
|
getAllBySessionId(sessionId) {
|
|
143070
143439
|
return this.conversations.getAllBySessionId(sessionId);
|
|
143071
143440
|
}
|
|
143072
|
-
// Ranked
|
|
143073
|
-
//
|
|
143074
|
-
//
|
|
143075
|
-
|
|
143076
|
-
|
|
143077
|
-
|
|
143078
|
-
|
|
143079
|
-
|
|
143080
|
-
const
|
|
143081
|
-
for (const
|
|
143082
|
-
const meta3 = this.conversations.getBySourcePath(
|
|
143083
|
-
if (meta3)
|
|
143441
|
+
// Ranked hits matching the FTS query, best first, each already resolved to its
|
|
143442
|
+
// active conversation row and carrying the body excerpt when the match was in
|
|
143443
|
+
// the conversation body.
|
|
143444
|
+
//
|
|
143445
|
+
// Filters are passed down into SQL rather than applied to the result: with a
|
|
143446
|
+
// wide corpus, filtering an already-LIMITed list drops conversations that
|
|
143447
|
+
// would have matched.
|
|
143448
|
+
searchHits(query, limit, filters = {}) {
|
|
143449
|
+
const hits = [];
|
|
143450
|
+
for (const hit of this.fts.search(query, limit, filters)) {
|
|
143451
|
+
const meta3 = this.conversations.getBySourcePath(hit.sourcePath);
|
|
143452
|
+
if (meta3) hits.push({ meta: meta3, body: hit.body });
|
|
143084
143453
|
}
|
|
143085
|
-
return
|
|
143454
|
+
return hits;
|
|
143455
|
+
}
|
|
143456
|
+
// Empty-query listing, mirroring the in-memory indexer's behavior.
|
|
143457
|
+
recentMetas(limit) {
|
|
143458
|
+
return this.conversations.recent(limit);
|
|
143086
143459
|
}
|
|
143087
143460
|
getProjects() {
|
|
143088
143461
|
return this.conversations.distinctProjects();
|
|
@@ -143236,6 +143609,10 @@ var DEFAULT_CONFIG_PATH2 = "~/.config/threadbase-scanner";
|
|
|
143236
143609
|
function defaultDbPath() {
|
|
143237
143610
|
return process.env.TB_SCANNER_DB ?? (0, import_path10.join)((0, import_os8.homedir)(), ".config", "threadbase-scanner", "index.db");
|
|
143238
143611
|
}
|
|
143612
|
+
function capForMemory(doc, max) {
|
|
143613
|
+
const combined = combineSearchContent(doc);
|
|
143614
|
+
return combined.length > max ? combined.slice(-max) : combined;
|
|
143615
|
+
}
|
|
143239
143616
|
var ConversationScanner = class {
|
|
143240
143617
|
metadataCache = /* @__PURE__ */ new Map();
|
|
143241
143618
|
// Parsed conversations plus (persistent claude-code entries only) the resume
|
|
@@ -143247,6 +143624,10 @@ var ConversationScanner = class {
|
|
|
143247
143624
|
sessionIdIndex = /* @__PURE__ */ new Map();
|
|
143248
143625
|
projects = /* @__PURE__ */ new Set();
|
|
143249
143626
|
indexer = new SearchIndexer();
|
|
143627
|
+
// Search body per conversation for the in-memory path, already tail-capped to
|
|
143628
|
+
// the content tier. Survives scan() so a statCache hit — which skips the parse
|
|
143629
|
+
// entirely — can still index a body rather than an empty string.
|
|
143630
|
+
searchContents = /* @__PURE__ */ new Map();
|
|
143250
143631
|
// Tier the most recent scan() ran with, so refreshFile() re-parses a single
|
|
143251
143632
|
// file at the same content depth. Defaults to the standard tier.
|
|
143252
143633
|
lastTier = resolveTier("standard");
|
|
@@ -143407,34 +143788,42 @@ var ConversationScanner = class {
|
|
|
143407
143788
|
try {
|
|
143408
143789
|
const s3 = (0, import_fs9.statSync)(filePath);
|
|
143409
143790
|
if (s3.mtimeMs === cached4.stat.mtimeMs && s3.size === cached4.stat.size) {
|
|
143410
|
-
return
|
|
143791
|
+
return {
|
|
143792
|
+
meta: cached4.meta,
|
|
143793
|
+
searchContent: this.searchContents.get(cached4.meta.id)
|
|
143794
|
+
};
|
|
143411
143795
|
}
|
|
143412
143796
|
} catch {
|
|
143413
143797
|
}
|
|
143414
143798
|
}
|
|
143415
143799
|
}
|
|
143416
143800
|
try {
|
|
143417
|
-
|
|
143801
|
+
let doc = emptySearchDocument();
|
|
143802
|
+
const meta3 = await parseMetaWithProvider(provider, filePath, account, tier, (entry) => {
|
|
143803
|
+
doc = appendSearchDelta(doc, extractSearchDelta(entry));
|
|
143804
|
+
});
|
|
143418
143805
|
if (meta3 && meta3.gitBranch === null && meta3.projectPath) {
|
|
143419
143806
|
meta3.gitBranch = resolveGitBranch(meta3.projectPath);
|
|
143420
143807
|
}
|
|
143421
|
-
return meta3;
|
|
143808
|
+
return { meta: meta3, searchContent: capForMemory(doc, tier.snippetMax) };
|
|
143422
143809
|
} catch (err) {
|
|
143423
143810
|
parseFailures++;
|
|
143424
143811
|
log15.warn({ filePath, account, provider: provider.name, err }, "scan: parse threw");
|
|
143425
|
-
return null;
|
|
143812
|
+
return { meta: null, searchContent: void 0 };
|
|
143426
143813
|
}
|
|
143427
143814
|
})
|
|
143428
143815
|
);
|
|
143429
143816
|
const batchMetas = [];
|
|
143430
|
-
for (const meta3 of results) {
|
|
143817
|
+
for (const { meta: meta3, searchContent } of results) {
|
|
143431
143818
|
if (meta3 && meta3.messageCount > 0) {
|
|
143432
143819
|
this.metadataCache.set(meta3.id, meta3);
|
|
143433
143820
|
this.addToSessionIndex(meta3);
|
|
143434
143821
|
this.projects.add(meta3.projectPath);
|
|
143435
143822
|
allMetas.push(meta3);
|
|
143436
143823
|
batchMetas.push(meta3);
|
|
143437
|
-
this.
|
|
143824
|
+
const content = searchContent ?? this.searchContents.get(meta3.id) ?? "";
|
|
143825
|
+
this.searchContents.set(meta3.id, content);
|
|
143826
|
+
this.indexer.addDocument(meta3, content);
|
|
143438
143827
|
}
|
|
143439
143828
|
}
|
|
143440
143829
|
if (batchMetas.length > 0) {
|
|
@@ -143471,12 +143860,29 @@ var ConversationScanner = class {
|
|
|
143471
143860
|
const activeProfiles = profiles.filter((p2) => p2.enabled && p2.scanHistory !== false);
|
|
143472
143861
|
await engine.indexAll(activeProfiles, { ...options, limit: void 0, offset: void 0 });
|
|
143473
143862
|
}
|
|
143474
|
-
|
|
143475
|
-
|
|
143476
|
-
|
|
143477
|
-
|
|
143478
|
-
|
|
143479
|
-
|
|
143863
|
+
if (query.trim()) {
|
|
143864
|
+
const want = (options.limit ?? 50) + (options.offset ?? 0);
|
|
143865
|
+
results = engine.searchHits(query, want, {
|
|
143866
|
+
account: options.account,
|
|
143867
|
+
provider: options.provider,
|
|
143868
|
+
project: options.project,
|
|
143869
|
+
since: options.since ? parseSinceCutoff(options.since) : void 0,
|
|
143870
|
+
include: options.include
|
|
143871
|
+
}).map(({ meta: meta3, body }) => ({
|
|
143872
|
+
meta: meta3,
|
|
143873
|
+
score: 1,
|
|
143874
|
+
matches: body ? [
|
|
143875
|
+
{ field: CONTENT_FIELD, snippet: body.snippet, highlights: body.highlights },
|
|
143876
|
+
...generateMatches(meta3, query).filter((m2) => m2.field !== "preview")
|
|
143877
|
+
] : generateMatches(meta3, query)
|
|
143878
|
+
}));
|
|
143879
|
+
} else {
|
|
143880
|
+
results = engine.recentMetas((options.limit ?? 50) + (options.offset ?? 0)).map((meta3) => ({
|
|
143881
|
+
meta: meta3,
|
|
143882
|
+
score: 1,
|
|
143883
|
+
matches: [{ field: "timestamp", snippet: meta3.preview }]
|
|
143884
|
+
}));
|
|
143885
|
+
}
|
|
143480
143886
|
} else {
|
|
143481
143887
|
if (this.indexer.getDocumentCount() === 0) {
|
|
143482
143888
|
log15.debug("search: index empty, triggering scan");
|
|
@@ -143670,8 +144076,11 @@ var ConversationScanner = class {
|
|
|
143670
144076
|
const previous = this.metadataCache.get(filePath);
|
|
143671
144077
|
const resolvedAccount = account ?? previous?.account ?? "default";
|
|
143672
144078
|
let meta3 = null;
|
|
144079
|
+
let searchDoc = emptySearchDocument();
|
|
143673
144080
|
try {
|
|
143674
|
-
meta3 = await parseMeta(filePath, resolvedAccount, this.lastTier)
|
|
144081
|
+
meta3 = await parseMeta(filePath, resolvedAccount, this.lastTier, (entry) => {
|
|
144082
|
+
searchDoc = appendSearchDelta(searchDoc, extractSearchDelta(entry));
|
|
144083
|
+
});
|
|
143675
144084
|
} catch (err) {
|
|
143676
144085
|
log15.warn({ filePath, err }, "refreshFile: parseMeta threw");
|
|
143677
144086
|
meta3 = null;
|
|
@@ -143688,6 +144097,7 @@ var ConversationScanner = class {
|
|
|
143688
144097
|
this.metadataCache.delete(previous.id);
|
|
143689
144098
|
this.removeFromSessionIndex(previous);
|
|
143690
144099
|
this.indexer.removeDocument(previous.id);
|
|
144100
|
+
this.searchContents.delete(previous.id);
|
|
143691
144101
|
}
|
|
143692
144102
|
log15.debug({ filePath }, "refreshFile: dropped (no parseable messages)");
|
|
143693
144103
|
return null;
|
|
@@ -143697,10 +144107,12 @@ var ConversationScanner = class {
|
|
|
143697
144107
|
this.metadataCache.set(meta3.id, meta3);
|
|
143698
144108
|
this.addToSessionIndex(meta3);
|
|
143699
144109
|
this.projects.add(meta3.projectPath);
|
|
144110
|
+
const searchContent = capForMemory(searchDoc, this.lastTier.snippetMax);
|
|
144111
|
+
this.searchContents.set(meta3.id, searchContent);
|
|
143700
144112
|
if (previous) {
|
|
143701
|
-
this.indexer.updateDocument(meta3);
|
|
144113
|
+
this.indexer.updateDocument(meta3, searchContent);
|
|
143702
144114
|
} else {
|
|
143703
|
-
this.indexer.addDocument(meta3);
|
|
144115
|
+
this.indexer.addDocument(meta3, searchContent);
|
|
143704
144116
|
}
|
|
143705
144117
|
log15.debug(
|
|
143706
144118
|
{ filePath, messageCount: meta3.messageCount },
|
|
@@ -145550,7 +145962,7 @@ function paginate(results, offset, limit) {
|
|
|
145550
145962
|
}
|
|
145551
145963
|
|
|
145552
145964
|
// src/utils/codexConversationLine.ts
|
|
145553
|
-
function
|
|
145965
|
+
function extractCodexText3(content) {
|
|
145554
145966
|
if (typeof content === "string") return content.trim();
|
|
145555
145967
|
if (!Array.isArray(content)) return "";
|
|
145556
145968
|
return content.map((item) => {
|
|
@@ -145589,7 +146001,7 @@ function classifyCodexLine(line) {
|
|
|
145589
146001
|
if (role !== "user" && role !== "assistant") {
|
|
145590
146002
|
return { kind: "ignored", reason: `role ${String(role)} is not rendered` };
|
|
145591
146003
|
}
|
|
145592
|
-
const text =
|
|
146004
|
+
const text = extractCodexText3(payload.content);
|
|
145593
146005
|
if (!text) {
|
|
145594
146006
|
return { kind: "ignored", reason: "message has no extractable text" };
|
|
145595
146007
|
}
|
|
@@ -148242,7 +148654,9 @@ var SessionHandlers = class {
|
|
|
148242
148654
|
res.end(JSON.stringify({ error: "Session not found" }));
|
|
148243
148655
|
return;
|
|
148244
148656
|
}
|
|
148657
|
+
const shouldForget = this.shouldForgetEmptySession(session);
|
|
148245
148658
|
if (session.status === "idle") {
|
|
148659
|
+
if (shouldForget) this.forgetEmptyStoppedSession(sessionId);
|
|
148246
148660
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
148247
148661
|
res.end(JSON.stringify({ status: "already_idle", sessionId }));
|
|
148248
148662
|
return;
|
|
@@ -148270,6 +148684,7 @@ var SessionHandlers = class {
|
|
|
148270
148684
|
this.ptyManager.putOnHold(sessionId);
|
|
148271
148685
|
this.discoveryCache = null;
|
|
148272
148686
|
const outcome = await Promise.race([idlePromise, timeoutPromise]);
|
|
148687
|
+
if (shouldForget) this.forgetEmptyStoppedSession(sessionId);
|
|
148273
148688
|
if (outcome === "idle") {
|
|
148274
148689
|
res.write(`${JSON.stringify({ event: "stopped", sessionId })}
|
|
148275
148690
|
`);
|
|
@@ -148282,6 +148697,42 @@ var SessionHandlers = class {
|
|
|
148282
148697
|
}
|
|
148283
148698
|
res.end();
|
|
148284
148699
|
}
|
|
148700
|
+
/**
|
|
148701
|
+
* An unused start: the user never submitted a prompt, and the conversation
|
|
148702
|
+
* cache has no row for this id (empty Codex/Claude often never write a JSONL).
|
|
148703
|
+
* `conversationId === sessionId` is not evidence of history — only the cache
|
|
148704
|
+
* is. promptCount > 0 or a cache hit keeps today's hold path.
|
|
148705
|
+
*/
|
|
148706
|
+
shouldForgetEmptySession(session) {
|
|
148707
|
+
const stored = this.sessionStore.getManaged(session.id);
|
|
148708
|
+
const promptCount = Math.max(session.promptCount, stored?.promptCount ?? 0);
|
|
148709
|
+
if (promptCount > 0) return false;
|
|
148710
|
+
return !this.hasCachedConversationFor(session, stored);
|
|
148711
|
+
}
|
|
148712
|
+
hasCachedConversationFor(session, stored) {
|
|
148713
|
+
const cache = this.cache;
|
|
148714
|
+
if (!cache) return false;
|
|
148715
|
+
const ids = /* @__PURE__ */ new Set([session.id]);
|
|
148716
|
+
if (session.boundConversationId) ids.add(session.boundConversationId);
|
|
148717
|
+
if (session.resumedFromConversationId) ids.add(session.resumedFromConversationId);
|
|
148718
|
+
if (stored?.boundConversationId) ids.add(stored.boundConversationId);
|
|
148719
|
+
if (stored?.resumedFromConversationId) ids.add(stored.resumedFromConversationId);
|
|
148720
|
+
for (const id of ids) {
|
|
148721
|
+
if (cache.hasConversation(id)) return true;
|
|
148722
|
+
}
|
|
148723
|
+
return false;
|
|
148724
|
+
}
|
|
148725
|
+
forgetEmptyStoppedSession(sessionId) {
|
|
148726
|
+
this.log.info(`[stop] forgetting empty session ${sessionId.slice(0, 8)}`, {
|
|
148727
|
+
event: "session.forget_empty",
|
|
148728
|
+
sessionId
|
|
148729
|
+
});
|
|
148730
|
+
this.deps.forgetSession(sessionId);
|
|
148731
|
+
this.wsHub.broadcast({
|
|
148732
|
+
type: "session_list",
|
|
148733
|
+
sessions: this.sessionStore.list(this.deps.ptyAttachedIds())
|
|
148734
|
+
});
|
|
148735
|
+
}
|
|
148285
148736
|
async handleAdopt(sessionId, res) {
|
|
148286
148737
|
const discovered = await discoverClaudeProcesses();
|
|
148287
148738
|
this.sessionStore.setDiscovered(discovered);
|
|
@@ -153707,6 +154158,7 @@ var StreamerServer = class {
|
|
|
153707
154158
|
spawnFlagOverrides: () => this.spawnFlagOverrides(),
|
|
153708
154159
|
resolveConversationTarget: (sessionId) => this.resolveConversationTarget(sessionId),
|
|
153709
154160
|
waitForStartupOutcome: (sessionId, timeoutMs) => this.waitForStartupOutcome(sessionId, timeoutMs),
|
|
154161
|
+
forgetSession: (sessionId) => this.forgetSession(sessionId),
|
|
153710
154162
|
abandonFailedStart: (sessionId) => this.abandonFailedStart(sessionId),
|
|
153711
154163
|
enrichResumedSessionAsync: (sessionId, projectPath, conv) => this.enrichResumedSessionAsync(sessionId, projectPath, conv),
|
|
153712
154164
|
findJsonlPath: (uuid3) => this.conversationHandlers.findJsonlPath(uuid3),
|
|
@@ -154529,13 +154981,16 @@ var StreamerServer = class {
|
|
|
154529
154981
|
json2(res, 400, { error: "Missing token or clientPublicKey" });
|
|
154530
154982
|
return;
|
|
154531
154983
|
}
|
|
154532
|
-
|
|
154533
|
-
|
|
154534
|
-
|
|
154535
|
-
|
|
154536
|
-
|
|
154537
|
-
|
|
154538
|
-
|
|
154984
|
+
const e2eeEnabled = describeE2eeCapability(this.featureFlags.e2ee).enabled;
|
|
154985
|
+
let e2eeRequest = null;
|
|
154986
|
+
if (e2eeEnabled) {
|
|
154987
|
+
try {
|
|
154988
|
+
e2eeRequest = parseE2eeRequest(body?.e2ee);
|
|
154989
|
+
} catch (err) {
|
|
154990
|
+
const e = err;
|
|
154991
|
+
json2(res, 400, { error: e.message, code: e.code });
|
|
154992
|
+
return;
|
|
154993
|
+
}
|
|
154539
154994
|
}
|
|
154540
154995
|
const precheck = this.pairTokens.wouldConsume(token);
|
|
154541
154996
|
if (!precheck.ok) {
|
|
@@ -154695,7 +155150,7 @@ var StreamerServer = class {
|
|
|
154695
155150
|
*/
|
|
154696
155151
|
getFeatureFlagsConfig() {
|
|
154697
155152
|
return {
|
|
154698
|
-
registry:
|
|
155153
|
+
registry: FEATURE_FLAG_LIST,
|
|
154699
155154
|
values: this.featureFlags,
|
|
154700
155155
|
sources: this.featureFlagSources
|
|
154701
155156
|
};
|
|
@@ -154929,29 +155384,44 @@ var StreamerServer = class {
|
|
|
154929
155384
|
});
|
|
154930
155385
|
}
|
|
154931
155386
|
/**
|
|
154932
|
-
* Drop every trace of a session
|
|
154933
|
-
*
|
|
155387
|
+
* Drop every trace of a managed session: in-memory store, durable registry
|
|
155388
|
+
* row, and the collision-probe markers that would otherwise outlive it.
|
|
154934
155389
|
*
|
|
154935
|
-
*
|
|
154936
|
-
*
|
|
154937
|
-
*
|
|
154938
|
-
*
|
|
154939
|
-
*
|
|
155390
|
+
* Used when a start never became usable (`abandonFailedStart`) and when stop
|
|
155391
|
+
* is asked to discard an empty session that has no cached conversation. The
|
|
155392
|
+
* registry delete is load-bearing — `rehydrateSessions` will bring the row
|
|
155393
|
+
* back on the next boot if it remains.
|
|
155394
|
+
*
|
|
155395
|
+
* Callers that kill the PTY (`putOnHold`) must do that *first*: onStatusChange
|
|
155396
|
+
* on idle writes `selfPtyEndedAt` and a registry status, and those have to
|
|
155397
|
+
* be cleared here afterwards.
|
|
154940
155398
|
*/
|
|
154941
|
-
|
|
155399
|
+
forgetSession(sessionId) {
|
|
154942
155400
|
this.sessionStore.removeManaged(sessionId);
|
|
154943
155401
|
this.selfPtyEndedAt.delete(sessionId);
|
|
154944
155402
|
this.contendedSessions.delete(sessionId);
|
|
154945
155403
|
try {
|
|
154946
155404
|
this.managedSessionsRepo?.delete(sessionId);
|
|
154947
155405
|
} catch (err) {
|
|
154948
|
-
this.log.warn("[registry] failed to drop a
|
|
155406
|
+
this.log.warn("[registry] failed to drop a session", {
|
|
154949
155407
|
event: "registry.forget_failed",
|
|
154950
155408
|
sessionId,
|
|
154951
155409
|
err
|
|
154952
155410
|
});
|
|
154953
155411
|
}
|
|
154954
155412
|
}
|
|
155413
|
+
/**
|
|
155414
|
+
* Drop every trace of a session that never became usable.
|
|
155415
|
+
*
|
|
155416
|
+
* The runner has already torn itself down (failStartup / handleExit); what
|
|
155417
|
+
* remains is server-side bookkeeping that would otherwise leave a dead
|
|
155418
|
+
* session in the list, a registry row claiming a spawn, and a `selfPtyEndedAt`
|
|
155419
|
+
* marker that would suppress the mtime collision signal on the NEXT resume —
|
|
155420
|
+
* i.e. it would help hide the very owner we just collided with.
|
|
155421
|
+
*/
|
|
155422
|
+
abandonFailedStart(sessionId) {
|
|
155423
|
+
this.forgetSession(sessionId);
|
|
155424
|
+
}
|
|
154955
155425
|
enrichResumedSessionAsync(sessionId, projectPath, conv) {
|
|
154956
155426
|
try {
|
|
154957
155427
|
if (!this.sessionStore.getManaged(sessionId)) return;
|
|
@@ -159147,7 +159617,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
|
|
|
159147
159617
|
(value, previous = []) => [...previous, value]
|
|
159148
159618
|
).option(
|
|
159149
159619
|
"--feature <id=bool>",
|
|
159150
|
-
|
|
159620
|
+
`Enable or disable a server feature flag. Ids are the FEATURE_FLAGS keys (${FEATURE_FLAG_IDS.join(", ")}), e.g. --feature ptyHost=true \u2014 not the THREADBASE_FEATURE_* env names. Repeatable. Overridden by the flag's env var; overrides feature_flags: in ~/.threadbase/server.yaml.`,
|
|
159151
159621
|
(value, previous = []) => [...previous, value]
|
|
159152
159622
|
).option(
|
|
159153
159623
|
"--claude-extra-args <args>",
|