@threadbase-sh/streamer 1.39.0 → 1.39.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 +93 -16
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +93 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +93 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -146080,6 +146080,20 @@ var StreamerServer = class {
|
|
|
146080
146080
|
// Set by onConversationChanged while a scan is in-flight; getScanner() does
|
|
146081
146081
|
// a single rescan after the current one completes instead of restarting it.
|
|
146082
146082
|
scannerStale = false;
|
|
146083
|
+
// WHICH files scannerStale is about. A directory event names exactly one
|
|
146084
|
+
// JSONL, and the only correct response is refreshFile() on that one file —
|
|
146085
|
+
// but scannerStale alone carries no identity, so honoring it used to mean a
|
|
146086
|
+
// full-tree rescan. On the non-persistent scanner this server actually runs
|
|
146087
|
+
// (buildStatCache => persistent:false, see listen()), scan() opens by
|
|
146088
|
+
// clearing metadataCache AND conversationLRU — so one live session appending
|
|
146089
|
+
// to its own transcript threw away every OTHER conversation's parsed
|
|
146090
|
+
// snapshot, and the next full fetch of an unrelated conversation re-parsed
|
|
146091
|
+
// it from disk (745-2877ms on a 5MB/1112-message history) while the
|
|
146092
|
+
// per-file paginated path stayed at ~20ms throughout. Populated alongside
|
|
146093
|
+
// scannerStale and drained with it by takeStaleFiles(); an armed flag with
|
|
146094
|
+
// an EMPTY set means "stale, source unknown" and still falls back to the
|
|
146095
|
+
// full rescan.
|
|
146096
|
+
staleFiles = /* @__PURE__ */ new Set();
|
|
146083
146097
|
// Single-flight guard for the background disk reconcile: a burst of list
|
|
146084
146098
|
// polls during active session writes shares one rescan instead of queueing
|
|
146085
146099
|
// a full rescan per request.
|
|
@@ -146248,7 +146262,10 @@ var StreamerServer = class {
|
|
|
146248
146262
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config2.directoryScanDebounceMs ?? 1e3;
|
|
146249
146263
|
this.markScannerStaleDebounced = debounce(() => {
|
|
146250
146264
|
if (this.scannerReady) this.scannerStale = true;
|
|
146251
|
-
else
|
|
146265
|
+
else {
|
|
146266
|
+
this.scanner = null;
|
|
146267
|
+
this.staleFiles.clear();
|
|
146268
|
+
}
|
|
146252
146269
|
}, this.directoryDebounceMs);
|
|
146253
146270
|
this.includeAgents = parseIncludeAgentsEnv(process.env.THREADBASE_INCLUDE_AGENTS);
|
|
146254
146271
|
this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);
|
|
@@ -146341,6 +146358,7 @@ var StreamerServer = class {
|
|
|
146341
146358
|
if (!tailed) this.maybeAttachExternalTail(filePath);
|
|
146342
146359
|
this.sweepIdleExternalTails();
|
|
146343
146360
|
this.cache?.invalidateByFilePath(filePath, { skipIfTailed: true });
|
|
146361
|
+
this.staleFiles.add(filePath);
|
|
146344
146362
|
this.markScannerStaleDebounced();
|
|
146345
146363
|
this.log.debug?.(`Scanner invalidated by directory event: ${filePath}`, {
|
|
146346
146364
|
filePath,
|
|
@@ -147587,21 +147605,46 @@ var StreamerServer = class {
|
|
|
147587
147605
|
// so a burst of list polls during active session writes shares one rescan
|
|
147588
147606
|
// rather than queueing a full rescan each; tracked so close() awaits the
|
|
147589
147607
|
// in-flight cache write before shutting the DB.
|
|
147590
|
-
startBackgroundConversationReconcile() {
|
|
147608
|
+
startBackgroundConversationReconcile(mode = "full") {
|
|
147591
147609
|
if (this.conversationReconcileInFlight) return;
|
|
147592
|
-
const
|
|
147610
|
+
const paths = mode === "files" ? this.takeStaleFiles() : [];
|
|
147611
|
+
const task = (paths.length > 0 ? this.reconcileStaleFilesFromDisk(paths) : this.reconcileConversationsCacheFromDisk()).finally(() => {
|
|
147593
147612
|
this.conversationReconcileInFlight = null;
|
|
147594
147613
|
});
|
|
147595
147614
|
this.conversationReconcileInFlight = task;
|
|
147596
147615
|
this.trackCacheWrite(task);
|
|
147597
147616
|
}
|
|
147598
|
-
|
|
147599
|
-
|
|
147600
|
-
|
|
147601
|
-
|
|
147617
|
+
// "files": a directory event named specific JSONLs, so refresh only those.
|
|
147618
|
+
// "full": disk drifted in ways a per-file refresh can't see (a project dir
|
|
147619
|
+
// appeared, rows vanished), so walk the tree. Order matters — the staleness
|
|
147620
|
+
// check short-circuits first so the HDD freshness probe stays off the hot
|
|
147621
|
+
// poll path, exactly as it did when this returned a boolean.
|
|
147622
|
+
conversationReconcileMode() {
|
|
147623
|
+
if (!this.cache) return null;
|
|
147624
|
+
if (this.scannerStale) return "files";
|
|
147625
|
+
if (!this.conversationsRepo || !this.cacheMetadataRepo) return null;
|
|
147602
147626
|
return shouldRefreshProjectsFromHdd(this.conversationsRepo, this.cacheMetadataRepo, {
|
|
147603
147627
|
projectsDirs: this.projectsDirsForFreshnessCheck()
|
|
147604
|
-
});
|
|
147628
|
+
}) ? "full" : null;
|
|
147629
|
+
}
|
|
147630
|
+
// The per-file half of reconcileConversationsCacheFromDisk: re-index just the
|
|
147631
|
+
// changed JSONLs and upsert their rows. No reconcileDeletions here — that
|
|
147632
|
+
// needs the whole live-path set, and deletions already have their own path
|
|
147633
|
+
// (onFileDeleted -> invalidateByFilePath). New projects still arrive via the
|
|
147634
|
+
// HDD-freshness "full" mode.
|
|
147635
|
+
async reconcileStaleFilesFromDisk(paths) {
|
|
147636
|
+
if (!this.cache) return;
|
|
147637
|
+
const scanner = await this.getScanner(true);
|
|
147638
|
+
const metas = await this.refreshStaleFiles(scanner, paths);
|
|
147639
|
+
if (metas.length === 0) return;
|
|
147640
|
+
try {
|
|
147641
|
+
this.cache.upsertFromScannerMeta(metas);
|
|
147642
|
+
} catch (err) {
|
|
147643
|
+
this.log.warn(
|
|
147644
|
+
`stale-file reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
147645
|
+
{ event: "conversations.reconcile_failed" }
|
|
147646
|
+
);
|
|
147647
|
+
}
|
|
147605
147648
|
}
|
|
147606
147649
|
async handleListConversations(url2, res) {
|
|
147607
147650
|
if (this.rejectIfWarmingUp(res)) return;
|
|
@@ -147611,10 +147654,11 @@ var StreamerServer = class {
|
|
|
147611
147654
|
const project = url2.searchParams.get("project") ?? void 0;
|
|
147612
147655
|
const providerFilter = url2.searchParams.get("provider") ?? void 0;
|
|
147613
147656
|
const bustCache = url2.searchParams.get("refresh") === "1";
|
|
147614
|
-
|
|
147657
|
+
const reconcileMode = this.conversationReconcileMode();
|
|
147658
|
+
if (this.cache && (bustCache || reconcileMode)) {
|
|
147615
147659
|
const canServeStale = !bustCache && this.cache.listConversations({ limit: 0, offset: 0 }).total > 0;
|
|
147616
147660
|
if (canServeStale) {
|
|
147617
|
-
this.startBackgroundConversationReconcile();
|
|
147661
|
+
this.startBackgroundConversationReconcile(reconcileMode ?? "full");
|
|
147618
147662
|
} else {
|
|
147619
147663
|
const shouldEmitProgress = createScanProgressThrottle();
|
|
147620
147664
|
await this.withWarmup(
|
|
@@ -147814,21 +147858,54 @@ var StreamerServer = class {
|
|
|
147814
147858
|
options ?? (this.scannerPersistenceDisabled ? { persistent: false } : void 0)
|
|
147815
147859
|
);
|
|
147816
147860
|
}
|
|
147861
|
+
// Drain the stale set and disarm the flag together. The caller owns the
|
|
147862
|
+
// returned paths: clearing before the refresh means events that land DURING
|
|
147863
|
+
// it re-arm the flag and get their own pass instead of being swallowed.
|
|
147864
|
+
takeStaleFiles() {
|
|
147865
|
+
const paths = [...this.staleFiles];
|
|
147866
|
+
this.staleFiles.clear();
|
|
147867
|
+
this.scannerStale = false;
|
|
147868
|
+
return paths;
|
|
147869
|
+
}
|
|
147870
|
+
// Reconcile exactly the JSONLs a directory event named. Failures are logged
|
|
147871
|
+
// and swallowed per file: one unreadable transcript must not abort the
|
|
147872
|
+
// others, and the file simply stays on its previous snapshot until the next
|
|
147873
|
+
// event — the same outcome the full rescan gave on a parse failure.
|
|
147874
|
+
async refreshStaleFiles(scanner, paths) {
|
|
147875
|
+
const metas = await Promise.all(
|
|
147876
|
+
paths.map(
|
|
147877
|
+
(filePath) => scanner.refreshFile(filePath).catch((err) => {
|
|
147878
|
+
this.log.warn("scanner.refreshFile: failed", {
|
|
147879
|
+
event: "scanner.refresh_failed",
|
|
147880
|
+
filePath,
|
|
147881
|
+
trigger: "directory-event",
|
|
147882
|
+
err
|
|
147883
|
+
});
|
|
147884
|
+
return null;
|
|
147885
|
+
})
|
|
147886
|
+
)
|
|
147887
|
+
);
|
|
147888
|
+
return metas.filter((m2) => m2 !== null);
|
|
147889
|
+
}
|
|
147817
147890
|
async getScanner(skipStaleRescan = false) {
|
|
147818
147891
|
if (this.scannerReady) {
|
|
147819
147892
|
await this.scannerReady;
|
|
147820
147893
|
if (this.scanner) {
|
|
147821
147894
|
if (skipStaleRescan) return this.scanner;
|
|
147822
147895
|
if (this.scannerStale) {
|
|
147823
|
-
|
|
147824
|
-
|
|
147825
|
-
|
|
147826
|
-
|
|
147896
|
+
const paths = this.takeStaleFiles();
|
|
147897
|
+
if (paths.length === 0) {
|
|
147898
|
+
this.scanner = null;
|
|
147899
|
+
this.scannerReady = null;
|
|
147900
|
+
return this.getScanner();
|
|
147901
|
+
}
|
|
147902
|
+
await this.refreshStaleFiles(this.scanner, paths);
|
|
147903
|
+
return this.scanner ?? this.getScanner();
|
|
147827
147904
|
}
|
|
147828
147905
|
return this.scanner;
|
|
147829
147906
|
}
|
|
147830
147907
|
}
|
|
147831
|
-
this.
|
|
147908
|
+
this.takeStaleFiles();
|
|
147832
147909
|
const statCache = this.buildStatCache(this.scanner);
|
|
147833
147910
|
this.scanner = this.newScanner(statCache ? { persistent: false } : void 0);
|
|
147834
147911
|
this.allScanners.add(this.scanner);
|
|
@@ -147862,7 +147939,7 @@ var StreamerServer = class {
|
|
|
147862
147939
|
// getScanner() anti-infinite-loop guard is preserved.
|
|
147863
147940
|
async rescanForRefresh(onProgress) {
|
|
147864
147941
|
if (this.scannerReady) await this.scannerReady;
|
|
147865
|
-
this.
|
|
147942
|
+
this.takeStaleFiles();
|
|
147866
147943
|
if (!this.scanner) {
|
|
147867
147944
|
this.scanner = new ConversationScanner();
|
|
147868
147945
|
this.allScanners.add(this.scanner);
|