@threadbase-sh/streamer 1.36.1 → 1.36.3
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 +580 -356
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +287 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +272 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1200,6 +1200,7 @@ declare class StreamerServer {
|
|
|
1200
1200
|
private allScanners;
|
|
1201
1201
|
private scannerReady;
|
|
1202
1202
|
private scannerStale;
|
|
1203
|
+
private conversationReconcileInFlight;
|
|
1203
1204
|
private refreshInFlight;
|
|
1204
1205
|
private binding;
|
|
1205
1206
|
private activeWarmups;
|
|
@@ -1285,6 +1286,16 @@ declare class StreamerServer {
|
|
|
1285
1286
|
private checkExchangeRateLimit;
|
|
1286
1287
|
private checkSessionStartRateLimit;
|
|
1287
1288
|
private checkSessionInputRateLimit;
|
|
1289
|
+
/** Project roots watched for conversation JSONLs (profiles or ~/.claude/projects). */
|
|
1290
|
+
private projectsDirsForFreshnessCheck;
|
|
1291
|
+
/**
|
|
1292
|
+
* Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by
|
|
1293
|
+
* the automatic freshness path when the directory watcher marked the scanner
|
|
1294
|
+
* stale or shouldRefreshProjectsFromHdd detected disk drift.
|
|
1295
|
+
*/
|
|
1296
|
+
private reconcileConversationsCacheFromDisk;
|
|
1297
|
+
private startBackgroundConversationReconcile;
|
|
1298
|
+
private shouldAutoReconcileConversationList;
|
|
1288
1299
|
private handleListConversations;
|
|
1289
1300
|
private handleConversationsCount;
|
|
1290
1301
|
private refreshCountInBackground;
|
package/dist/index.d.ts
CHANGED
|
@@ -1200,6 +1200,7 @@ declare class StreamerServer {
|
|
|
1200
1200
|
private allScanners;
|
|
1201
1201
|
private scannerReady;
|
|
1202
1202
|
private scannerStale;
|
|
1203
|
+
private conversationReconcileInFlight;
|
|
1203
1204
|
private refreshInFlight;
|
|
1204
1205
|
private binding;
|
|
1205
1206
|
private activeWarmups;
|
|
@@ -1285,6 +1286,16 @@ declare class StreamerServer {
|
|
|
1285
1286
|
private checkExchangeRateLimit;
|
|
1286
1287
|
private checkSessionStartRateLimit;
|
|
1287
1288
|
private checkSessionInputRateLimit;
|
|
1289
|
+
/** Project roots watched for conversation JSONLs (profiles or ~/.claude/projects). */
|
|
1290
|
+
private projectsDirsForFreshnessCheck;
|
|
1291
|
+
/**
|
|
1292
|
+
* Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by
|
|
1293
|
+
* the automatic freshness path when the directory watcher marked the scanner
|
|
1294
|
+
* stale or shouldRefreshProjectsFromHdd detected disk drift.
|
|
1295
|
+
*/
|
|
1296
|
+
private reconcileConversationsCacheFromDisk;
|
|
1297
|
+
private startBackgroundConversationReconcile;
|
|
1298
|
+
private shouldAutoReconcileConversationList;
|
|
1288
1299
|
private handleListConversations;
|
|
1289
1300
|
private handleConversationsCount;
|
|
1290
1301
|
private refreshCountInBackground;
|
package/dist/index.js
CHANGED
|
@@ -2684,14 +2684,14 @@ import {
|
|
|
2684
2684
|
createReadStream,
|
|
2685
2685
|
existsSync as existsSync10,
|
|
2686
2686
|
watch as fsWatch,
|
|
2687
|
-
readdirSync as
|
|
2687
|
+
readdirSync as readdirSync6,
|
|
2688
2688
|
readFileSync as readFileSync8,
|
|
2689
|
-
statSync as
|
|
2689
|
+
statSync as statSync9
|
|
2690
2690
|
} from "fs";
|
|
2691
2691
|
import { realpath as realpath2 } from "fs/promises";
|
|
2692
2692
|
import { createServer } from "http";
|
|
2693
|
-
import { homedir as
|
|
2694
|
-
import { basename as basename5, dirname as dirname9, join as
|
|
2693
|
+
import { homedir as homedir9 } from "os";
|
|
2694
|
+
import { basename as basename5, dirname as dirname9, join as join18 } from "path";
|
|
2695
2695
|
import { createInterface } from "readline";
|
|
2696
2696
|
|
|
2697
2697
|
// node_modules/nanoid/index.js
|
|
@@ -5420,6 +5420,14 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
5420
5420
|
};
|
|
5421
5421
|
}
|
|
5422
5422
|
|
|
5423
|
+
// src/services/cache/cacheMetadata.ts
|
|
5424
|
+
function getCacheMetadata(repo, key) {
|
|
5425
|
+
return repo.getCacheMetadata(key);
|
|
5426
|
+
}
|
|
5427
|
+
function setCacheMetadata(repo, key, value) {
|
|
5428
|
+
repo.setCacheMetadata(key, value);
|
|
5429
|
+
}
|
|
5430
|
+
|
|
5423
5431
|
// src/services/cache-integrity/cacheIntegrityMonitor.ts
|
|
5424
5432
|
import { createHash as createHash2 } from "crypto";
|
|
5425
5433
|
import { existsSync as existsSync8 } from "fs";
|
|
@@ -5960,6 +5968,140 @@ function pruneAgentConversations(cache) {
|
|
|
5960
5968
|
return { scanned: rows.length, pruned, missing };
|
|
5961
5969
|
}
|
|
5962
5970
|
|
|
5971
|
+
// src/utils/dates.ts
|
|
5972
|
+
import { compareDesc, isValid, parseISO } from "date-fns";
|
|
5973
|
+
function parseIsoDateOrNull(value) {
|
|
5974
|
+
if (!value) return null;
|
|
5975
|
+
const parsed = parseISO(value);
|
|
5976
|
+
return isValid(parsed) ? parsed : null;
|
|
5977
|
+
}
|
|
5978
|
+
function compareIsoDesc(a, b) {
|
|
5979
|
+
const dateA = parseIsoDateOrNull(a);
|
|
5980
|
+
const dateB = parseIsoDateOrNull(b);
|
|
5981
|
+
if (!dateA && !dateB) return 0;
|
|
5982
|
+
if (!dateA) return 1;
|
|
5983
|
+
if (!dateB) return -1;
|
|
5984
|
+
return compareDesc(dateA, dateB);
|
|
5985
|
+
}
|
|
5986
|
+
|
|
5987
|
+
// src/services/projects/ensureProjectsForConversations.ts
|
|
5988
|
+
function ensureProjectsForConversations(repo, conversations) {
|
|
5989
|
+
const conversationsByPath = /* @__PURE__ */ new Map();
|
|
5990
|
+
for (const conversation of conversations) {
|
|
5991
|
+
if (!conversation.projectPath) continue;
|
|
5992
|
+
const canonical = canonicalizeProjectPath(conversation.projectPath);
|
|
5993
|
+
if (!canonical) continue;
|
|
5994
|
+
const existing = conversationsByPath.get(canonical) ?? [];
|
|
5995
|
+
existing.push(conversation);
|
|
5996
|
+
conversationsByPath.set(canonical, existing);
|
|
5997
|
+
}
|
|
5998
|
+
const pathToProjectId = /* @__PURE__ */ new Map();
|
|
5999
|
+
for (const [path, projectConversations] of conversationsByPath) {
|
|
6000
|
+
const latest = pickLatestConversation(projectConversations);
|
|
6001
|
+
const project = repo.upsertProjectByPath(path, {
|
|
6002
|
+
lastConversationId: latest?.id ?? null,
|
|
6003
|
+
lastConversationCreatedAt: latest?.createdAt ?? null,
|
|
6004
|
+
latestMessageAt: latest?.latestMessageAt ?? null
|
|
6005
|
+
});
|
|
6006
|
+
pathToProjectId.set(path, project.id);
|
|
6007
|
+
}
|
|
6008
|
+
return pathToProjectId;
|
|
6009
|
+
}
|
|
6010
|
+
function pickLatestConversation(conversations) {
|
|
6011
|
+
if (conversations.length === 0) return void 0;
|
|
6012
|
+
return [...conversations].sort((a, b) => {
|
|
6013
|
+
const cmp = compareIsoDesc(a.latestMessageAt ?? null, b.latestMessageAt ?? null);
|
|
6014
|
+
if (cmp !== 0) return cmp;
|
|
6015
|
+
return compareIsoDesc(a.createdAt ?? null, b.createdAt ?? null);
|
|
6016
|
+
})[0];
|
|
6017
|
+
}
|
|
6018
|
+
|
|
6019
|
+
// src/services/conversations/refreshConversationCache.ts
|
|
6020
|
+
function refreshConversationCache(deps) {
|
|
6021
|
+
const { projectsRepo, conversationsRepo, cacheMetadataRepo } = deps;
|
|
6022
|
+
const conversations = conversationsRepo.listConversationsForProjectBackfill();
|
|
6023
|
+
const pathToProjectId = ensureProjectsForConversations(
|
|
6024
|
+
projectsRepo,
|
|
6025
|
+
conversations.map((c) => ({
|
|
6026
|
+
id: c.id,
|
|
6027
|
+
projectPath: c.projectPath,
|
|
6028
|
+
latestMessageAt: c.lastActivity ?? null,
|
|
6029
|
+
createdAt: c.lastActivity ?? null
|
|
6030
|
+
}))
|
|
6031
|
+
);
|
|
6032
|
+
let conversationsBackfilled = 0;
|
|
6033
|
+
for (const conversation of conversations) {
|
|
6034
|
+
if (!conversation.projectPath) continue;
|
|
6035
|
+
if (conversation.projectId) continue;
|
|
6036
|
+
const projectId = pathToProjectId.get(canonicalizeProjectPath(conversation.projectPath));
|
|
6037
|
+
if (!projectId) continue;
|
|
6038
|
+
conversationsRepo.updateConversationProjectId({
|
|
6039
|
+
conversationId: conversation.id,
|
|
6040
|
+
projectId
|
|
6041
|
+
});
|
|
6042
|
+
conversationsBackfilled += 1;
|
|
6043
|
+
}
|
|
6044
|
+
const latest = conversationsRepo.getLatestConversation();
|
|
6045
|
+
if (latest) {
|
|
6046
|
+
setCacheMetadata(cacheMetadataRepo, "last_conversation_id", latest.id);
|
|
6047
|
+
if (latest.lastActivity) {
|
|
6048
|
+
setCacheMetadata(cacheMetadataRepo, "last_conversation_created_at", latest.lastActivity);
|
|
6049
|
+
}
|
|
6050
|
+
}
|
|
6051
|
+
setCacheMetadata(cacheMetadataRepo, "conversations_last_indexed_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
6052
|
+
return {
|
|
6053
|
+
projectsTouched: pathToProjectId.size,
|
|
6054
|
+
conversationsBackfilled,
|
|
6055
|
+
latestConversationId: latest?.id ?? null
|
|
6056
|
+
};
|
|
6057
|
+
}
|
|
6058
|
+
|
|
6059
|
+
// src/services/conversations/shouldRefreshProjectsFromHdd.ts
|
|
6060
|
+
import { readdirSync as readdirSync5, statSync as statSync7 } from "fs";
|
|
6061
|
+
import { homedir as homedir8 } from "os";
|
|
6062
|
+
import { join as join16 } from "path";
|
|
6063
|
+
var DEFAULT_PROJECTS_DIR = join16(homedir8(), ".claude", "projects");
|
|
6064
|
+
function maxProjectsTreeMtimeMs(projectsDir) {
|
|
6065
|
+
let maxMs;
|
|
6066
|
+
try {
|
|
6067
|
+
maxMs = statSync7(projectsDir).mtimeMs;
|
|
6068
|
+
} catch {
|
|
6069
|
+
return null;
|
|
6070
|
+
}
|
|
6071
|
+
try {
|
|
6072
|
+
for (const ent of readdirSync5(projectsDir, { withFileTypes: true })) {
|
|
6073
|
+
if (!ent.isDirectory()) continue;
|
|
6074
|
+
try {
|
|
6075
|
+
const childMs = statSync7(join16(projectsDir, ent.name)).mtimeMs;
|
|
6076
|
+
if (childMs > maxMs) maxMs = childMs;
|
|
6077
|
+
} catch {
|
|
6078
|
+
}
|
|
6079
|
+
}
|
|
6080
|
+
} catch {
|
|
6081
|
+
}
|
|
6082
|
+
return maxMs;
|
|
6083
|
+
}
|
|
6084
|
+
function shouldRefreshProjectsFromHdd(conversationsRepo, cacheMetadataRepo, opts = {}) {
|
|
6085
|
+
if (conversationsRepo.hasOrphanRows()) return true;
|
|
6086
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
6087
|
+
if (opts.projectsDirs) {
|
|
6088
|
+
for (const d of opts.projectsDirs) dirs.add(d);
|
|
6089
|
+
}
|
|
6090
|
+
dirs.add(opts.projectsDir ?? DEFAULT_PROJECTS_DIR);
|
|
6091
|
+
let newestMs = null;
|
|
6092
|
+
for (const dir of dirs) {
|
|
6093
|
+
const ms = maxProjectsTreeMtimeMs(dir);
|
|
6094
|
+
if (ms === null) continue;
|
|
6095
|
+
if (newestMs === null || ms > newestMs) newestMs = ms;
|
|
6096
|
+
}
|
|
6097
|
+
if (newestMs === null) return false;
|
|
6098
|
+
const lastIndexedIso = getCacheMetadata(cacheMetadataRepo, "conversations_last_indexed_at");
|
|
6099
|
+
if (!lastIndexedIso) return true;
|
|
6100
|
+
const lastIndexedMs = Date.parse(lastIndexedIso);
|
|
6101
|
+
if (Number.isNaN(lastIndexedMs)) return true;
|
|
6102
|
+
return newestMs > lastIndexedMs;
|
|
6103
|
+
}
|
|
6104
|
+
|
|
5963
6105
|
// src/services/projectChats/deriveProjectChatTitle.ts
|
|
5964
6106
|
function deriveProjectChatTitle(input) {
|
|
5965
6107
|
const trimmed = input.title?.trim();
|
|
@@ -6121,7 +6263,7 @@ function resolveAnswer(pending, body) {
|
|
|
6121
6263
|
}
|
|
6122
6264
|
|
|
6123
6265
|
// src/services/sessions/conversationBusy.ts
|
|
6124
|
-
import { statSync as
|
|
6266
|
+
import { statSync as statSync8 } from "fs";
|
|
6125
6267
|
var RESUME_BUSY_WINDOW_MS = 12e4;
|
|
6126
6268
|
function resolveResumeBusyWindowMs(env = process.env) {
|
|
6127
6269
|
const raw = env.THREADBASE_RESUME_BUSY_WINDOW_MS;
|
|
@@ -6138,7 +6280,7 @@ function conversationBusy(input) {
|
|
|
6138
6280
|
let lastActivityMs = null;
|
|
6139
6281
|
if (input.jsonlPath) {
|
|
6140
6282
|
try {
|
|
6141
|
-
const mtimeMs =
|
|
6283
|
+
const mtimeMs = statSync8(input.jsonlPath).mtimeMs;
|
|
6142
6284
|
const age = now - mtimeMs;
|
|
6143
6285
|
lastActivityMs = Math.max(0, age);
|
|
6144
6286
|
const isSelfEcho = input.selfPtyEndedAt != null && mtimeMs <= input.selfPtyEndedAt + SELF_ACTIVITY_SKEW_MS;
|
|
@@ -6366,7 +6508,7 @@ function discoveredToResponse(d, conversationId) {
|
|
|
6366
6508
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
6367
6509
|
import { mkdir as mkdir3, writeFile } from "fs/promises";
|
|
6368
6510
|
import heicConvert from "heic-convert";
|
|
6369
|
-
import { join as
|
|
6511
|
+
import { join as join17 } from "path";
|
|
6370
6512
|
var UPLOAD_DIR_NAME = ".threadbase-uploads";
|
|
6371
6513
|
var MAX_BYTES = 25 * 1024 * 1024;
|
|
6372
6514
|
var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
|
|
@@ -6399,9 +6541,9 @@ async function saveUploadFile(input) {
|
|
|
6399
6541
|
}
|
|
6400
6542
|
const id = `up_${randomBytes3(8).toString("hex")}`;
|
|
6401
6543
|
const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
|
|
6402
|
-
const dir =
|
|
6544
|
+
const dir = join17(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
|
|
6403
6545
|
await mkdir3(dir, { recursive: true });
|
|
6404
|
-
const filePath =
|
|
6546
|
+
const filePath = join17(dir, `${Date.now()}-${id}-${safeName}`);
|
|
6405
6547
|
await writeFile(filePath, buffer);
|
|
6406
6548
|
return {
|
|
6407
6549
|
id,
|
|
@@ -6511,14 +6653,6 @@ function computeConversationEtag({
|
|
|
6511
6653
|
return `"${digest}"`;
|
|
6512
6654
|
}
|
|
6513
6655
|
|
|
6514
|
-
// src/utils/dates.ts
|
|
6515
|
-
import { compareDesc, isValid, parseISO } from "date-fns";
|
|
6516
|
-
function parseIsoDateOrNull(value) {
|
|
6517
|
-
if (!value) return null;
|
|
6518
|
-
const parsed = parseISO(value);
|
|
6519
|
-
return isValid(parsed) ? parsed : null;
|
|
6520
|
-
}
|
|
6521
|
-
|
|
6522
6656
|
// src/utils/isScannedSnapshotStale.ts
|
|
6523
6657
|
var STALENESS_TOLERANCE_MS = 1e3;
|
|
6524
6658
|
function isScannedSnapshotStale(snapshotTimestamp, fileMtimeMs) {
|
|
@@ -6729,6 +6863,10 @@ var StreamerServer = class {
|
|
|
6729
6863
|
// Set by onConversationChanged while a scan is in-flight; getScanner() does
|
|
6730
6864
|
// a single rescan after the current one completes instead of restarting it.
|
|
6731
6865
|
scannerStale = false;
|
|
6866
|
+
// Single-flight guard for the background disk reconcile: a burst of list
|
|
6867
|
+
// polls during active session writes shares one rescan instead of queueing
|
|
6868
|
+
// a full rescan per request.
|
|
6869
|
+
conversationReconcileInFlight = null;
|
|
6732
6870
|
// Single-flight + TTL guard around scanner.refreshFile (see refreshFileGuarded).
|
|
6733
6871
|
// A live file's mtime is always newer than the snapshot, so an unguarded
|
|
6734
6872
|
// refresh fires on every request and re-parses the whole file from byte 0.
|
|
@@ -6819,13 +6957,13 @@ var StreamerServer = class {
|
|
|
6819
6957
|
this.disableDb = config.disableDb ?? false;
|
|
6820
6958
|
this.scannerPersistenceDisabled = config.scannerPersistent === false;
|
|
6821
6959
|
this.scanProfiles = config.scanProfiles;
|
|
6822
|
-
this.codexRoots = config.codexRoots ?? [
|
|
6960
|
+
this.codexRoots = config.codexRoots ?? [join18(homedir9(), ".codex", "sessions")];
|
|
6823
6961
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
6824
6962
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
6825
6963
|
this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
|
|
6826
6964
|
this.defaultModel = config.defaultModel ?? "sonnet";
|
|
6827
6965
|
this.defaultEffort = config.defaultEffort ?? "low";
|
|
6828
|
-
this.cacheDir = config.cacheDir ?? loadCacheDir() ??
|
|
6966
|
+
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? join18(homedir9(), ".threadbase", "cache");
|
|
6829
6967
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
6830
6968
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
6831
6969
|
this.markScannerStaleDebounced = debounce(() => {
|
|
@@ -6866,7 +7004,7 @@ var StreamerServer = class {
|
|
|
6866
7004
|
const seqs = cache.extendMessageIndex(
|
|
6867
7005
|
filePath,
|
|
6868
7006
|
spans,
|
|
6869
|
-
|
|
7007
|
+
statSync9(filePath),
|
|
6870
7008
|
readFrom,
|
|
6871
7009
|
endOffset
|
|
6872
7010
|
);
|
|
@@ -7032,7 +7170,7 @@ var StreamerServer = class {
|
|
|
7032
7170
|
temporalClient,
|
|
7033
7171
|
taskQueue: agentConfig.temporal.taskQueue
|
|
7034
7172
|
});
|
|
7035
|
-
const conversationsBaseDir = agentConfig.conversationsDir ||
|
|
7173
|
+
const conversationsBaseDir = agentConfig.conversationsDir || join18(dirname9(this.cacheDir), "conversations");
|
|
7036
7174
|
conversationWriter = createConversationWriter({
|
|
7037
7175
|
baseDir: conversationsBaseDir
|
|
7038
7176
|
});
|
|
@@ -7355,7 +7493,7 @@ var StreamerServer = class {
|
|
|
7355
7493
|
});
|
|
7356
7494
|
try {
|
|
7357
7495
|
this.cache = ConversationCache.open(
|
|
7358
|
-
|
|
7496
|
+
join18(this.cacheDir, "cache.db"),
|
|
7359
7497
|
this.tailSize,
|
|
7360
7498
|
void 0,
|
|
7361
7499
|
{
|
|
@@ -7481,6 +7619,20 @@ var StreamerServer = class {
|
|
|
7481
7619
|
count: pruned.length,
|
|
7482
7620
|
event: "cache.prune_ghosts"
|
|
7483
7621
|
});
|
|
7622
|
+
if (this.projectsRepo && this.conversationsRepo && this.cacheMetadataRepo) {
|
|
7623
|
+
refreshConversationCache({
|
|
7624
|
+
cache: this.cache,
|
|
7625
|
+
projectsRepo: this.projectsRepo,
|
|
7626
|
+
conversationsRepo: this.conversationsRepo,
|
|
7627
|
+
cacheMetadataRepo: this.cacheMetadataRepo
|
|
7628
|
+
});
|
|
7629
|
+
} else if (this.cacheMetadataRepo) {
|
|
7630
|
+
setCacheMetadata(
|
|
7631
|
+
this.cacheMetadataRepo,
|
|
7632
|
+
"conversations_last_indexed_at",
|
|
7633
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
7634
|
+
);
|
|
7635
|
+
}
|
|
7484
7636
|
}
|
|
7485
7637
|
}).catch((err) => {
|
|
7486
7638
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -7720,6 +7872,68 @@ var StreamerServer = class {
|
|
|
7720
7872
|
checkSessionInputRateLimit(sessionId) {
|
|
7721
7873
|
return this.checkRateLimit(this.sessionInputAttempts, sessionId, 500, 6e4);
|
|
7722
7874
|
}
|
|
7875
|
+
/** Project roots watched for conversation JSONLs (profiles or ~/.claude/projects). */
|
|
7876
|
+
projectsDirsForFreshnessCheck() {
|
|
7877
|
+
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
7878
|
+
return this.scanProfiles.filter((p) => p.enabled).map((p) => join18(p.configDir, "projects"));
|
|
7879
|
+
}
|
|
7880
|
+
return [join18(homedir9(), ".claude", "projects")];
|
|
7881
|
+
}
|
|
7882
|
+
/**
|
|
7883
|
+
* Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by
|
|
7884
|
+
* the automatic freshness path when the directory watcher marked the scanner
|
|
7885
|
+
* stale or shouldRefreshProjectsFromHdd detected disk drift.
|
|
7886
|
+
*/
|
|
7887
|
+
async reconcileConversationsCacheFromDisk(onProgress) {
|
|
7888
|
+
if (!this.cache) return;
|
|
7889
|
+
const scanner = await this.rescanForRefresh(onProgress);
|
|
7890
|
+
const metas = [...scanner.getMetadataCache().values()];
|
|
7891
|
+
try {
|
|
7892
|
+
this.cache.upsertFromScannerMeta(metas);
|
|
7893
|
+
if (!this.cacheMonitor?.pending) {
|
|
7894
|
+
this.cache.reconcileDeletions(canonicalLivePathSet(metas));
|
|
7895
|
+
}
|
|
7896
|
+
if (this.projectsRepo && this.conversationsRepo && this.cacheMetadataRepo) {
|
|
7897
|
+
refreshConversationCache({
|
|
7898
|
+
cache: this.cache,
|
|
7899
|
+
projectsRepo: this.projectsRepo,
|
|
7900
|
+
conversationsRepo: this.conversationsRepo,
|
|
7901
|
+
cacheMetadataRepo: this.cacheMetadataRepo
|
|
7902
|
+
});
|
|
7903
|
+
} else if (this.cacheMetadataRepo) {
|
|
7904
|
+
setCacheMetadata(
|
|
7905
|
+
this.cacheMetadataRepo,
|
|
7906
|
+
"conversations_last_indexed_at",
|
|
7907
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
7908
|
+
);
|
|
7909
|
+
}
|
|
7910
|
+
} catch (err) {
|
|
7911
|
+
this.log.warn(
|
|
7912
|
+
`refresh reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
7913
|
+
{ event: "conversations.reconcile_failed" }
|
|
7914
|
+
);
|
|
7915
|
+
}
|
|
7916
|
+
}
|
|
7917
|
+
// Reconcile the cache from disk without blocking the caller. Single-flighted
|
|
7918
|
+
// so a burst of list polls during active session writes shares one rescan
|
|
7919
|
+
// rather than queueing a full rescan each; tracked so close() awaits the
|
|
7920
|
+
// in-flight cache write before shutting the DB.
|
|
7921
|
+
startBackgroundConversationReconcile() {
|
|
7922
|
+
if (this.conversationReconcileInFlight) return;
|
|
7923
|
+
const task = this.reconcileConversationsCacheFromDisk().finally(() => {
|
|
7924
|
+
this.conversationReconcileInFlight = null;
|
|
7925
|
+
});
|
|
7926
|
+
this.conversationReconcileInFlight = task;
|
|
7927
|
+
this.trackCacheWrite(task);
|
|
7928
|
+
}
|
|
7929
|
+
shouldAutoReconcileConversationList() {
|
|
7930
|
+
if (!this.cache) return false;
|
|
7931
|
+
if (this.scannerStale) return true;
|
|
7932
|
+
if (!this.conversationsRepo || !this.cacheMetadataRepo) return false;
|
|
7933
|
+
return shouldRefreshProjectsFromHdd(this.conversationsRepo, this.cacheMetadataRepo, {
|
|
7934
|
+
projectsDirs: this.projectsDirsForFreshnessCheck()
|
|
7935
|
+
});
|
|
7936
|
+
}
|
|
7723
7937
|
async handleListConversations(url, res) {
|
|
7724
7938
|
if (this.rejectIfWarmingUp(res)) return;
|
|
7725
7939
|
const limit = intParam(url, "limit", 50);
|
|
@@ -7728,18 +7942,19 @@ var StreamerServer = class {
|
|
|
7728
7942
|
const project = url.searchParams.get("project") ?? void 0;
|
|
7729
7943
|
const providerFilter = url.searchParams.get("provider") ?? void 0;
|
|
7730
7944
|
const bustCache = url.searchParams.get("refresh") === "1";
|
|
7731
|
-
if (
|
|
7732
|
-
const
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7945
|
+
if (this.cache && (bustCache || this.shouldAutoReconcileConversationList())) {
|
|
7946
|
+
const canServeStale = !bustCache && this.cache.listConversations({ limit: 0, offset: 0 }).total > 0;
|
|
7947
|
+
if (canServeStale) {
|
|
7948
|
+
this.startBackgroundConversationReconcile();
|
|
7949
|
+
} else {
|
|
7950
|
+
const shouldEmitProgress = createScanProgressThrottle();
|
|
7951
|
+
await this.withWarmup(
|
|
7952
|
+
"conversation_refresh",
|
|
7953
|
+
() => this.reconcileConversationsCacheFromDisk((scanned, total2) => {
|
|
7954
|
+
if (shouldEmitProgress(scanned, total2)) {
|
|
7955
|
+
this.wsHub.broadcast({ type: "scan_progress", scanned, total: total2 });
|
|
7956
|
+
}
|
|
7957
|
+
})
|
|
7743
7958
|
);
|
|
7744
7959
|
}
|
|
7745
7960
|
}
|
|
@@ -7976,7 +8191,7 @@ var StreamerServer = class {
|
|
|
7976
8191
|
// getFreshScanner() this does NOT discard the warm scanner. scannerReady is
|
|
7977
8192
|
// only ever reassigned to a live scan promise (never nulled mid-scan), so the
|
|
7978
8193
|
// getScanner() anti-infinite-loop guard is preserved.
|
|
7979
|
-
async rescanForRefresh() {
|
|
8194
|
+
async rescanForRefresh(onProgress) {
|
|
7980
8195
|
if (this.scannerReady) await this.scannerReady;
|
|
7981
8196
|
this.scannerStale = false;
|
|
7982
8197
|
if (!this.scanner) {
|
|
@@ -7987,7 +8202,8 @@ var StreamerServer = class {
|
|
|
7987
8202
|
this.scannerReady = scanner.scan({
|
|
7988
8203
|
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
7989
8204
|
...this.codexScanOpts(),
|
|
7990
|
-
fullRescan: true
|
|
8205
|
+
fullRescan: true,
|
|
8206
|
+
...onProgress ? { onProgress } : {}
|
|
7991
8207
|
});
|
|
7992
8208
|
await this.scannerReady;
|
|
7993
8209
|
return scanner;
|
|
@@ -8002,21 +8218,21 @@ var StreamerServer = class {
|
|
|
8002
8218
|
*/
|
|
8003
8219
|
projectsDirs() {
|
|
8004
8220
|
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
8005
|
-
return this.scanProfiles.filter((p) => p.enabled).map((p) =>
|
|
8221
|
+
return this.scanProfiles.filter((p) => p.enabled).map((p) => join18(p.configDir, "projects"));
|
|
8006
8222
|
}
|
|
8007
|
-
return [
|
|
8223
|
+
return [join18(homedir9(), ".claude", "projects")];
|
|
8008
8224
|
}
|
|
8009
8225
|
findJsonlPath(uuid) {
|
|
8010
8226
|
const filename = `${uuid}.jsonl`;
|
|
8011
8227
|
for (const projectsDir of this.projectsDirs()) {
|
|
8012
8228
|
if (!existsSync10(projectsDir)) continue;
|
|
8013
|
-
for (const dir of
|
|
8014
|
-
const fp =
|
|
8229
|
+
for (const dir of readdirSync6(projectsDir)) {
|
|
8230
|
+
const fp = join18(projectsDir, dir, filename);
|
|
8015
8231
|
if (existsSync10(fp)) return fp;
|
|
8016
|
-
const projectDir =
|
|
8232
|
+
const projectDir = join18(projectsDir, dir);
|
|
8017
8233
|
try {
|
|
8018
|
-
for (const sub of
|
|
8019
|
-
const subagentPath =
|
|
8234
|
+
for (const sub of readdirSync6(projectDir)) {
|
|
8235
|
+
const subagentPath = join18(projectDir, sub, "subagents", filename);
|
|
8020
8236
|
if (existsSync10(subagentPath)) return subagentPath;
|
|
8021
8237
|
}
|
|
8022
8238
|
} catch {
|
|
@@ -8117,7 +8333,7 @@ var StreamerServer = class {
|
|
|
8117
8333
|
if (this.isManagedTailPath(key)) return;
|
|
8118
8334
|
let mtimeMs;
|
|
8119
8335
|
try {
|
|
8120
|
-
mtimeMs =
|
|
8336
|
+
mtimeMs = statSync9(filePath).mtimeMs;
|
|
8121
8337
|
} catch {
|
|
8122
8338
|
return;
|
|
8123
8339
|
}
|
|
@@ -8299,7 +8515,7 @@ var StreamerServer = class {
|
|
|
8299
8515
|
if (!conv.filePath) return false;
|
|
8300
8516
|
let mtimeMs = null;
|
|
8301
8517
|
try {
|
|
8302
|
-
mtimeMs =
|
|
8518
|
+
mtimeMs = statSync9(conv.filePath).mtimeMs;
|
|
8303
8519
|
} catch {
|
|
8304
8520
|
return false;
|
|
8305
8521
|
}
|
|
@@ -9219,7 +9435,7 @@ var StreamerServer = class {
|
|
|
9219
9435
|
sessionStore: this.sessionStore,
|
|
9220
9436
|
// biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
|
|
9221
9437
|
agentClient: this.agentClient,
|
|
9222
|
-
conversationsDir: this.cacheDir ?
|
|
9438
|
+
conversationsDir: this.cacheDir ? join18(dirname9(this.cacheDir), "conversations") : "",
|
|
9223
9439
|
agentConfig: this.agentConfig
|
|
9224
9440
|
});
|
|
9225
9441
|
json(res, result.status, result.body);
|
|
@@ -9378,9 +9594,9 @@ var StreamerServer = class {
|
|
|
9378
9594
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
9379
9595
|
watchForJsonl(sessionId, projectPath) {
|
|
9380
9596
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
9381
|
-
const projectsDir =
|
|
9597
|
+
const projectsDir = join18(homedir9(), ".claude", "projects", encoded);
|
|
9382
9598
|
const expectedFile = `${sessionId}.jsonl`;
|
|
9383
|
-
const filePath =
|
|
9599
|
+
const filePath = join18(projectsDir, expectedFile);
|
|
9384
9600
|
const deadline = Date.now() + 12e4;
|
|
9385
9601
|
let watcher = null;
|
|
9386
9602
|
const cleanup = () => {
|
|
@@ -9402,10 +9618,10 @@ var StreamerServer = class {
|
|
|
9402
9618
|
if (!resolvedFilePath && existsSync10(projectsDir)) {
|
|
9403
9619
|
try {
|
|
9404
9620
|
const now = Date.now();
|
|
9405
|
-
const match =
|
|
9406
|
-
({ f }) => basename5(f, ".jsonl") === sessionId || this.readFirstLineSessionId(
|
|
9621
|
+
const match = readdirSync6(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync9(join18(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
|
|
9622
|
+
({ f }) => basename5(f, ".jsonl") === sessionId || this.readFirstLineSessionId(join18(projectsDir, f)) === sessionId
|
|
9407
9623
|
).sort((a, b) => b.mtime - a.mtime)[0];
|
|
9408
|
-
if (match) resolvedFilePath =
|
|
9624
|
+
if (match) resolvedFilePath = join18(projectsDir, match.f);
|
|
9409
9625
|
} catch {
|
|
9410
9626
|
}
|
|
9411
9627
|
}
|
|
@@ -9453,7 +9669,7 @@ var StreamerServer = class {
|
|
|
9453
9669
|
watchForCodexRollout(sessionId, projectPath) {
|
|
9454
9670
|
const deadline = Date.now() + 12e4;
|
|
9455
9671
|
const now = /* @__PURE__ */ new Date();
|
|
9456
|
-
const dateDir =
|
|
9672
|
+
const dateDir = join18(
|
|
9457
9673
|
String(now.getFullYear()),
|
|
9458
9674
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
9459
9675
|
String(now.getDate()).padStart(2, "0")
|
|
@@ -9494,18 +9710,18 @@ var StreamerServer = class {
|
|
|
9494
9710
|
this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
|
|
9495
9711
|
);
|
|
9496
9712
|
for (const root of this.codexRoots) {
|
|
9497
|
-
const sessionsDir =
|
|
9713
|
+
const sessionsDir = join18(root, dateDir);
|
|
9498
9714
|
if (!existsSync10(sessionsDir)) continue;
|
|
9499
9715
|
let candidateFiles;
|
|
9500
9716
|
try {
|
|
9501
|
-
candidateFiles =
|
|
9717
|
+
candidateFiles = readdirSync6(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
9502
9718
|
} catch {
|
|
9503
9719
|
continue;
|
|
9504
9720
|
}
|
|
9505
9721
|
const nowMs = Date.now();
|
|
9506
|
-
const recentCandidates = candidateFiles.map((f) => ({ f, mtime:
|
|
9722
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: statSync9(join18(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
9507
9723
|
for (const { f } of recentCandidates) {
|
|
9508
|
-
const candidatePath =
|
|
9724
|
+
const candidatePath = join18(sessionsDir, f);
|
|
9509
9725
|
const match = matchesProjectPath(candidatePath);
|
|
9510
9726
|
if (!match) continue;
|
|
9511
9727
|
if (boundElsewhere.has(match.id)) continue;
|