@threadbase-sh/streamer 1.24.3 → 1.24.5
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 +423 -102
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +145 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +145 -24
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.d.cts
CHANGED
|
@@ -515,6 +515,34 @@ declare class ConversationCache {
|
|
|
515
515
|
* JSONL has been deleted.
|
|
516
516
|
*/
|
|
517
517
|
pruneGhostFiles(exists?: (filePath: string) => boolean): string[];
|
|
518
|
+
/**
|
|
519
|
+
* Reconcile the cache against the authoritative set of conversation file
|
|
520
|
+
* paths a fresh scan surfaced: drop any cached row whose `file_path` is not
|
|
521
|
+
* in `livePaths` (removed from disk, or now filtered out — e.g. became an
|
|
522
|
+
* agent JSONL). This is the "removed conversations" half of a ?refresh=1
|
|
523
|
+
* reconcile; the additions/updates half is upsertFromScannerMeta.
|
|
524
|
+
*
|
|
525
|
+
* Skip semantics depend on whether the file still exists on disk:
|
|
526
|
+
* - File GONE from disk → always removed, tail or not. This matches the old
|
|
527
|
+
* invalidate()+rebuild behavior (a deleted conversation must disappear on
|
|
528
|
+
* refresh) and keeps refresh=1 truthful about removals. NOTE: this is an
|
|
529
|
+
* INTENTIONAL divergence from pruneGhostFiles(), which KEEPS tailed ghosts
|
|
530
|
+
* so their cached history stays viewable on a background prune. refresh=1
|
|
531
|
+
* has the opposite contract (mobile relies on removals being reflected), so
|
|
532
|
+
* do not "unify" the two — they serve different purposes.
|
|
533
|
+
* - File STILL on disk but absent from `livePaths` → the CRITICAL #2 race:
|
|
534
|
+
* the scan snapshot predates a just-created (and now live-tailed) file.
|
|
535
|
+
* A tailed row here is actively maintained from real content, so it is
|
|
536
|
+
* kept — dropping it would flicker the active conversation out of
|
|
537
|
+
* /api/conversations. An untailed on-disk row not in the snapshot is a
|
|
538
|
+
* transient scan/discovery gap; it is left alone (not removed) and the
|
|
539
|
+
* next reconcile picks it up, rather than risk removing a real file the
|
|
540
|
+
* scan simply hasn't surfaced yet.
|
|
541
|
+
* Returns the removed IDs.
|
|
542
|
+
*/
|
|
543
|
+
reconcileDeletions(livePaths: Set<string>, opts?: {
|
|
544
|
+
exists?: (filePath: string) => boolean;
|
|
545
|
+
}): string[];
|
|
518
546
|
}
|
|
519
547
|
|
|
520
548
|
type CacheMetadataKey = "last_conversation_id" | "last_conversation_created_at" | "projects_last_indexed_at" | "conversations_last_indexed_at" | "conversations_dirty";
|
|
@@ -840,6 +868,7 @@ declare class StreamerServer {
|
|
|
840
868
|
private scannerStale;
|
|
841
869
|
private binding;
|
|
842
870
|
private cacheReady;
|
|
871
|
+
private inFlightCacheWrites;
|
|
843
872
|
private apiKey;
|
|
844
873
|
private apiKeySource;
|
|
845
874
|
private localNoAuth;
|
|
@@ -897,6 +926,7 @@ declare class StreamerServer {
|
|
|
897
926
|
}): Promise<void>;
|
|
898
927
|
private bindWithRetry;
|
|
899
928
|
private bindWithRetryLoop;
|
|
929
|
+
private trackCacheWrite;
|
|
900
930
|
close(): Promise<void>;
|
|
901
931
|
private handleRequest;
|
|
902
932
|
private handlePairStart;
|
|
@@ -917,6 +947,7 @@ declare class StreamerServer {
|
|
|
917
947
|
private newScanner;
|
|
918
948
|
private getScanner;
|
|
919
949
|
private getFreshScanner;
|
|
950
|
+
private rescanForRefresh;
|
|
920
951
|
private findJsonlPath;
|
|
921
952
|
private readCwdFromJsonl;
|
|
922
953
|
private findConversationByUuid;
|
|
@@ -987,6 +1018,15 @@ declare class ConversationWatcher {
|
|
|
987
1018
|
constructor(events?: ConversationWatcherEvents);
|
|
988
1019
|
watch(filePath: string): void;
|
|
989
1020
|
unwatch(filePath: string): void;
|
|
1021
|
+
/**
|
|
1022
|
+
* Re-drive the tail read for a file that's already being tailed. A per-file
|
|
1023
|
+
* chokidar handle can die silently (fs.watch stops firing after inode churn)
|
|
1024
|
+
* while the coarser directory watcher keeps reporting changes — calling this
|
|
1025
|
+
* from the directory-event path makes the tail self-healing. Reads are
|
|
1026
|
+
* offset-based and coalesced, so a redundant poke after a normal change
|
|
1027
|
+
* event is a cheap stat + no-op. Returns false for untailed paths.
|
|
1028
|
+
*/
|
|
1029
|
+
poke(filePath: string): boolean;
|
|
990
1030
|
/**
|
|
991
1031
|
* Watch a directory of conversation JSONL files. Fires
|
|
992
1032
|
* onConversationChanged for any add/change/unlink event so the caller
|
package/dist/index.d.ts
CHANGED
|
@@ -515,6 +515,34 @@ declare class ConversationCache {
|
|
|
515
515
|
* JSONL has been deleted.
|
|
516
516
|
*/
|
|
517
517
|
pruneGhostFiles(exists?: (filePath: string) => boolean): string[];
|
|
518
|
+
/**
|
|
519
|
+
* Reconcile the cache against the authoritative set of conversation file
|
|
520
|
+
* paths a fresh scan surfaced: drop any cached row whose `file_path` is not
|
|
521
|
+
* in `livePaths` (removed from disk, or now filtered out — e.g. became an
|
|
522
|
+
* agent JSONL). This is the "removed conversations" half of a ?refresh=1
|
|
523
|
+
* reconcile; the additions/updates half is upsertFromScannerMeta.
|
|
524
|
+
*
|
|
525
|
+
* Skip semantics depend on whether the file still exists on disk:
|
|
526
|
+
* - File GONE from disk → always removed, tail or not. This matches the old
|
|
527
|
+
* invalidate()+rebuild behavior (a deleted conversation must disappear on
|
|
528
|
+
* refresh) and keeps refresh=1 truthful about removals. NOTE: this is an
|
|
529
|
+
* INTENTIONAL divergence from pruneGhostFiles(), which KEEPS tailed ghosts
|
|
530
|
+
* so their cached history stays viewable on a background prune. refresh=1
|
|
531
|
+
* has the opposite contract (mobile relies on removals being reflected), so
|
|
532
|
+
* do not "unify" the two — they serve different purposes.
|
|
533
|
+
* - File STILL on disk but absent from `livePaths` → the CRITICAL #2 race:
|
|
534
|
+
* the scan snapshot predates a just-created (and now live-tailed) file.
|
|
535
|
+
* A tailed row here is actively maintained from real content, so it is
|
|
536
|
+
* kept — dropping it would flicker the active conversation out of
|
|
537
|
+
* /api/conversations. An untailed on-disk row not in the snapshot is a
|
|
538
|
+
* transient scan/discovery gap; it is left alone (not removed) and the
|
|
539
|
+
* next reconcile picks it up, rather than risk removing a real file the
|
|
540
|
+
* scan simply hasn't surfaced yet.
|
|
541
|
+
* Returns the removed IDs.
|
|
542
|
+
*/
|
|
543
|
+
reconcileDeletions(livePaths: Set<string>, opts?: {
|
|
544
|
+
exists?: (filePath: string) => boolean;
|
|
545
|
+
}): string[];
|
|
518
546
|
}
|
|
519
547
|
|
|
520
548
|
type CacheMetadataKey = "last_conversation_id" | "last_conversation_created_at" | "projects_last_indexed_at" | "conversations_last_indexed_at" | "conversations_dirty";
|
|
@@ -840,6 +868,7 @@ declare class StreamerServer {
|
|
|
840
868
|
private scannerStale;
|
|
841
869
|
private binding;
|
|
842
870
|
private cacheReady;
|
|
871
|
+
private inFlightCacheWrites;
|
|
843
872
|
private apiKey;
|
|
844
873
|
private apiKeySource;
|
|
845
874
|
private localNoAuth;
|
|
@@ -897,6 +926,7 @@ declare class StreamerServer {
|
|
|
897
926
|
}): Promise<void>;
|
|
898
927
|
private bindWithRetry;
|
|
899
928
|
private bindWithRetryLoop;
|
|
929
|
+
private trackCacheWrite;
|
|
900
930
|
close(): Promise<void>;
|
|
901
931
|
private handleRequest;
|
|
902
932
|
private handlePairStart;
|
|
@@ -917,6 +947,7 @@ declare class StreamerServer {
|
|
|
917
947
|
private newScanner;
|
|
918
948
|
private getScanner;
|
|
919
949
|
private getFreshScanner;
|
|
950
|
+
private rescanForRefresh;
|
|
920
951
|
private findJsonlPath;
|
|
921
952
|
private readCwdFromJsonl;
|
|
922
953
|
private findConversationByUuid;
|
|
@@ -987,6 +1018,15 @@ declare class ConversationWatcher {
|
|
|
987
1018
|
constructor(events?: ConversationWatcherEvents);
|
|
988
1019
|
watch(filePath: string): void;
|
|
989
1020
|
unwatch(filePath: string): void;
|
|
1021
|
+
/**
|
|
1022
|
+
* Re-drive the tail read for a file that's already being tailed. A per-file
|
|
1023
|
+
* chokidar handle can die silently (fs.watch stops firing after inode churn)
|
|
1024
|
+
* while the coarser directory watcher keeps reporting changes — calling this
|
|
1025
|
+
* from the directory-event path makes the tail self-healing. Reads are
|
|
1026
|
+
* offset-based and coalesced, so a redundant poke after a normal change
|
|
1027
|
+
* event is a cheap stat + no-op. Returns false for untailed paths.
|
|
1028
|
+
*/
|
|
1029
|
+
poke(filePath: string): boolean;
|
|
990
1030
|
/**
|
|
991
1031
|
* Watch a directory of conversation JSONL files. Fires
|
|
992
1032
|
* onConversationChanged for any add/change/unlink event so the caller
|
package/dist/index.js
CHANGED
|
@@ -3768,6 +3768,61 @@ var ConversationCache = class _ConversationCache {
|
|
|
3768
3768
|
}
|
|
3769
3769
|
return ghosts;
|
|
3770
3770
|
}
|
|
3771
|
+
/**
|
|
3772
|
+
* Reconcile the cache against the authoritative set of conversation file
|
|
3773
|
+
* paths a fresh scan surfaced: drop any cached row whose `file_path` is not
|
|
3774
|
+
* in `livePaths` (removed from disk, or now filtered out — e.g. became an
|
|
3775
|
+
* agent JSONL). This is the "removed conversations" half of a ?refresh=1
|
|
3776
|
+
* reconcile; the additions/updates half is upsertFromScannerMeta.
|
|
3777
|
+
*
|
|
3778
|
+
* Skip semantics depend on whether the file still exists on disk:
|
|
3779
|
+
* - File GONE from disk → always removed, tail or not. This matches the old
|
|
3780
|
+
* invalidate()+rebuild behavior (a deleted conversation must disappear on
|
|
3781
|
+
* refresh) and keeps refresh=1 truthful about removals. NOTE: this is an
|
|
3782
|
+
* INTENTIONAL divergence from pruneGhostFiles(), which KEEPS tailed ghosts
|
|
3783
|
+
* so their cached history stays viewable on a background prune. refresh=1
|
|
3784
|
+
* has the opposite contract (mobile relies on removals being reflected), so
|
|
3785
|
+
* do not "unify" the two — they serve different purposes.
|
|
3786
|
+
* - File STILL on disk but absent from `livePaths` → the CRITICAL #2 race:
|
|
3787
|
+
* the scan snapshot predates a just-created (and now live-tailed) file.
|
|
3788
|
+
* A tailed row here is actively maintained from real content, so it is
|
|
3789
|
+
* kept — dropping it would flicker the active conversation out of
|
|
3790
|
+
* /api/conversations. An untailed on-disk row not in the snapshot is a
|
|
3791
|
+
* transient scan/discovery gap; it is left alone (not removed) and the
|
|
3792
|
+
* next reconcile picks it up, rather than risk removing a real file the
|
|
3793
|
+
* scan simply hasn't surfaced yet.
|
|
3794
|
+
* Returns the removed IDs.
|
|
3795
|
+
*/
|
|
3796
|
+
reconcileDeletions(livePaths, opts) {
|
|
3797
|
+
const exists = opts?.exists ?? existsSync5;
|
|
3798
|
+
const rows = this.stmts.allFilePaths.all();
|
|
3799
|
+
const removed = [];
|
|
3800
|
+
const drop = this.db.transaction((ids) => {
|
|
3801
|
+
for (const id of ids) {
|
|
3802
|
+
this.stmts.deleteTailById.run(id);
|
|
3803
|
+
this.stmts.deleteById.run(id);
|
|
3804
|
+
}
|
|
3805
|
+
});
|
|
3806
|
+
for (const row of rows) {
|
|
3807
|
+
if (livePaths.has(row.file_path)) continue;
|
|
3808
|
+
if (exists(row.file_path)) continue;
|
|
3809
|
+
removed.push(row.id);
|
|
3810
|
+
}
|
|
3811
|
+
if (removed.length > 0) {
|
|
3812
|
+
drop(removed);
|
|
3813
|
+
if (this.fileIndexLoaded) {
|
|
3814
|
+
for (const id of removed) {
|
|
3815
|
+
for (const [fp, cid] of this.fileIndex) {
|
|
3816
|
+
if (cid === id) {
|
|
3817
|
+
this.fileIndex.delete(fp);
|
|
3818
|
+
break;
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
return removed;
|
|
3825
|
+
}
|
|
3771
3826
|
};
|
|
3772
3827
|
|
|
3773
3828
|
// src/db/repositories/cacheMetadata.repository.ts
|
|
@@ -4127,6 +4182,19 @@ var ConversationWatcher = class {
|
|
|
4127
4182
|
void entry.watcher.close();
|
|
4128
4183
|
this.files.delete(filePath);
|
|
4129
4184
|
}
|
|
4185
|
+
/**
|
|
4186
|
+
* Re-drive the tail read for a file that's already being tailed. A per-file
|
|
4187
|
+
* chokidar handle can die silently (fs.watch stops firing after inode churn)
|
|
4188
|
+
* while the coarser directory watcher keeps reporting changes — calling this
|
|
4189
|
+
* from the directory-event path makes the tail self-healing. Reads are
|
|
4190
|
+
* offset-based and coalesced, so a redundant poke after a normal change
|
|
4191
|
+
* event is a cheap stat + no-op. Returns false for untailed paths.
|
|
4192
|
+
*/
|
|
4193
|
+
poke(filePath) {
|
|
4194
|
+
if (!this.files.has(filePath)) return false;
|
|
4195
|
+
void this.readNewLines(filePath);
|
|
4196
|
+
return true;
|
|
4197
|
+
}
|
|
4130
4198
|
/**
|
|
4131
4199
|
* Watch a directory of conversation JSONL files. Fires
|
|
4132
4200
|
* onConversationChanged for any add/change/unlink event so the caller
|
|
@@ -4832,6 +4900,14 @@ var StreamerServer = class {
|
|
|
4832
4900
|
// window so the self-healing kickstart-relaunch race doesn't spam warn.
|
|
4833
4901
|
binding = false;
|
|
4834
4902
|
cacheReady = false;
|
|
4903
|
+
// Every fire-and-forget task that runs a scan and then writes to this.cache
|
|
4904
|
+
// in an async continuation (startup warm-up, background count refresh, …).
|
|
4905
|
+
// close() awaits all of them before closing this.cache, so a scan's post-scan
|
|
4906
|
+
// cache writes (upsertFromScannerMeta / populateTailFromFile / pruneGhostFiles
|
|
4907
|
+
// / reconcileDeletions) can never hit a cache.db that was already closed
|
|
4908
|
+
// ("database connection is not open"), which would otherwise leave the cache
|
|
4909
|
+
// empty. Register via trackCacheWrite(); each entry removes itself on settle.
|
|
4910
|
+
inFlightCacheWrites = /* @__PURE__ */ new Set();
|
|
4835
4911
|
apiKey;
|
|
4836
4912
|
apiKeySource;
|
|
4837
4913
|
localNoAuth;
|
|
@@ -4963,6 +5039,7 @@ var StreamerServer = class {
|
|
|
4963
5039
|
}
|
|
4964
5040
|
},
|
|
4965
5041
|
onConversationChanged: (filePath) => {
|
|
5042
|
+
this.fileWatcher.poke(filePath);
|
|
4966
5043
|
this.cache?.invalidateByFilePath(filePath, { skipIfTailed: true });
|
|
4967
5044
|
this.markScannerStaleDebounced();
|
|
4968
5045
|
this.log.debug?.(`Scanner invalidated by directory event: ${filePath}`, {
|
|
@@ -5399,6 +5476,7 @@ var StreamerServer = class {
|
|
|
5399
5476
|
});
|
|
5400
5477
|
}
|
|
5401
5478
|
});
|
|
5479
|
+
this.trackCacheWrite(warmUp);
|
|
5402
5480
|
if (opts?.awaitReady) await warmUp;
|
|
5403
5481
|
}
|
|
5404
5482
|
// Bind the HTTP listener, retrying on a transient EADDRINUSE. See the call
|
|
@@ -5446,11 +5524,23 @@ var StreamerServer = class {
|
|
|
5446
5524
|
}
|
|
5447
5525
|
}
|
|
5448
5526
|
}
|
|
5527
|
+
// Register a fire-and-forget task that writes to this.cache after a scan, so
|
|
5528
|
+
// close() can await it before closing cache.db. Removes itself on settle. The
|
|
5529
|
+
// caller keeps its own error handling; this wrapper swallows rejections so a
|
|
5530
|
+
// failed task never rejects close()'s Promise.all.
|
|
5531
|
+
trackCacheWrite(task) {
|
|
5532
|
+
const guarded = task.catch(() => void 0);
|
|
5533
|
+
this.inFlightCacheWrites.add(guarded);
|
|
5534
|
+
void guarded.finally(() => {
|
|
5535
|
+
this.inFlightCacheWrites.delete(guarded);
|
|
5536
|
+
});
|
|
5537
|
+
}
|
|
5449
5538
|
async close() {
|
|
5450
5539
|
for (const timer of this.ptyGraceTimers.values()) clearTimeout(timer);
|
|
5451
5540
|
this.ptyGraceTimers.clear();
|
|
5452
5541
|
this.markScannerStaleDebounced.cancel();
|
|
5453
|
-
|
|
5542
|
+
await Promise.all([...this.inFlightCacheWrites]);
|
|
5543
|
+
await Promise.all([...this.allScanners].map((s) => s.close()));
|
|
5454
5544
|
this.allScanners.clear();
|
|
5455
5545
|
this.scanner = null;
|
|
5456
5546
|
this.cache?.close();
|
|
@@ -5590,12 +5680,23 @@ var StreamerServer = class {
|
|
|
5590
5680
|
const project = url.searchParams.get("project") ?? void 0;
|
|
5591
5681
|
const providerFilter = url.searchParams.get("provider") ?? void 0;
|
|
5592
5682
|
const bustCache = url.searchParams.get("refresh") === "1";
|
|
5593
|
-
if (bustCache) {
|
|
5594
|
-
this.
|
|
5595
|
-
|
|
5596
|
-
|
|
5683
|
+
if (bustCache && this.cache) {
|
|
5684
|
+
const scanner2 = await this.rescanForRefresh();
|
|
5685
|
+
const metas2 = [...scanner2.getMetadataCache().values()];
|
|
5686
|
+
try {
|
|
5687
|
+
this.cache.upsertFromScannerMeta(metas2);
|
|
5688
|
+
const livePaths = new Set(
|
|
5689
|
+
metas2.map((m) => m.filePath).filter((p) => Boolean(p))
|
|
5690
|
+
);
|
|
5691
|
+
this.cache.reconcileDeletions(livePaths);
|
|
5692
|
+
} catch (err) {
|
|
5693
|
+
this.log.warn(
|
|
5694
|
+
`refresh reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
5695
|
+
{ event: "conversations.reconcile_failed" }
|
|
5696
|
+
);
|
|
5697
|
+
}
|
|
5597
5698
|
}
|
|
5598
|
-
if (this.cache
|
|
5699
|
+
if (this.cache) {
|
|
5599
5700
|
const { conversations, total: total2 } = this.cache.listConversations({
|
|
5600
5701
|
project,
|
|
5601
5702
|
provider: providerFilter,
|
|
@@ -5660,12 +5761,6 @@ var StreamerServer = class {
|
|
|
5660
5761
|
};
|
|
5661
5762
|
});
|
|
5662
5763
|
json(res, 200, { conversations: adapted, hasMore: offset + limit < total, offset, total });
|
|
5663
|
-
if (this.cache && bustCache) {
|
|
5664
|
-
try {
|
|
5665
|
-
this.cache.upsertFromScannerMeta([...scanner.getMetadataCache().values()]);
|
|
5666
|
-
} catch {
|
|
5667
|
-
}
|
|
5668
|
-
}
|
|
5669
5764
|
}
|
|
5670
5765
|
async handleConversationsCount(url, res) {
|
|
5671
5766
|
const project = url.searchParams.get("project") ?? void 0;
|
|
@@ -5694,19 +5789,21 @@ var StreamerServer = class {
|
|
|
5694
5789
|
// later count reflects new/removed conversations. Never awaited by the request
|
|
5695
5790
|
// path — refresh=1 returns the cached total synchronously and this catches up.
|
|
5696
5791
|
refreshCountInBackground() {
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
this.cache
|
|
5792
|
+
this.trackCacheWrite(
|
|
5793
|
+
(async () => {
|
|
5794
|
+
try {
|
|
5795
|
+
const scanner = await this.getFreshScanner();
|
|
5796
|
+
if (this.cache) {
|
|
5797
|
+
this.cache.upsertFromScannerMeta([...scanner.getMetadataCache().values()]);
|
|
5798
|
+
}
|
|
5799
|
+
} catch (err) {
|
|
5800
|
+
this.log.warn(
|
|
5801
|
+
`Background count refresh failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
5802
|
+
{ event: "count.refresh_failed" }
|
|
5803
|
+
);
|
|
5702
5804
|
}
|
|
5703
|
-
}
|
|
5704
|
-
|
|
5705
|
-
`Background count refresh failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
5706
|
-
{ event: "count.refresh_failed" }
|
|
5707
|
-
);
|
|
5708
|
-
}
|
|
5709
|
-
})();
|
|
5805
|
+
})()
|
|
5806
|
+
);
|
|
5710
5807
|
}
|
|
5711
5808
|
handleSessionsCount(res) {
|
|
5712
5809
|
json(res, 200, { total: this.sessionStore.list(this.ptyAttachedIds()).length });
|
|
@@ -5819,6 +5916,30 @@ var StreamerServer = class {
|
|
|
5819
5916
|
this.scannerReady = null;
|
|
5820
5917
|
return this.getScanner();
|
|
5821
5918
|
}
|
|
5919
|
+
// refresh=1's scan: reuse the WARM persistent scanner (its index.db + cursors
|
|
5920
|
+
// survive, so classify() still skips unchanged files) and re-run its scan
|
|
5921
|
+
// with fullRescan:true — the escape hatch that bypasses the scanner's
|
|
5922
|
+
// dir-mtime discovery gate, since an explicit user pull-to-refresh is exactly
|
|
5923
|
+
// the "don't trust the gate, check disk for real" signal. Unlike
|
|
5924
|
+
// getFreshScanner() this does NOT discard the warm scanner. scannerReady is
|
|
5925
|
+
// only ever reassigned to a live scan promise (never nulled mid-scan), so the
|
|
5926
|
+
// getScanner() anti-infinite-loop guard is preserved.
|
|
5927
|
+
async rescanForRefresh() {
|
|
5928
|
+
if (this.scannerReady) await this.scannerReady;
|
|
5929
|
+
this.scannerStale = false;
|
|
5930
|
+
if (!this.scanner) {
|
|
5931
|
+
this.scanner = new ConversationScanner();
|
|
5932
|
+
this.allScanners.add(this.scanner);
|
|
5933
|
+
}
|
|
5934
|
+
const scanner = this.scanner;
|
|
5935
|
+
this.scannerReady = scanner.scan({
|
|
5936
|
+
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
5937
|
+
...this.codexScanOpts(),
|
|
5938
|
+
fullRescan: true
|
|
5939
|
+
});
|
|
5940
|
+
await this.scannerReady;
|
|
5941
|
+
return scanner;
|
|
5942
|
+
}
|
|
5822
5943
|
findJsonlPath(uuid) {
|
|
5823
5944
|
const projectsDir = join12(homedir5(), ".claude", "projects");
|
|
5824
5945
|
if (!existsSync7(projectsDir)) return null;
|