@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.cjs
CHANGED
|
@@ -2725,11 +2725,11 @@ var import_node_ws = require("@hono/node-ws");
|
|
|
2725
2725
|
var import_client = require("@temporalio/client");
|
|
2726
2726
|
var import_scanner3 = require("@threadbase-sh/scanner");
|
|
2727
2727
|
var import_events = require("events");
|
|
2728
|
-
var
|
|
2728
|
+
var import_fs18 = require("fs");
|
|
2729
2729
|
var import_promises7 = require("fs/promises");
|
|
2730
2730
|
var import_http = require("http");
|
|
2731
|
-
var
|
|
2732
|
-
var
|
|
2731
|
+
var import_os9 = require("os");
|
|
2732
|
+
var import_path18 = require("path");
|
|
2733
2733
|
var import_readline = require("readline");
|
|
2734
2734
|
|
|
2735
2735
|
// node_modules/nanoid/index.js
|
|
@@ -5456,6 +5456,14 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
5456
5456
|
};
|
|
5457
5457
|
}
|
|
5458
5458
|
|
|
5459
|
+
// src/services/cache/cacheMetadata.ts
|
|
5460
|
+
function getCacheMetadata(repo, key) {
|
|
5461
|
+
return repo.getCacheMetadata(key);
|
|
5462
|
+
}
|
|
5463
|
+
function setCacheMetadata(repo, key, value) {
|
|
5464
|
+
repo.setCacheMetadata(key, value);
|
|
5465
|
+
}
|
|
5466
|
+
|
|
5459
5467
|
// src/services/cache-integrity/cacheIntegrityMonitor.ts
|
|
5460
5468
|
var import_crypto8 = require("crypto");
|
|
5461
5469
|
var import_fs13 = require("fs");
|
|
@@ -5996,6 +6004,140 @@ function pruneAgentConversations(cache) {
|
|
|
5996
6004
|
return { scanned: rows.length, pruned, missing };
|
|
5997
6005
|
}
|
|
5998
6006
|
|
|
6007
|
+
// src/utils/dates.ts
|
|
6008
|
+
var import_date_fns = require("date-fns");
|
|
6009
|
+
function parseIsoDateOrNull(value) {
|
|
6010
|
+
if (!value) return null;
|
|
6011
|
+
const parsed = (0, import_date_fns.parseISO)(value);
|
|
6012
|
+
return (0, import_date_fns.isValid)(parsed) ? parsed : null;
|
|
6013
|
+
}
|
|
6014
|
+
function compareIsoDesc(a, b) {
|
|
6015
|
+
const dateA = parseIsoDateOrNull(a);
|
|
6016
|
+
const dateB = parseIsoDateOrNull(b);
|
|
6017
|
+
if (!dateA && !dateB) return 0;
|
|
6018
|
+
if (!dateA) return 1;
|
|
6019
|
+
if (!dateB) return -1;
|
|
6020
|
+
return (0, import_date_fns.compareDesc)(dateA, dateB);
|
|
6021
|
+
}
|
|
6022
|
+
|
|
6023
|
+
// src/services/projects/ensureProjectsForConversations.ts
|
|
6024
|
+
function ensureProjectsForConversations(repo, conversations) {
|
|
6025
|
+
const conversationsByPath = /* @__PURE__ */ new Map();
|
|
6026
|
+
for (const conversation of conversations) {
|
|
6027
|
+
if (!conversation.projectPath) continue;
|
|
6028
|
+
const canonical = canonicalizeProjectPath(conversation.projectPath);
|
|
6029
|
+
if (!canonical) continue;
|
|
6030
|
+
const existing = conversationsByPath.get(canonical) ?? [];
|
|
6031
|
+
existing.push(conversation);
|
|
6032
|
+
conversationsByPath.set(canonical, existing);
|
|
6033
|
+
}
|
|
6034
|
+
const pathToProjectId = /* @__PURE__ */ new Map();
|
|
6035
|
+
for (const [path, projectConversations] of conversationsByPath) {
|
|
6036
|
+
const latest = pickLatestConversation(projectConversations);
|
|
6037
|
+
const project = repo.upsertProjectByPath(path, {
|
|
6038
|
+
lastConversationId: latest?.id ?? null,
|
|
6039
|
+
lastConversationCreatedAt: latest?.createdAt ?? null,
|
|
6040
|
+
latestMessageAt: latest?.latestMessageAt ?? null
|
|
6041
|
+
});
|
|
6042
|
+
pathToProjectId.set(path, project.id);
|
|
6043
|
+
}
|
|
6044
|
+
return pathToProjectId;
|
|
6045
|
+
}
|
|
6046
|
+
function pickLatestConversation(conversations) {
|
|
6047
|
+
if (conversations.length === 0) return void 0;
|
|
6048
|
+
return [...conversations].sort((a, b) => {
|
|
6049
|
+
const cmp = compareIsoDesc(a.latestMessageAt ?? null, b.latestMessageAt ?? null);
|
|
6050
|
+
if (cmp !== 0) return cmp;
|
|
6051
|
+
return compareIsoDesc(a.createdAt ?? null, b.createdAt ?? null);
|
|
6052
|
+
})[0];
|
|
6053
|
+
}
|
|
6054
|
+
|
|
6055
|
+
// src/services/conversations/refreshConversationCache.ts
|
|
6056
|
+
function refreshConversationCache(deps) {
|
|
6057
|
+
const { projectsRepo, conversationsRepo, cacheMetadataRepo } = deps;
|
|
6058
|
+
const conversations = conversationsRepo.listConversationsForProjectBackfill();
|
|
6059
|
+
const pathToProjectId = ensureProjectsForConversations(
|
|
6060
|
+
projectsRepo,
|
|
6061
|
+
conversations.map((c) => ({
|
|
6062
|
+
id: c.id,
|
|
6063
|
+
projectPath: c.projectPath,
|
|
6064
|
+
latestMessageAt: c.lastActivity ?? null,
|
|
6065
|
+
createdAt: c.lastActivity ?? null
|
|
6066
|
+
}))
|
|
6067
|
+
);
|
|
6068
|
+
let conversationsBackfilled = 0;
|
|
6069
|
+
for (const conversation of conversations) {
|
|
6070
|
+
if (!conversation.projectPath) continue;
|
|
6071
|
+
if (conversation.projectId) continue;
|
|
6072
|
+
const projectId = pathToProjectId.get(canonicalizeProjectPath(conversation.projectPath));
|
|
6073
|
+
if (!projectId) continue;
|
|
6074
|
+
conversationsRepo.updateConversationProjectId({
|
|
6075
|
+
conversationId: conversation.id,
|
|
6076
|
+
projectId
|
|
6077
|
+
});
|
|
6078
|
+
conversationsBackfilled += 1;
|
|
6079
|
+
}
|
|
6080
|
+
const latest = conversationsRepo.getLatestConversation();
|
|
6081
|
+
if (latest) {
|
|
6082
|
+
setCacheMetadata(cacheMetadataRepo, "last_conversation_id", latest.id);
|
|
6083
|
+
if (latest.lastActivity) {
|
|
6084
|
+
setCacheMetadata(cacheMetadataRepo, "last_conversation_created_at", latest.lastActivity);
|
|
6085
|
+
}
|
|
6086
|
+
}
|
|
6087
|
+
setCacheMetadata(cacheMetadataRepo, "conversations_last_indexed_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
6088
|
+
return {
|
|
6089
|
+
projectsTouched: pathToProjectId.size,
|
|
6090
|
+
conversationsBackfilled,
|
|
6091
|
+
latestConversationId: latest?.id ?? null
|
|
6092
|
+
};
|
|
6093
|
+
}
|
|
6094
|
+
|
|
6095
|
+
// src/services/conversations/shouldRefreshProjectsFromHdd.ts
|
|
6096
|
+
var import_fs16 = require("fs");
|
|
6097
|
+
var import_os8 = require("os");
|
|
6098
|
+
var import_path16 = require("path");
|
|
6099
|
+
var DEFAULT_PROJECTS_DIR = (0, import_path16.join)((0, import_os8.homedir)(), ".claude", "projects");
|
|
6100
|
+
function maxProjectsTreeMtimeMs(projectsDir) {
|
|
6101
|
+
let maxMs;
|
|
6102
|
+
try {
|
|
6103
|
+
maxMs = (0, import_fs16.statSync)(projectsDir).mtimeMs;
|
|
6104
|
+
} catch {
|
|
6105
|
+
return null;
|
|
6106
|
+
}
|
|
6107
|
+
try {
|
|
6108
|
+
for (const ent of (0, import_fs16.readdirSync)(projectsDir, { withFileTypes: true })) {
|
|
6109
|
+
if (!ent.isDirectory()) continue;
|
|
6110
|
+
try {
|
|
6111
|
+
const childMs = (0, import_fs16.statSync)((0, import_path16.join)(projectsDir, ent.name)).mtimeMs;
|
|
6112
|
+
if (childMs > maxMs) maxMs = childMs;
|
|
6113
|
+
} catch {
|
|
6114
|
+
}
|
|
6115
|
+
}
|
|
6116
|
+
} catch {
|
|
6117
|
+
}
|
|
6118
|
+
return maxMs;
|
|
6119
|
+
}
|
|
6120
|
+
function shouldRefreshProjectsFromHdd(conversationsRepo, cacheMetadataRepo, opts = {}) {
|
|
6121
|
+
if (conversationsRepo.hasOrphanRows()) return true;
|
|
6122
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
6123
|
+
if (opts.projectsDirs) {
|
|
6124
|
+
for (const d of opts.projectsDirs) dirs.add(d);
|
|
6125
|
+
}
|
|
6126
|
+
dirs.add(opts.projectsDir ?? DEFAULT_PROJECTS_DIR);
|
|
6127
|
+
let newestMs = null;
|
|
6128
|
+
for (const dir of dirs) {
|
|
6129
|
+
const ms = maxProjectsTreeMtimeMs(dir);
|
|
6130
|
+
if (ms === null) continue;
|
|
6131
|
+
if (newestMs === null || ms > newestMs) newestMs = ms;
|
|
6132
|
+
}
|
|
6133
|
+
if (newestMs === null) return false;
|
|
6134
|
+
const lastIndexedIso = getCacheMetadata(cacheMetadataRepo, "conversations_last_indexed_at");
|
|
6135
|
+
if (!lastIndexedIso) return true;
|
|
6136
|
+
const lastIndexedMs = Date.parse(lastIndexedIso);
|
|
6137
|
+
if (Number.isNaN(lastIndexedMs)) return true;
|
|
6138
|
+
return newestMs > lastIndexedMs;
|
|
6139
|
+
}
|
|
6140
|
+
|
|
5999
6141
|
// src/services/projectChats/deriveProjectChatTitle.ts
|
|
6000
6142
|
function deriveProjectChatTitle(input) {
|
|
6001
6143
|
const trimmed = input.title?.trim();
|
|
@@ -6157,7 +6299,7 @@ function resolveAnswer(pending, body) {
|
|
|
6157
6299
|
}
|
|
6158
6300
|
|
|
6159
6301
|
// src/services/sessions/conversationBusy.ts
|
|
6160
|
-
var
|
|
6302
|
+
var import_fs17 = require("fs");
|
|
6161
6303
|
var RESUME_BUSY_WINDOW_MS = 12e4;
|
|
6162
6304
|
function resolveResumeBusyWindowMs(env = process.env) {
|
|
6163
6305
|
const raw = env.THREADBASE_RESUME_BUSY_WINDOW_MS;
|
|
@@ -6174,7 +6316,7 @@ function conversationBusy(input) {
|
|
|
6174
6316
|
let lastActivityMs = null;
|
|
6175
6317
|
if (input.jsonlPath) {
|
|
6176
6318
|
try {
|
|
6177
|
-
const mtimeMs = (0,
|
|
6319
|
+
const mtimeMs = (0, import_fs17.statSync)(input.jsonlPath).mtimeMs;
|
|
6178
6320
|
const age = now - mtimeMs;
|
|
6179
6321
|
lastActivityMs = Math.max(0, age);
|
|
6180
6322
|
const isSelfEcho = input.selfPtyEndedAt != null && mtimeMs <= input.selfPtyEndedAt + SELF_ACTIVITY_SKEW_MS;
|
|
@@ -6402,7 +6544,7 @@ function discoveredToResponse(d, conversationId) {
|
|
|
6402
6544
|
var import_crypto9 = require("crypto");
|
|
6403
6545
|
var import_promises6 = require("fs/promises");
|
|
6404
6546
|
var import_heic_convert = __toESM(require("heic-convert"), 1);
|
|
6405
|
-
var
|
|
6547
|
+
var import_path17 = require("path");
|
|
6406
6548
|
var UPLOAD_DIR_NAME = ".threadbase-uploads";
|
|
6407
6549
|
var MAX_BYTES = 25 * 1024 * 1024;
|
|
6408
6550
|
var HEIC_MIMES = /* @__PURE__ */ new Set(["image/heic", "image/heif"]);
|
|
@@ -6435,9 +6577,9 @@ async function saveUploadFile(input) {
|
|
|
6435
6577
|
}
|
|
6436
6578
|
const id = `up_${(0, import_crypto9.randomBytes)(8).toString("hex")}`;
|
|
6437
6579
|
const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
|
|
6438
|
-
const dir = (0,
|
|
6580
|
+
const dir = (0, import_path17.join)(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
|
|
6439
6581
|
await (0, import_promises6.mkdir)(dir, { recursive: true });
|
|
6440
|
-
const filePath = (0,
|
|
6582
|
+
const filePath = (0, import_path17.join)(dir, `${Date.now()}-${id}-${safeName}`);
|
|
6441
6583
|
await (0, import_promises6.writeFile)(filePath, buffer);
|
|
6442
6584
|
return {
|
|
6443
6585
|
id,
|
|
@@ -6547,14 +6689,6 @@ function computeConversationEtag({
|
|
|
6547
6689
|
return `"${digest}"`;
|
|
6548
6690
|
}
|
|
6549
6691
|
|
|
6550
|
-
// src/utils/dates.ts
|
|
6551
|
-
var import_date_fns = require("date-fns");
|
|
6552
|
-
function parseIsoDateOrNull(value) {
|
|
6553
|
-
if (!value) return null;
|
|
6554
|
-
const parsed = (0, import_date_fns.parseISO)(value);
|
|
6555
|
-
return (0, import_date_fns.isValid)(parsed) ? parsed : null;
|
|
6556
|
-
}
|
|
6557
|
-
|
|
6558
6692
|
// src/utils/isScannedSnapshotStale.ts
|
|
6559
6693
|
var STALENESS_TOLERANCE_MS = 1e3;
|
|
6560
6694
|
function isScannedSnapshotStale(snapshotTimestamp, fileMtimeMs) {
|
|
@@ -6765,6 +6899,10 @@ var StreamerServer = class {
|
|
|
6765
6899
|
// Set by onConversationChanged while a scan is in-flight; getScanner() does
|
|
6766
6900
|
// a single rescan after the current one completes instead of restarting it.
|
|
6767
6901
|
scannerStale = false;
|
|
6902
|
+
// Single-flight guard for the background disk reconcile: a burst of list
|
|
6903
|
+
// polls during active session writes shares one rescan instead of queueing
|
|
6904
|
+
// a full rescan per request.
|
|
6905
|
+
conversationReconcileInFlight = null;
|
|
6768
6906
|
// Single-flight + TTL guard around scanner.refreshFile (see refreshFileGuarded).
|
|
6769
6907
|
// A live file's mtime is always newer than the snapshot, so an unguarded
|
|
6770
6908
|
// refresh fires on every request and re-parses the whole file from byte 0.
|
|
@@ -6855,13 +6993,13 @@ var StreamerServer = class {
|
|
|
6855
6993
|
this.disableDb = config.disableDb ?? false;
|
|
6856
6994
|
this.scannerPersistenceDisabled = config.scannerPersistent === false;
|
|
6857
6995
|
this.scanProfiles = config.scanProfiles;
|
|
6858
|
-
this.codexRoots = config.codexRoots ?? [(0,
|
|
6996
|
+
this.codexRoots = config.codexRoots ?? [(0, import_path18.join)((0, import_os9.homedir)(), ".codex", "sessions")];
|
|
6859
6997
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
6860
6998
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
6861
6999
|
this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
|
|
6862
7000
|
this.defaultModel = config.defaultModel ?? "sonnet";
|
|
6863
7001
|
this.defaultEffort = config.defaultEffort ?? "low";
|
|
6864
|
-
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0,
|
|
7002
|
+
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path18.join)((0, import_os9.homedir)(), ".threadbase", "cache");
|
|
6865
7003
|
this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
|
|
6866
7004
|
this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
|
|
6867
7005
|
this.markScannerStaleDebounced = debounce(() => {
|
|
@@ -6902,7 +7040,7 @@ var StreamerServer = class {
|
|
|
6902
7040
|
const seqs = cache.extendMessageIndex(
|
|
6903
7041
|
filePath,
|
|
6904
7042
|
spans,
|
|
6905
|
-
(0,
|
|
7043
|
+
(0, import_fs18.statSync)(filePath),
|
|
6906
7044
|
readFrom,
|
|
6907
7045
|
endOffset
|
|
6908
7046
|
);
|
|
@@ -7068,7 +7206,7 @@ var StreamerServer = class {
|
|
|
7068
7206
|
temporalClient,
|
|
7069
7207
|
taskQueue: agentConfig.temporal.taskQueue
|
|
7070
7208
|
});
|
|
7071
|
-
const conversationsBaseDir = agentConfig.conversationsDir || (0,
|
|
7209
|
+
const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path18.join)((0, import_path18.dirname)(this.cacheDir), "conversations");
|
|
7072
7210
|
conversationWriter = createConversationWriter({
|
|
7073
7211
|
baseDir: conversationsBaseDir
|
|
7074
7212
|
});
|
|
@@ -7391,7 +7529,7 @@ var StreamerServer = class {
|
|
|
7391
7529
|
});
|
|
7392
7530
|
try {
|
|
7393
7531
|
this.cache = ConversationCache.open(
|
|
7394
|
-
(0,
|
|
7532
|
+
(0, import_path18.join)(this.cacheDir, "cache.db"),
|
|
7395
7533
|
this.tailSize,
|
|
7396
7534
|
void 0,
|
|
7397
7535
|
{
|
|
@@ -7436,7 +7574,7 @@ var StreamerServer = class {
|
|
|
7436
7574
|
this.fileWatcher.watchDirectory(dir);
|
|
7437
7575
|
}
|
|
7438
7576
|
for (const dir of this.codexRoots) {
|
|
7439
|
-
if (!(0,
|
|
7577
|
+
if (!(0, import_fs18.existsSync)(dir)) continue;
|
|
7440
7578
|
this.fileWatcher.watchDirectory(dir);
|
|
7441
7579
|
}
|
|
7442
7580
|
} catch (err) {
|
|
@@ -7517,6 +7655,20 @@ var StreamerServer = class {
|
|
|
7517
7655
|
count: pruned.length,
|
|
7518
7656
|
event: "cache.prune_ghosts"
|
|
7519
7657
|
});
|
|
7658
|
+
if (this.projectsRepo && this.conversationsRepo && this.cacheMetadataRepo) {
|
|
7659
|
+
refreshConversationCache({
|
|
7660
|
+
cache: this.cache,
|
|
7661
|
+
projectsRepo: this.projectsRepo,
|
|
7662
|
+
conversationsRepo: this.conversationsRepo,
|
|
7663
|
+
cacheMetadataRepo: this.cacheMetadataRepo
|
|
7664
|
+
});
|
|
7665
|
+
} else if (this.cacheMetadataRepo) {
|
|
7666
|
+
setCacheMetadata(
|
|
7667
|
+
this.cacheMetadataRepo,
|
|
7668
|
+
"conversations_last_indexed_at",
|
|
7669
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
7670
|
+
);
|
|
7671
|
+
}
|
|
7520
7672
|
}
|
|
7521
7673
|
}).catch((err) => {
|
|
7522
7674
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -7756,6 +7908,68 @@ var StreamerServer = class {
|
|
|
7756
7908
|
checkSessionInputRateLimit(sessionId) {
|
|
7757
7909
|
return this.checkRateLimit(this.sessionInputAttempts, sessionId, 500, 6e4);
|
|
7758
7910
|
}
|
|
7911
|
+
/** Project roots watched for conversation JSONLs (profiles or ~/.claude/projects). */
|
|
7912
|
+
projectsDirsForFreshnessCheck() {
|
|
7913
|
+
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
7914
|
+
return this.scanProfiles.filter((p) => p.enabled).map((p) => (0, import_path18.join)(p.configDir, "projects"));
|
|
7915
|
+
}
|
|
7916
|
+
return [(0, import_path18.join)((0, import_os9.homedir)(), ".claude", "projects")];
|
|
7917
|
+
}
|
|
7918
|
+
/**
|
|
7919
|
+
* Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by
|
|
7920
|
+
* the automatic freshness path when the directory watcher marked the scanner
|
|
7921
|
+
* stale or shouldRefreshProjectsFromHdd detected disk drift.
|
|
7922
|
+
*/
|
|
7923
|
+
async reconcileConversationsCacheFromDisk(onProgress) {
|
|
7924
|
+
if (!this.cache) return;
|
|
7925
|
+
const scanner = await this.rescanForRefresh(onProgress);
|
|
7926
|
+
const metas = [...scanner.getMetadataCache().values()];
|
|
7927
|
+
try {
|
|
7928
|
+
this.cache.upsertFromScannerMeta(metas);
|
|
7929
|
+
if (!this.cacheMonitor?.pending) {
|
|
7930
|
+
this.cache.reconcileDeletions(canonicalLivePathSet(metas));
|
|
7931
|
+
}
|
|
7932
|
+
if (this.projectsRepo && this.conversationsRepo && this.cacheMetadataRepo) {
|
|
7933
|
+
refreshConversationCache({
|
|
7934
|
+
cache: this.cache,
|
|
7935
|
+
projectsRepo: this.projectsRepo,
|
|
7936
|
+
conversationsRepo: this.conversationsRepo,
|
|
7937
|
+
cacheMetadataRepo: this.cacheMetadataRepo
|
|
7938
|
+
});
|
|
7939
|
+
} else if (this.cacheMetadataRepo) {
|
|
7940
|
+
setCacheMetadata(
|
|
7941
|
+
this.cacheMetadataRepo,
|
|
7942
|
+
"conversations_last_indexed_at",
|
|
7943
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
7944
|
+
);
|
|
7945
|
+
}
|
|
7946
|
+
} catch (err) {
|
|
7947
|
+
this.log.warn(
|
|
7948
|
+
`refresh reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
7949
|
+
{ event: "conversations.reconcile_failed" }
|
|
7950
|
+
);
|
|
7951
|
+
}
|
|
7952
|
+
}
|
|
7953
|
+
// Reconcile the cache from disk without blocking the caller. Single-flighted
|
|
7954
|
+
// so a burst of list polls during active session writes shares one rescan
|
|
7955
|
+
// rather than queueing a full rescan each; tracked so close() awaits the
|
|
7956
|
+
// in-flight cache write before shutting the DB.
|
|
7957
|
+
startBackgroundConversationReconcile() {
|
|
7958
|
+
if (this.conversationReconcileInFlight) return;
|
|
7959
|
+
const task = this.reconcileConversationsCacheFromDisk().finally(() => {
|
|
7960
|
+
this.conversationReconcileInFlight = null;
|
|
7961
|
+
});
|
|
7962
|
+
this.conversationReconcileInFlight = task;
|
|
7963
|
+
this.trackCacheWrite(task);
|
|
7964
|
+
}
|
|
7965
|
+
shouldAutoReconcileConversationList() {
|
|
7966
|
+
if (!this.cache) return false;
|
|
7967
|
+
if (this.scannerStale) return true;
|
|
7968
|
+
if (!this.conversationsRepo || !this.cacheMetadataRepo) return false;
|
|
7969
|
+
return shouldRefreshProjectsFromHdd(this.conversationsRepo, this.cacheMetadataRepo, {
|
|
7970
|
+
projectsDirs: this.projectsDirsForFreshnessCheck()
|
|
7971
|
+
});
|
|
7972
|
+
}
|
|
7759
7973
|
async handleListConversations(url, res) {
|
|
7760
7974
|
if (this.rejectIfWarmingUp(res)) return;
|
|
7761
7975
|
const limit = intParam(url, "limit", 50);
|
|
@@ -7764,18 +7978,19 @@ var StreamerServer = class {
|
|
|
7764
7978
|
const project = url.searchParams.get("project") ?? void 0;
|
|
7765
7979
|
const providerFilter = url.searchParams.get("provider") ?? void 0;
|
|
7766
7980
|
const bustCache = url.searchParams.get("refresh") === "1";
|
|
7767
|
-
if (
|
|
7768
|
-
const
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7981
|
+
if (this.cache && (bustCache || this.shouldAutoReconcileConversationList())) {
|
|
7982
|
+
const canServeStale = !bustCache && this.cache.listConversations({ limit: 0, offset: 0 }).total > 0;
|
|
7983
|
+
if (canServeStale) {
|
|
7984
|
+
this.startBackgroundConversationReconcile();
|
|
7985
|
+
} else {
|
|
7986
|
+
const shouldEmitProgress = createScanProgressThrottle();
|
|
7987
|
+
await this.withWarmup(
|
|
7988
|
+
"conversation_refresh",
|
|
7989
|
+
() => this.reconcileConversationsCacheFromDisk((scanned, total2) => {
|
|
7990
|
+
if (shouldEmitProgress(scanned, total2)) {
|
|
7991
|
+
this.wsHub.broadcast({ type: "scan_progress", scanned, total: total2 });
|
|
7992
|
+
}
|
|
7993
|
+
})
|
|
7779
7994
|
);
|
|
7780
7995
|
}
|
|
7781
7996
|
}
|
|
@@ -8012,7 +8227,7 @@ var StreamerServer = class {
|
|
|
8012
8227
|
// getFreshScanner() this does NOT discard the warm scanner. scannerReady is
|
|
8013
8228
|
// only ever reassigned to a live scan promise (never nulled mid-scan), so the
|
|
8014
8229
|
// getScanner() anti-infinite-loop guard is preserved.
|
|
8015
|
-
async rescanForRefresh() {
|
|
8230
|
+
async rescanForRefresh(onProgress) {
|
|
8016
8231
|
if (this.scannerReady) await this.scannerReady;
|
|
8017
8232
|
this.scannerStale = false;
|
|
8018
8233
|
if (!this.scanner) {
|
|
@@ -8023,7 +8238,8 @@ var StreamerServer = class {
|
|
|
8023
8238
|
this.scannerReady = scanner.scan({
|
|
8024
8239
|
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
8025
8240
|
...this.codexScanOpts(),
|
|
8026
|
-
fullRescan: true
|
|
8241
|
+
fullRescan: true,
|
|
8242
|
+
...onProgress ? { onProgress } : {}
|
|
8027
8243
|
});
|
|
8028
8244
|
await this.scannerReady;
|
|
8029
8245
|
return scanner;
|
|
@@ -8038,22 +8254,22 @@ var StreamerServer = class {
|
|
|
8038
8254
|
*/
|
|
8039
8255
|
projectsDirs() {
|
|
8040
8256
|
if (this.scanProfiles && this.scanProfiles.length > 0) {
|
|
8041
|
-
return this.scanProfiles.filter((p) => p.enabled).map((p) => (0,
|
|
8257
|
+
return this.scanProfiles.filter((p) => p.enabled).map((p) => (0, import_path18.join)(p.configDir, "projects"));
|
|
8042
8258
|
}
|
|
8043
|
-
return [(0,
|
|
8259
|
+
return [(0, import_path18.join)((0, import_os9.homedir)(), ".claude", "projects")];
|
|
8044
8260
|
}
|
|
8045
8261
|
findJsonlPath(uuid) {
|
|
8046
8262
|
const filename = `${uuid}.jsonl`;
|
|
8047
8263
|
for (const projectsDir of this.projectsDirs()) {
|
|
8048
|
-
if (!(0,
|
|
8049
|
-
for (const dir of (0,
|
|
8050
|
-
const fp = (0,
|
|
8051
|
-
if ((0,
|
|
8052
|
-
const projectDir = (0,
|
|
8264
|
+
if (!(0, import_fs18.existsSync)(projectsDir)) continue;
|
|
8265
|
+
for (const dir of (0, import_fs18.readdirSync)(projectsDir)) {
|
|
8266
|
+
const fp = (0, import_path18.join)(projectsDir, dir, filename);
|
|
8267
|
+
if ((0, import_fs18.existsSync)(fp)) return fp;
|
|
8268
|
+
const projectDir = (0, import_path18.join)(projectsDir, dir);
|
|
8053
8269
|
try {
|
|
8054
|
-
for (const sub of (0,
|
|
8055
|
-
const subagentPath = (0,
|
|
8056
|
-
if ((0,
|
|
8270
|
+
for (const sub of (0, import_fs18.readdirSync)(projectDir)) {
|
|
8271
|
+
const subagentPath = (0, import_path18.join)(projectDir, sub, "subagents", filename);
|
|
8272
|
+
if ((0, import_fs18.existsSync)(subagentPath)) return subagentPath;
|
|
8057
8273
|
}
|
|
8058
8274
|
} catch {
|
|
8059
8275
|
}
|
|
@@ -8063,7 +8279,7 @@ var StreamerServer = class {
|
|
|
8063
8279
|
}
|
|
8064
8280
|
async readCwdFromJsonl(filePath) {
|
|
8065
8281
|
return new Promise((resolve2) => {
|
|
8066
|
-
const rl = (0, import_readline.createInterface)({ input: (0,
|
|
8282
|
+
const rl = (0, import_readline.createInterface)({ input: (0, import_fs18.createReadStream)(filePath), crlfDelay: Infinity });
|
|
8067
8283
|
let found = false;
|
|
8068
8284
|
rl.on("line", (line) => {
|
|
8069
8285
|
if (found) return;
|
|
@@ -8153,7 +8369,7 @@ var StreamerServer = class {
|
|
|
8153
8369
|
if (this.isManagedTailPath(key)) return;
|
|
8154
8370
|
let mtimeMs;
|
|
8155
8371
|
try {
|
|
8156
|
-
mtimeMs = (0,
|
|
8372
|
+
mtimeMs = (0, import_fs18.statSync)(filePath).mtimeMs;
|
|
8157
8373
|
} catch {
|
|
8158
8374
|
return;
|
|
8159
8375
|
}
|
|
@@ -8335,7 +8551,7 @@ var StreamerServer = class {
|
|
|
8335
8551
|
if (!conv.filePath) return false;
|
|
8336
8552
|
let mtimeMs = null;
|
|
8337
8553
|
try {
|
|
8338
|
-
mtimeMs = (0,
|
|
8554
|
+
mtimeMs = (0, import_fs18.statSync)(conv.filePath).mtimeMs;
|
|
8339
8555
|
} catch {
|
|
8340
8556
|
return false;
|
|
8341
8557
|
}
|
|
@@ -8708,7 +8924,7 @@ var StreamerServer = class {
|
|
|
8708
8924
|
if (this.rejectIfWarmingUp(res)) return;
|
|
8709
8925
|
const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
|
|
8710
8926
|
if (session) {
|
|
8711
|
-
if (!(0,
|
|
8927
|
+
if (!(0, import_fs18.existsSync)(session.projectPath)) {
|
|
8712
8928
|
session.failureReason = `Project directory not found: ${session.projectPath}`;
|
|
8713
8929
|
}
|
|
8714
8930
|
if (this.ptyManager.hasSession(sessionId)) {
|
|
@@ -9179,7 +9395,7 @@ var StreamerServer = class {
|
|
|
9179
9395
|
const jsonlCwd = jsonlPath ? await this.readCwdFromJsonl(jsonlPath) : null;
|
|
9180
9396
|
if (jsonlCwd) {
|
|
9181
9397
|
projectPath = jsonlCwd;
|
|
9182
|
-
projectName = projectName || (0,
|
|
9398
|
+
projectName = projectName || (0, import_path18.basename)(jsonlCwd);
|
|
9183
9399
|
}
|
|
9184
9400
|
}
|
|
9185
9401
|
if (!projectPath) {
|
|
@@ -9255,7 +9471,7 @@ var StreamerServer = class {
|
|
|
9255
9471
|
sessionStore: this.sessionStore,
|
|
9256
9472
|
// biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
|
|
9257
9473
|
agentClient: this.agentClient,
|
|
9258
|
-
conversationsDir: this.cacheDir ? (0,
|
|
9474
|
+
conversationsDir: this.cacheDir ? (0, import_path18.join)((0, import_path18.dirname)(this.cacheDir), "conversations") : "",
|
|
9259
9475
|
agentConfig: this.agentConfig
|
|
9260
9476
|
});
|
|
9261
9477
|
json(res, result.status, result.body);
|
|
@@ -9399,7 +9615,7 @@ var StreamerServer = class {
|
|
|
9399
9615
|
// file isn't slurped in full.
|
|
9400
9616
|
readFirstLineSessionId(filePath) {
|
|
9401
9617
|
try {
|
|
9402
|
-
const content = (0,
|
|
9618
|
+
const content = (0, import_fs18.readFileSync)(filePath, "utf8");
|
|
9403
9619
|
const nl = content.indexOf("\n");
|
|
9404
9620
|
const firstLine = nl === -1 ? content : content.slice(0, nl);
|
|
9405
9621
|
if (!firstLine.trim()) return null;
|
|
@@ -9414,9 +9630,9 @@ var StreamerServer = class {
|
|
|
9414
9630
|
// was passed to Claude via --session-id so the filename matches from the start.
|
|
9415
9631
|
watchForJsonl(sessionId, projectPath) {
|
|
9416
9632
|
const encoded = projectPath.replace(/[/\\:.]/g, "-");
|
|
9417
|
-
const projectsDir = (0,
|
|
9633
|
+
const projectsDir = (0, import_path18.join)((0, import_os9.homedir)(), ".claude", "projects", encoded);
|
|
9418
9634
|
const expectedFile = `${sessionId}.jsonl`;
|
|
9419
|
-
const filePath = (0,
|
|
9635
|
+
const filePath = (0, import_path18.join)(projectsDir, expectedFile);
|
|
9420
9636
|
const deadline = Date.now() + 12e4;
|
|
9421
9637
|
let watcher = null;
|
|
9422
9638
|
const cleanup = () => {
|
|
@@ -9434,14 +9650,14 @@ var StreamerServer = class {
|
|
|
9434
9650
|
cleanup();
|
|
9435
9651
|
return;
|
|
9436
9652
|
}
|
|
9437
|
-
let resolvedFilePath = (0,
|
|
9438
|
-
if (!resolvedFilePath && (0,
|
|
9653
|
+
let resolvedFilePath = (0, import_fs18.existsSync)(filePath) ? filePath : null;
|
|
9654
|
+
if (!resolvedFilePath && (0, import_fs18.existsSync)(projectsDir)) {
|
|
9439
9655
|
try {
|
|
9440
9656
|
const now = Date.now();
|
|
9441
|
-
const match = (0,
|
|
9442
|
-
({ f }) => (0,
|
|
9657
|
+
const match = (0, import_fs18.readdirSync)(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: (0, import_fs18.statSync)((0, import_path18.join)(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
|
|
9658
|
+
({ f }) => (0, import_path18.basename)(f, ".jsonl") === sessionId || this.readFirstLineSessionId((0, import_path18.join)(projectsDir, f)) === sessionId
|
|
9443
9659
|
).sort((a, b) => b.mtime - a.mtime)[0];
|
|
9444
|
-
if (match) resolvedFilePath = (0,
|
|
9660
|
+
if (match) resolvedFilePath = (0, import_path18.join)(projectsDir, match.f);
|
|
9445
9661
|
} catch {
|
|
9446
9662
|
}
|
|
9447
9663
|
}
|
|
@@ -9449,7 +9665,7 @@ var StreamerServer = class {
|
|
|
9449
9665
|
cleanup();
|
|
9450
9666
|
this.sessionFileMap.set(sessionId, resolvedFilePath);
|
|
9451
9667
|
try {
|
|
9452
|
-
const existing = (0,
|
|
9668
|
+
const existing = (0, import_fs18.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
|
|
9453
9669
|
if (existing.length > 0) {
|
|
9454
9670
|
this.broadcastConversationLines(sessionId, existing);
|
|
9455
9671
|
}
|
|
@@ -9473,7 +9689,7 @@ var StreamerServer = class {
|
|
|
9473
9689
|
if (this.sessionFileMap.has(sessionId)) return;
|
|
9474
9690
|
try {
|
|
9475
9691
|
require("fs").mkdirSync(projectsDir, { recursive: true });
|
|
9476
|
-
watcher = (0,
|
|
9692
|
+
watcher = (0, import_fs18.watch)(projectsDir, tryWire);
|
|
9477
9693
|
watcher.on("error", cleanup);
|
|
9478
9694
|
} catch {
|
|
9479
9695
|
}
|
|
@@ -9489,7 +9705,7 @@ var StreamerServer = class {
|
|
|
9489
9705
|
watchForCodexRollout(sessionId, projectPath) {
|
|
9490
9706
|
const deadline = Date.now() + 12e4;
|
|
9491
9707
|
const now = /* @__PURE__ */ new Date();
|
|
9492
|
-
const dateDir = (0,
|
|
9708
|
+
const dateDir = (0, import_path18.join)(
|
|
9493
9709
|
String(now.getFullYear()),
|
|
9494
9710
|
String(now.getMonth() + 1).padStart(2, "0"),
|
|
9495
9711
|
String(now.getDate()).padStart(2, "0")
|
|
@@ -9502,7 +9718,7 @@ var StreamerServer = class {
|
|
|
9502
9718
|
};
|
|
9503
9719
|
const matchesProjectPath = (candidatePath) => {
|
|
9504
9720
|
try {
|
|
9505
|
-
const firstLine = (0,
|
|
9721
|
+
const firstLine = (0, import_fs18.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
|
|
9506
9722
|
if (!firstLine) return null;
|
|
9507
9723
|
const parsed = JSON.parse(firstLine);
|
|
9508
9724
|
if (parsed?.type !== "session_meta") return null;
|
|
@@ -9530,18 +9746,18 @@ var StreamerServer = class {
|
|
|
9530
9746
|
this.sessionStore.listManaged().filter((s) => s.id !== sessionId && s.boundConversationId != null).map((s) => s.boundConversationId)
|
|
9531
9747
|
);
|
|
9532
9748
|
for (const root of this.codexRoots) {
|
|
9533
|
-
const sessionsDir = (0,
|
|
9534
|
-
if (!(0,
|
|
9749
|
+
const sessionsDir = (0, import_path18.join)(root, dateDir);
|
|
9750
|
+
if (!(0, import_fs18.existsSync)(sessionsDir)) continue;
|
|
9535
9751
|
let candidateFiles;
|
|
9536
9752
|
try {
|
|
9537
|
-
candidateFiles = (0,
|
|
9753
|
+
candidateFiles = (0, import_fs18.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
9538
9754
|
} catch {
|
|
9539
9755
|
continue;
|
|
9540
9756
|
}
|
|
9541
9757
|
const nowMs = Date.now();
|
|
9542
|
-
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0,
|
|
9758
|
+
const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0, import_fs18.statSync)((0, import_path18.join)(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
|
|
9543
9759
|
for (const { f } of recentCandidates) {
|
|
9544
|
-
const candidatePath = (0,
|
|
9760
|
+
const candidatePath = (0, import_path18.join)(sessionsDir, f);
|
|
9545
9761
|
const match = matchesProjectPath(candidatePath);
|
|
9546
9762
|
if (!match) continue;
|
|
9547
9763
|
if (boundElsewhere.has(match.id)) continue;
|
|
@@ -9551,7 +9767,7 @@ var StreamerServer = class {
|
|
|
9551
9767
|
this.sessionFileMap.set(sessionId, candidatePath);
|
|
9552
9768
|
this.fileWatcher.watch(candidatePath);
|
|
9553
9769
|
try {
|
|
9554
|
-
const existing = (0,
|
|
9770
|
+
const existing = (0, import_fs18.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
|
|
9555
9771
|
if (existing.length > 0) {
|
|
9556
9772
|
this.broadcastConversationLines(sessionId, existing);
|
|
9557
9773
|
}
|
|
@@ -9685,7 +9901,7 @@ async function waitForProcessExit(pid, timeoutMs, pollMs = ADOPT_KILL_POLL_MS) {
|
|
|
9685
9901
|
}
|
|
9686
9902
|
function classifyResumability(cwd) {
|
|
9687
9903
|
if (!cwd) return { resumable: true };
|
|
9688
|
-
if ((0,
|
|
9904
|
+
if ((0, import_fs18.existsSync)(cwd)) return { resumable: true };
|
|
9689
9905
|
const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
|
|
9690
9906
|
return {
|
|
9691
9907
|
resumable: false,
|