@threadbase-sh/streamer 1.24.4 → 1.24.6
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 +409 -102
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +131 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +131 -24
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/cli.cjs
CHANGED
|
@@ -101524,7 +101524,7 @@ var require_async2 = __commonJS({
|
|
|
101524
101524
|
readdirWithFileTypes(directory, settings, callback);
|
|
101525
101525
|
return;
|
|
101526
101526
|
}
|
|
101527
|
-
|
|
101527
|
+
readdir7(directory, settings, callback);
|
|
101528
101528
|
}
|
|
101529
101529
|
exports2.read = read;
|
|
101530
101530
|
function readdirWithFileTypes(directory, settings, callback) {
|
|
@@ -101573,7 +101573,7 @@ var require_async2 = __commonJS({
|
|
|
101573
101573
|
});
|
|
101574
101574
|
};
|
|
101575
101575
|
}
|
|
101576
|
-
function
|
|
101576
|
+
function readdir7(directory, settings, callback) {
|
|
101577
101577
|
settings.fs.readdir(directory, (readdirError, names) => {
|
|
101578
101578
|
if (readdirError !== null) {
|
|
101579
101579
|
callFailureCallback(callback, readdirError);
|
|
@@ -101608,7 +101608,7 @@ var require_async2 = __commonJS({
|
|
|
101608
101608
|
});
|
|
101609
101609
|
});
|
|
101610
101610
|
}
|
|
101611
|
-
exports2.readdir =
|
|
101611
|
+
exports2.readdir = readdir7;
|
|
101612
101612
|
function callFailureCallback(callback, error51) {
|
|
101613
101613
|
callback(error51);
|
|
101614
101614
|
}
|
|
@@ -101632,7 +101632,7 @@ var require_sync2 = __commonJS({
|
|
|
101632
101632
|
if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) {
|
|
101633
101633
|
return readdirWithFileTypes(directory, settings);
|
|
101634
101634
|
}
|
|
101635
|
-
return
|
|
101635
|
+
return readdir7(directory, settings);
|
|
101636
101636
|
}
|
|
101637
101637
|
exports2.read = read;
|
|
101638
101638
|
function readdirWithFileTypes(directory, settings) {
|
|
@@ -101657,7 +101657,7 @@ var require_sync2 = __commonJS({
|
|
|
101657
101657
|
});
|
|
101658
101658
|
}
|
|
101659
101659
|
exports2.readdirWithFileTypes = readdirWithFileTypes;
|
|
101660
|
-
function
|
|
101660
|
+
function readdir7(directory, settings) {
|
|
101661
101661
|
const names = settings.fs.readdirSync(directory);
|
|
101662
101662
|
return names.map((name) => {
|
|
101663
101663
|
const entryPath = common.joinPathSegments(directory, name, settings.pathSegmentSeparator);
|
|
@@ -101673,7 +101673,7 @@ var require_sync2 = __commonJS({
|
|
|
101673
101673
|
return entry;
|
|
101674
101674
|
});
|
|
101675
101675
|
}
|
|
101676
|
-
exports2.readdir =
|
|
101676
|
+
exports2.readdir = readdir7;
|
|
101677
101677
|
}
|
|
101678
101678
|
});
|
|
101679
101679
|
|
|
@@ -124915,14 +124915,14 @@ var prompt_exports = {};
|
|
|
124915
124915
|
__export(prompt_exports, {
|
|
124916
124916
|
interactivePrompt: () => interactivePrompt
|
|
124917
124917
|
});
|
|
124918
|
-
var import_node_process2,
|
|
124918
|
+
var import_node_process2, import_promises18, interactivePrompt;
|
|
124919
124919
|
var init_prompt = __esm({
|
|
124920
124920
|
"src/lifecycle/prompt.ts"() {
|
|
124921
124921
|
"use strict";
|
|
124922
124922
|
import_node_process2 = require("process");
|
|
124923
|
-
|
|
124923
|
+
import_promises18 = require("readline/promises");
|
|
124924
124924
|
interactivePrompt = async ({ prodPort, suggestedAltPort }) => {
|
|
124925
|
-
const rl = (0,
|
|
124925
|
+
const rl = (0, import_promises18.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
|
|
124926
124926
|
try {
|
|
124927
124927
|
import_node_process2.stdout.write(
|
|
124928
124928
|
`
|
|
@@ -129481,6 +129481,7 @@ var import_readline3 = require("readline");
|
|
|
129481
129481
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
129482
129482
|
var import_fs11 = require("fs");
|
|
129483
129483
|
var import_path8 = require("path");
|
|
129484
|
+
var import_promises7 = require("fs/promises");
|
|
129484
129485
|
var import_fs12 = require("fs");
|
|
129485
129486
|
var import_fs13 = require("fs");
|
|
129486
129487
|
var import_path9 = require("path");
|
|
@@ -132302,30 +132303,33 @@ function fingerprint(filePath, size) {
|
|
|
132302
132303
|
return hash2.digest("hex");
|
|
132303
132304
|
}
|
|
132304
132305
|
function classify(filePath, existing) {
|
|
132305
|
-
let
|
|
132306
|
+
let stat42;
|
|
132306
132307
|
try {
|
|
132307
132308
|
const s3 = (0, import_fs9.statSync)(filePath);
|
|
132308
|
-
|
|
132309
|
+
stat42 = { size: s3.size, mtimeMs: s3.mtimeMs };
|
|
132309
132310
|
} catch {
|
|
132310
132311
|
return { change: "vanished" };
|
|
132311
132312
|
}
|
|
132312
132313
|
if (!existing || existing.status !== "active" || existing.last_indexed_offset === 0) {
|
|
132313
|
-
return { change: "reindex", stat:
|
|
132314
|
+
return { change: "reindex", stat: stat42 };
|
|
132314
132315
|
}
|
|
132315
|
-
if (
|
|
132316
|
-
return { change: "reindex", stat:
|
|
132316
|
+
if (stat42.size < existing.last_indexed_offset) {
|
|
132317
|
+
return { change: "reindex", stat: stat42 };
|
|
132317
132318
|
}
|
|
132318
|
-
if (
|
|
132319
|
-
return { change: "unchanged", stat:
|
|
132319
|
+
if (stat42.size === existing.size_bytes && stat42.mtimeMs === existing.mtime_ms) {
|
|
132320
|
+
return { change: "unchanged", stat: stat42 };
|
|
132320
132321
|
}
|
|
132321
|
-
if (
|
|
132322
|
-
const fp = fingerprint(filePath,
|
|
132322
|
+
if (stat42.size === existing.last_indexed_offset) {
|
|
132323
|
+
const fp = fingerprint(filePath, stat42.size);
|
|
132323
132324
|
if (existing.content_fingerprint && fp !== existing.content_fingerprint) {
|
|
132324
|
-
return { change: "reindex", stat:
|
|
132325
|
+
return { change: "reindex", stat: stat42 };
|
|
132325
132326
|
}
|
|
132326
|
-
return { change: "unchanged", stat:
|
|
132327
|
+
return { change: "unchanged", stat: stat42 };
|
|
132328
|
+
}
|
|
132329
|
+
if (existing.content_fingerprint && existing.size_bytes === existing.last_indexed_offset && fingerprint(filePath, existing.last_indexed_offset) === existing.content_fingerprint) {
|
|
132330
|
+
return { change: "appended", stat: stat42 };
|
|
132327
132331
|
}
|
|
132328
|
-
return { change: "
|
|
132332
|
+
return { change: "reindex", stat: stat42 };
|
|
132329
132333
|
}
|
|
132330
132334
|
async function parseMetaWithProvider(provider, filePath, account, tier) {
|
|
132331
132335
|
const log8 = getLogger2();
|
|
@@ -132368,7 +132372,7 @@ function resolveTier(tierName, customTiers) {
|
|
|
132368
132372
|
}
|
|
132369
132373
|
return tier;
|
|
132370
132374
|
}
|
|
132371
|
-
var SCHEMA_VERSION =
|
|
132375
|
+
var SCHEMA_VERSION = 4;
|
|
132372
132376
|
var SCHEMA_SQL = `
|
|
132373
132377
|
CREATE TABLE IF NOT EXISTS conversation_files (
|
|
132374
132378
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -132502,6 +132506,24 @@ CREATE TABLE IF NOT EXISTS message_checkpoints (
|
|
|
132502
132506
|
|
|
132503
132507
|
CREATE INDEX IF NOT EXISTS idx_message_checkpoints_lookup
|
|
132504
132508
|
ON message_checkpoints(source_path, message_index);
|
|
132509
|
+
|
|
132510
|
+
-- Per-directory mtime watermark for the discovery dir-mtime gate (skips the
|
|
132511
|
+
-- glob for a directory whose file/subdir SET hasn't changed; never skips
|
|
132512
|
+
-- per-file classify()). One row for a profile's projectsDir itself (parent_root
|
|
132513
|
+
-- IS NULL) and one row per immediate project subdirectory discovered under it
|
|
132514
|
+
-- (parent_root = the projectsDir path). has_nested marks a project dir known to
|
|
132515
|
+
-- contain files below its own top level (e.g. subagents/) \u2014 those always
|
|
132516
|
+
-- re-glob regardless of mtime, since a project-dir-level watermark can't see a
|
|
132517
|
+
-- change two levels down.
|
|
132518
|
+
CREATE TABLE IF NOT EXISTS scanned_dirs (
|
|
132519
|
+
path TEXT PRIMARY KEY,
|
|
132520
|
+
parent_root TEXT,
|
|
132521
|
+
mtime_ms INTEGER NOT NULL,
|
|
132522
|
+
has_nested INTEGER NOT NULL DEFAULT 0,
|
|
132523
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
132524
|
+
);
|
|
132525
|
+
|
|
132526
|
+
CREATE INDEX IF NOT EXISTS idx_scanned_dirs_parent_root ON scanned_dirs(parent_root);
|
|
132505
132527
|
`;
|
|
132506
132528
|
function runMigrations(db) {
|
|
132507
132529
|
const current = db.pragma("user_version", { simple: true });
|
|
@@ -132547,6 +132569,82 @@ function openDatabase(dbPath) {
|
|
|
132547
132569
|
getLogger2().debug({ dbPath }, "db: opened");
|
|
132548
132570
|
return db;
|
|
132549
132571
|
}
|
|
132572
|
+
var FULL_RECONCILE_EVERY_N_SCANS = 20;
|
|
132573
|
+
async function discoverJsonlFilesGated(dirs, files, scannedDirs, options = {}) {
|
|
132574
|
+
const log8 = getLogger2();
|
|
132575
|
+
if (options.fullRescan) {
|
|
132576
|
+
return discoverJsonlFiles(dirs);
|
|
132577
|
+
}
|
|
132578
|
+
const results = [];
|
|
132579
|
+
for (const rawDir of dirs) {
|
|
132580
|
+
const { account } = rawDir;
|
|
132581
|
+
const projectsDir = rawDir.projectsDir.replace(/\\/g, "/");
|
|
132582
|
+
const resolved = await resolveProjectDirs(projectsDir, scannedDirs);
|
|
132583
|
+
if (resolved === null) continue;
|
|
132584
|
+
for (const projectDir of resolved.entries) {
|
|
132585
|
+
let dirStat;
|
|
132586
|
+
try {
|
|
132587
|
+
dirStat = await (0, import_promises7.stat)(projectDir);
|
|
132588
|
+
} catch {
|
|
132589
|
+
scannedDirs.remove(projectDir);
|
|
132590
|
+
continue;
|
|
132591
|
+
}
|
|
132592
|
+
const watermark = scannedDirs.get(projectDir);
|
|
132593
|
+
const canReuse = watermark !== void 0 && watermark.mtime_ms === dirStat.mtimeMs && watermark.has_nested === 0;
|
|
132594
|
+
if (canReuse) {
|
|
132595
|
+
const known = files.activePathsByParentDir(projectDir);
|
|
132596
|
+
if (known.length > 0) {
|
|
132597
|
+
for (const row of known) {
|
|
132598
|
+
results.push({ filePath: row.absolute_path, account: row.account });
|
|
132599
|
+
}
|
|
132600
|
+
continue;
|
|
132601
|
+
}
|
|
132602
|
+
}
|
|
132603
|
+
const found = await discoverJsonlFiles([{ projectsDir: projectDir, account }]);
|
|
132604
|
+
const hasNested = found.some((f2) => dirnameOf(f2.filePath) !== projectDir);
|
|
132605
|
+
scannedDirs.upsert(projectDir, projectsDir, dirStat.mtimeMs, hasNested);
|
|
132606
|
+
results.push(...found);
|
|
132607
|
+
}
|
|
132608
|
+
if (resolved.commitRoot) {
|
|
132609
|
+
scannedDirs.upsert(projectsDir, null, resolved.commitRoot.mtimeMs, false);
|
|
132610
|
+
const seen = new Set(resolved.entries);
|
|
132611
|
+
for (const known of scannedDirs.childrenOf(projectsDir)) {
|
|
132612
|
+
if (!seen.has(known.path)) scannedDirs.remove(known.path);
|
|
132613
|
+
}
|
|
132614
|
+
}
|
|
132615
|
+
}
|
|
132616
|
+
log8.debug(
|
|
132617
|
+
{ totalFiles: results.length, dirs: dirs.length },
|
|
132618
|
+
"dir-watermark: gated discovery complete"
|
|
132619
|
+
);
|
|
132620
|
+
return results;
|
|
132621
|
+
}
|
|
132622
|
+
async function resolveProjectDirs(projectsDir, scannedDirs) {
|
|
132623
|
+
let rootStat;
|
|
132624
|
+
try {
|
|
132625
|
+
rootStat = await (0, import_promises7.stat)(projectsDir);
|
|
132626
|
+
} catch {
|
|
132627
|
+
return null;
|
|
132628
|
+
}
|
|
132629
|
+
const rootWatermark = scannedDirs.get(projectsDir);
|
|
132630
|
+
if (rootWatermark !== void 0 && rootWatermark.mtime_ms === rootStat.mtimeMs) {
|
|
132631
|
+
return { entries: scannedDirs.childrenOf(projectsDir).map((row) => row.path) };
|
|
132632
|
+
}
|
|
132633
|
+
let entries;
|
|
132634
|
+
try {
|
|
132635
|
+
entries = (await (0, import_promises7.readdir)(projectsDir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => joinPath(projectsDir, e.name));
|
|
132636
|
+
} catch {
|
|
132637
|
+
return null;
|
|
132638
|
+
}
|
|
132639
|
+
return { entries, commitRoot: { mtimeMs: rootStat.mtimeMs } };
|
|
132640
|
+
}
|
|
132641
|
+
function dirnameOf(filePath) {
|
|
132642
|
+
const idx = filePath.lastIndexOf("/");
|
|
132643
|
+
return idx === -1 ? filePath : filePath.slice(0, idx);
|
|
132644
|
+
}
|
|
132645
|
+
function joinPath(dir, name) {
|
|
132646
|
+
return dir.endsWith("/") ? `${dir}${name}` : `${dir}/${name}`;
|
|
132647
|
+
}
|
|
132550
132648
|
async function tailReduce(filePath, startOffset, startLine, state, tier) {
|
|
132551
132649
|
const stream = (0, import_fs12.createReadStream)(filePath, { start: startOffset, encoding: "utf8" });
|
|
132552
132650
|
let buffer = "";
|
|
@@ -132743,6 +132841,27 @@ var ConversationFilesRepo = class {
|
|
|
132743
132841
|
const rows = this.db.prepare("SELECT absolute_path FROM conversation_files WHERE status != 'deleted'").all();
|
|
132744
132842
|
return rows.map((r) => r.absolute_path);
|
|
132745
132843
|
}
|
|
132844
|
+
// Active file paths belonging to any of the given accounts. Backs the
|
|
132845
|
+
// deletion-reconcile so a scan that covered only some accounts can't mark
|
|
132846
|
+
// another account's files deleted (they live in the same shared index.db but
|
|
132847
|
+
// a different profile owns them). Returns [] for an empty account list.
|
|
132848
|
+
activePathsByAccounts(accounts) {
|
|
132849
|
+
if (accounts.length === 0) return [];
|
|
132850
|
+
const placeholders = accounts.map(() => "?").join(", ");
|
|
132851
|
+
const rows = this.db.prepare(
|
|
132852
|
+
`SELECT absolute_path FROM conversation_files
|
|
132853
|
+
WHERE status != 'deleted' AND account IN (${placeholders})`
|
|
132854
|
+
).all(...accounts);
|
|
132855
|
+
return rows.map((r) => r.absolute_path);
|
|
132856
|
+
}
|
|
132857
|
+
// Active files whose immediate parent is exactly parentDir (no nested
|
|
132858
|
+
// subdirectories). Backs the dir-mtime gate's reuse path: a project dir with
|
|
132859
|
+
// an unchanged mtime and no nested files can skip the glob entirely.
|
|
132860
|
+
activePathsByParentDir(parentDir) {
|
|
132861
|
+
return this.db.prepare(
|
|
132862
|
+
"SELECT absolute_path, account FROM conversation_files WHERE parent_dir = ? AND status != 'deleted'"
|
|
132863
|
+
).all(parentDir);
|
|
132864
|
+
}
|
|
132746
132865
|
};
|
|
132747
132866
|
function rowToMeta(row) {
|
|
132748
132867
|
return {
|
|
@@ -132986,6 +133105,34 @@ function toMatchQuery(query) {
|
|
|
132986
133105
|
if (terms.length === 0) return "";
|
|
132987
133106
|
return terms.map((t2) => `"${t2}"*`).join(" AND ");
|
|
132988
133107
|
}
|
|
133108
|
+
var ScannedDirsRepo = class {
|
|
133109
|
+
constructor(db) {
|
|
133110
|
+
this.db = db;
|
|
133111
|
+
}
|
|
133112
|
+
db;
|
|
133113
|
+
get(path2) {
|
|
133114
|
+
return this.db.prepare("SELECT * FROM scanned_dirs WHERE path = ?").get(path2);
|
|
133115
|
+
}
|
|
133116
|
+
// Known project subdirectories under a root, path ascending (stable order).
|
|
133117
|
+
childrenOf(parentRoot) {
|
|
133118
|
+
return this.db.prepare("SELECT * FROM scanned_dirs WHERE parent_root = ? ORDER BY path ASC").all(parentRoot);
|
|
133119
|
+
}
|
|
133120
|
+
upsert(path2, parentRoot, mtimeMs, hasNested) {
|
|
133121
|
+
this.db.prepare(
|
|
133122
|
+
`INSERT INTO scanned_dirs (path, parent_root, mtime_ms, has_nested, updated_at)
|
|
133123
|
+
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
133124
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
133125
|
+
parent_root = excluded.parent_root,
|
|
133126
|
+
mtime_ms = excluded.mtime_ms,
|
|
133127
|
+
has_nested = excluded.has_nested,
|
|
133128
|
+
updated_at = CURRENT_TIMESTAMP`
|
|
133129
|
+
).run(path2, parentRoot, mtimeMs, hasNested ? 1 : 0);
|
|
133130
|
+
}
|
|
133131
|
+
// Drop a project subdirectory's watermark row (it vanished from disk).
|
|
133132
|
+
remove(path2) {
|
|
133133
|
+
this.db.prepare("DELETE FROM scanned_dirs WHERE path = ?").run(path2);
|
|
133134
|
+
}
|
|
133135
|
+
};
|
|
132989
133136
|
var BATCH_SIZE = 12;
|
|
132990
133137
|
var PersistentEngine = class {
|
|
132991
133138
|
db;
|
|
@@ -132993,15 +133140,22 @@ var PersistentEngine = class {
|
|
|
132993
133140
|
conversations;
|
|
132994
133141
|
fts;
|
|
132995
133142
|
checkpoints;
|
|
133143
|
+
scannedDirs;
|
|
132996
133144
|
// When true, write a portable <file>.idx.json sidecar next to each indexed
|
|
132997
133145
|
// JSONL. Off by default.
|
|
132998
133146
|
sidecar;
|
|
133147
|
+
// Counts indexAll() passes so the dir-mtime gate's full-reconcile backstop
|
|
133148
|
+
// (FULL_RECONCILE_EVERY_N_SCANS) can fire periodically. In-memory only: a
|
|
133149
|
+
// restart just means the first few post-restart scans don't force an early
|
|
133150
|
+
// backstop pass, which is harmless (watermarks themselves persist in the DB).
|
|
133151
|
+
scanCount = 0;
|
|
132999
133152
|
constructor(dbPath, options = {}) {
|
|
133000
133153
|
this.db = openDatabase(dbPath);
|
|
133001
133154
|
this.files = new ConversationFilesRepo(this.db);
|
|
133002
133155
|
this.conversations = new ConversationsRepo(this.db);
|
|
133003
133156
|
this.fts = new FtsRepo(this.db);
|
|
133004
133157
|
this.checkpoints = new CheckpointsRepo(this.db);
|
|
133158
|
+
this.scannedDirs = new ScannedDirsRepo(this.db);
|
|
133005
133159
|
this.sidecar = options.sidecar ?? false;
|
|
133006
133160
|
}
|
|
133007
133161
|
close() {
|
|
@@ -133020,7 +133174,12 @@ var PersistentEngine = class {
|
|
|
133020
133174
|
projectsDir: getProjectsDir(p2),
|
|
133021
133175
|
account: p2.id
|
|
133022
133176
|
}));
|
|
133023
|
-
|
|
133177
|
+
this.scanCount++;
|
|
133178
|
+
const forceFullGlob = options.fullRescan === true || this.scanCount % FULL_RECONCILE_EVERY_N_SCANS === 0;
|
|
133179
|
+
const gated = await discoverJsonlFilesGated(configDirs, this.files, this.scannedDirs, {
|
|
133180
|
+
fullRescan: forceFullGlob
|
|
133181
|
+
});
|
|
133182
|
+
for (const f2 of gated) discovered.push(f2);
|
|
133024
133183
|
}
|
|
133025
133184
|
const codex = new CodexCliProvider();
|
|
133026
133185
|
if (enabled.includes(CODEX_CLI_PROVIDER) && (options.codexRoots?.length ?? 0) > 0) {
|
|
@@ -133059,8 +133218,15 @@ var PersistentEngine = class {
|
|
|
133059
133218
|
scanned += batch.length;
|
|
133060
133219
|
options.onProgress?.(scanned, discovered.length);
|
|
133061
133220
|
}
|
|
133221
|
+
const coveredAccounts = /* @__PURE__ */ new Set();
|
|
133222
|
+
if (enabled.includes(CLAUDE_CODE_PROVIDER)) {
|
|
133223
|
+
for (const p2 of activeProfiles) coveredAccounts.add(p2.id);
|
|
133224
|
+
}
|
|
133225
|
+
if (enabled.includes(CODEX_CLI_PROVIDER) && (options.codexRoots?.length ?? 0) > 0) {
|
|
133226
|
+
coveredAccounts.add("codex");
|
|
133227
|
+
}
|
|
133062
133228
|
const seen = new Set(discovered.map((d) => d.filePath));
|
|
133063
|
-
for (const path2 of this.files.
|
|
133229
|
+
for (const path2 of this.files.activePathsByAccounts([...coveredAccounts])) {
|
|
133064
133230
|
if (!seen.has(path2)) this.markDeleted(path2);
|
|
133065
133231
|
}
|
|
133066
133232
|
log8.info({ scanned, indexed: this.conversations.count() }, "persistent: indexAll complete");
|
|
@@ -133075,8 +133241,8 @@ var PersistentEngine = class {
|
|
|
133075
133241
|
const log8 = getLogger2();
|
|
133076
133242
|
const tier = resolveTier(tierName, customTiers);
|
|
133077
133243
|
const existing = this.files.getByPath(filePath);
|
|
133078
|
-
const { change, stat:
|
|
133079
|
-
if (change === "vanished" || !
|
|
133244
|
+
const { change, stat: stat42 } = classify(filePath, existing);
|
|
133245
|
+
if (change === "vanished" || !stat42) {
|
|
133080
133246
|
this.markDeleted(filePath);
|
|
133081
133247
|
return null;
|
|
133082
133248
|
}
|
|
@@ -133084,7 +133250,7 @@ var PersistentEngine = class {
|
|
|
133084
133250
|
return this.conversations.getBySourcePath(filePath);
|
|
133085
133251
|
}
|
|
133086
133252
|
if (provider && provider.name !== CLAUDE_CODE_PROVIDER) {
|
|
133087
|
-
return this.indexFileWithProvider(provider, filePath, account, tier,
|
|
133253
|
+
return this.indexFileWithProvider(provider, filePath, account, tier, stat42, resolveGitBranch);
|
|
133088
133254
|
}
|
|
133089
133255
|
const resume = change === "appended" && !force && existing?.reducer_state;
|
|
133090
133256
|
const state = resume ? JSON.parse(existing.reducer_state) : initialReducerState();
|
|
@@ -133103,15 +133269,15 @@ var PersistentEngine = class {
|
|
|
133103
133269
|
return null;
|
|
133104
133270
|
}
|
|
133105
133271
|
meta3.gitBranch = resolveGitBranch(meta3.projectPath);
|
|
133106
|
-
const fp =
|
|
133272
|
+
const fp = stat42.size > 0 ? fingerprint(filePath, stat42.size) : null;
|
|
133107
133273
|
const fileId = this.files.ensure(filePath, account);
|
|
133108
133274
|
const upsert = this.db.transaction(() => {
|
|
133109
133275
|
this.conversations.upsert(fileId, meta3, state.pageMessageCount);
|
|
133110
133276
|
this.fts.upsert(meta3);
|
|
133111
133277
|
this.checkpoints.remove(filePath);
|
|
133112
133278
|
this.files.updateCursor(fileId, {
|
|
133113
|
-
sizeBytes:
|
|
133114
|
-
mtimeMs:
|
|
133279
|
+
sizeBytes: stat42.size,
|
|
133280
|
+
mtimeMs: stat42.mtimeMs,
|
|
133115
133281
|
// Advance only to the last fully-parsed line; a trailing partial line
|
|
133116
133282
|
// is left for the next pass.
|
|
133117
133283
|
offset: result.newOffset,
|
|
@@ -133128,8 +133294,8 @@ var PersistentEngine = class {
|
|
|
133128
133294
|
buildSidecar(
|
|
133129
133295
|
meta3,
|
|
133130
133296
|
{
|
|
133131
|
-
sizeBytes:
|
|
133132
|
-
mtimeMs:
|
|
133297
|
+
sizeBytes: stat42.size,
|
|
133298
|
+
mtimeMs: stat42.mtimeMs,
|
|
133133
133299
|
offset: result.newOffset,
|
|
133134
133300
|
line: result.newLine
|
|
133135
133301
|
},
|
|
@@ -133148,7 +133314,7 @@ var PersistentEngine = class {
|
|
|
133148
133314
|
// the Threadbase path uses. The cursor records size/mtime (and offset = size)
|
|
133149
133315
|
// so the next pass classifies an unchanged file as "unchanged" and skips it;
|
|
133150
133316
|
// any change reparses from 0 again. No reducer_state is persisted.
|
|
133151
|
-
async indexFileWithProvider(provider, filePath, account, tier,
|
|
133317
|
+
async indexFileWithProvider(provider, filePath, account, tier, stat42, resolveGitBranch) {
|
|
133152
133318
|
const log8 = getLogger2();
|
|
133153
133319
|
const meta3 = await parseMetaWithProvider(provider, filePath, account, tier);
|
|
133154
133320
|
if (!meta3) {
|
|
@@ -133158,18 +133324,18 @@ var PersistentEngine = class {
|
|
|
133158
133324
|
if (meta3.gitBranch === null && meta3.projectPath) {
|
|
133159
133325
|
meta3.gitBranch = resolveGitBranch(meta3.projectPath);
|
|
133160
133326
|
}
|
|
133161
|
-
const fp =
|
|
133327
|
+
const fp = stat42.size > 0 ? fingerprint(filePath, stat42.size) : null;
|
|
133162
133328
|
const fileId = this.files.ensure(filePath, account);
|
|
133163
133329
|
const upsert = this.db.transaction(() => {
|
|
133164
133330
|
this.conversations.upsert(fileId, meta3, meta3.messageCount);
|
|
133165
133331
|
this.fts.upsert(meta3);
|
|
133166
133332
|
this.checkpoints.remove(filePath);
|
|
133167
133333
|
this.files.updateCursor(fileId, {
|
|
133168
|
-
sizeBytes:
|
|
133169
|
-
mtimeMs:
|
|
133334
|
+
sizeBytes: stat42.size,
|
|
133335
|
+
mtimeMs: stat42.mtimeMs,
|
|
133170
133336
|
// offset = size marks the file fully consumed (non-zero so the next pass
|
|
133171
133337
|
// can classify it "unchanged"); no resumable reducer state is kept.
|
|
133172
|
-
offset:
|
|
133338
|
+
offset: stat42.size,
|
|
133173
133339
|
line: 0,
|
|
133174
133340
|
reducerState: null,
|
|
133175
133341
|
fingerprint: fp,
|
|
@@ -133363,10 +133529,21 @@ var ConversationScanner = class {
|
|
|
133363
133529
|
dbPath;
|
|
133364
133530
|
sidecarEnabled;
|
|
133365
133531
|
engineInstance = null;
|
|
133532
|
+
// The most recent scan()'s promise while it is still running, or null when
|
|
133533
|
+
// idle. close() awaits it before closing the SQLite handle so a fire-and-
|
|
133534
|
+
// forget scan can't have its DB shut mid-indexAll ("database connection is
|
|
133535
|
+
// not open"). Tracks the latest scan only — concurrent scans on one instance
|
|
133536
|
+
// aren't a supported pattern (a single writer DB), and the latest resolving
|
|
133537
|
+
// implies earlier ones already settled in practice.
|
|
133538
|
+
inFlightScan = null;
|
|
133366
133539
|
emitter = new import_events2.EventEmitter();
|
|
133367
133540
|
watcher = null;
|
|
133368
133541
|
queue = null;
|
|
133369
133542
|
periodicTimer = null;
|
|
133543
|
+
// The in-flight periodicReconcile() job, if one is mid-run. unwatch() awaits
|
|
133544
|
+
// it before dropping the queue/engine so its post-await engine.files access
|
|
133545
|
+
// can't hit a closed DB (the watch-mode half of Bug #4).
|
|
133546
|
+
inFlightReconcile = null;
|
|
133370
133547
|
constructor(options) {
|
|
133371
133548
|
this.conversationLRU = new LRUCache(options?.conversationCacheSize ?? 5);
|
|
133372
133549
|
if (options?.persistent === false) {
|
|
@@ -133389,14 +133566,31 @@ var ConversationScanner = class {
|
|
|
133389
133566
|
return this.engineInstance;
|
|
133390
133567
|
}
|
|
133391
133568
|
// Release the SQLite connection. No-op in legacy mode. Safe to call
|
|
133392
|
-
// repeatedly.
|
|
133393
|
-
//
|
|
133394
|
-
close()
|
|
133395
|
-
|
|
133569
|
+
// repeatedly. Awaits watcher teardown AND any in-flight scan before closing
|
|
133570
|
+
// the DB handle, so a fire-and-forget scan() can never have its connection
|
|
133571
|
+
// shut mid-indexAll() ("database connection is not open"). This is why close()
|
|
133572
|
+
// is async — callers should await it during shutdown. (Scanner review Bug #4.)
|
|
133573
|
+
async close() {
|
|
133574
|
+
if (this.watcher || this.queue || this.periodicTimer) await this.unwatch();
|
|
133575
|
+
if (this.inFlightScan) {
|
|
133576
|
+
try {
|
|
133577
|
+
await this.inFlightScan;
|
|
133578
|
+
} catch {
|
|
133579
|
+
}
|
|
133580
|
+
}
|
|
133396
133581
|
this.engineInstance?.close();
|
|
133397
133582
|
this.engineInstance = null;
|
|
133398
133583
|
}
|
|
133399
133584
|
async scan(options = {}) {
|
|
133585
|
+
const promise2 = this.runScan(options);
|
|
133586
|
+
this.inFlightScan = promise2;
|
|
133587
|
+
try {
|
|
133588
|
+
return await promise2;
|
|
133589
|
+
} finally {
|
|
133590
|
+
if (this.inFlightScan === promise2) this.inFlightScan = null;
|
|
133591
|
+
}
|
|
133592
|
+
}
|
|
133593
|
+
async runScan(options) {
|
|
133400
133594
|
const profiles = await this.resolveProfiles(options.profiles);
|
|
133401
133595
|
const activeProfiles = profiles.filter((p2) => p2.enabled && p2.scanHistory !== false);
|
|
133402
133596
|
this.lastTier = resolveTier(options.tier ?? "standard", options.tiers);
|
|
@@ -133710,7 +133904,7 @@ var ConversationScanner = class {
|
|
|
133710
133904
|
this.lastTier.name,
|
|
133711
133905
|
void 0,
|
|
133712
133906
|
readGitBranch,
|
|
133713
|
-
|
|
133907
|
+
false,
|
|
133714
133908
|
provider
|
|
133715
133909
|
);
|
|
133716
133910
|
evict2(meta22);
|
|
@@ -133832,7 +134026,10 @@ var ConversationScanner = class {
|
|
|
133832
134026
|
const periodicMs = options.periodicMs ?? 6e4;
|
|
133833
134027
|
if (periodicMs > 0) {
|
|
133834
134028
|
this.periodicTimer = setInterval(() => {
|
|
133835
|
-
|
|
134029
|
+
const job = this.periodicReconcile(activeProfiles).finally(() => {
|
|
134030
|
+
if (this.inFlightReconcile === job) this.inFlightReconcile = null;
|
|
134031
|
+
});
|
|
134032
|
+
this.inFlightReconcile = job;
|
|
133836
134033
|
}, periodicMs);
|
|
133837
134034
|
this.periodicTimer.unref?.();
|
|
133838
134035
|
}
|
|
@@ -133876,6 +134073,9 @@ var ConversationScanner = class {
|
|
|
133876
134073
|
clearInterval(this.periodicTimer);
|
|
133877
134074
|
this.periodicTimer = null;
|
|
133878
134075
|
}
|
|
134076
|
+
if (this.inFlightReconcile) {
|
|
134077
|
+
await this.inFlightReconcile;
|
|
134078
|
+
}
|
|
133879
134079
|
if (this.watcher) {
|
|
133880
134080
|
await this.watcher.stop();
|
|
133881
134081
|
this.watcher = null;
|
|
@@ -134018,7 +134218,7 @@ async function search(query, options, scanner) {
|
|
|
134018
134218
|
// src/server.ts
|
|
134019
134219
|
var import_events3 = require("events");
|
|
134020
134220
|
var import_fs24 = require("fs");
|
|
134021
|
-
var
|
|
134221
|
+
var import_promises15 = require("fs/promises");
|
|
134022
134222
|
var import_http = require("http");
|
|
134023
134223
|
var import_os10 = require("os");
|
|
134024
134224
|
var import_path21 = require("path");
|
|
@@ -134110,7 +134310,7 @@ function readAgentConfig(env = process.env) {
|
|
|
134110
134310
|
}
|
|
134111
134311
|
|
|
134112
134312
|
// src/agent/conversation-writer.ts
|
|
134113
|
-
var
|
|
134313
|
+
var import_promises8 = require("fs/promises");
|
|
134114
134314
|
var import_node_path6 = require("path");
|
|
134115
134315
|
function createConversationWriter(opts) {
|
|
134116
134316
|
const { baseDir } = opts;
|
|
@@ -134120,7 +134320,7 @@ function createConversationWriter(opts) {
|
|
|
134120
134320
|
throw new Error("ConversationWriter: refusing to write empty assistant turn");
|
|
134121
134321
|
}
|
|
134122
134322
|
const file2 = (0, import_node_path6.join)(baseDir, `${args.sessionId}.jsonl`);
|
|
134123
|
-
await (0,
|
|
134323
|
+
await (0, import_promises8.mkdir)((0, import_node_path6.dirname)(file2), { recursive: true });
|
|
134124
134324
|
const record2 = {
|
|
134125
134325
|
role: "assistant",
|
|
134126
134326
|
turnId: args.turnId,
|
|
@@ -134130,7 +134330,7 @@ function createConversationWriter(opts) {
|
|
|
134130
134330
|
};
|
|
134131
134331
|
const line = `${JSON.stringify(record2)}
|
|
134132
134332
|
`;
|
|
134133
|
-
await (0,
|
|
134333
|
+
await (0, import_promises8.appendFile)(file2, line, { encoding: "utf8" });
|
|
134134
134334
|
}
|
|
134135
134335
|
};
|
|
134136
134336
|
}
|
|
@@ -137048,7 +137248,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
|
|
|
137048
137248
|
init_auth();
|
|
137049
137249
|
|
|
137050
137250
|
// src/browse.ts
|
|
137051
|
-
var
|
|
137251
|
+
var import_promises9 = require("fs/promises");
|
|
137052
137252
|
var import_path10 = require("path");
|
|
137053
137253
|
var BrowsePathNotFoundError = class extends Error {
|
|
137054
137254
|
constructor(message) {
|
|
@@ -137070,7 +137270,7 @@ async function resolveBrowsePath(browseRoot, relativePath) {
|
|
|
137070
137270
|
throw new Error("Path outside browse root");
|
|
137071
137271
|
}
|
|
137072
137272
|
try {
|
|
137073
|
-
await (0,
|
|
137273
|
+
await (0, import_promises9.realpath)(target);
|
|
137074
137274
|
} catch (err) {
|
|
137075
137275
|
if (err?.code === "ENOENT") {
|
|
137076
137276
|
throw new BrowsePathNotFoundError(`Path not found: ${target}`);
|
|
@@ -137080,7 +137280,7 @@ async function resolveBrowsePath(browseRoot, relativePath) {
|
|
|
137080
137280
|
return target;
|
|
137081
137281
|
}
|
|
137082
137282
|
async function listDirectories(absolutePath) {
|
|
137083
|
-
const entries = await (0,
|
|
137283
|
+
const entries = await (0, import_promises9.readdir)(absolutePath, { withFileTypes: true });
|
|
137084
137284
|
return entries.filter((e) => e.isDirectory()).map((e) => ({ name: e.name })).sort((a, b2) => a.name.localeCompare(b2.name));
|
|
137085
137285
|
}
|
|
137086
137286
|
async function createDirectory(parentAbsolutePath, name) {
|
|
@@ -137089,12 +137289,12 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
137089
137289
|
}
|
|
137090
137290
|
const target = (0, import_path10.join)(parentAbsolutePath, name);
|
|
137091
137291
|
try {
|
|
137092
|
-
const s3 = await (0,
|
|
137292
|
+
const s3 = await (0, import_promises9.stat)(target);
|
|
137093
137293
|
if (s3.isDirectory()) throw new Error("Directory already exists");
|
|
137094
137294
|
} catch (err) {
|
|
137095
137295
|
if (err.code !== "ENOENT") throw err;
|
|
137096
137296
|
}
|
|
137097
|
-
await (0,
|
|
137297
|
+
await (0, import_promises9.mkdir)(target);
|
|
137098
137298
|
return target;
|
|
137099
137299
|
}
|
|
137100
137300
|
|
|
@@ -137904,6 +138104,61 @@ var ConversationCache = class _ConversationCache {
|
|
|
137904
138104
|
}
|
|
137905
138105
|
return ghosts;
|
|
137906
138106
|
}
|
|
138107
|
+
/**
|
|
138108
|
+
* Reconcile the cache against the authoritative set of conversation file
|
|
138109
|
+
* paths a fresh scan surfaced: drop any cached row whose `file_path` is not
|
|
138110
|
+
* in `livePaths` (removed from disk, or now filtered out — e.g. became an
|
|
138111
|
+
* agent JSONL). This is the "removed conversations" half of a ?refresh=1
|
|
138112
|
+
* reconcile; the additions/updates half is upsertFromScannerMeta.
|
|
138113
|
+
*
|
|
138114
|
+
* Skip semantics depend on whether the file still exists on disk:
|
|
138115
|
+
* - File GONE from disk → always removed, tail or not. This matches the old
|
|
138116
|
+
* invalidate()+rebuild behavior (a deleted conversation must disappear on
|
|
138117
|
+
* refresh) and keeps refresh=1 truthful about removals. NOTE: this is an
|
|
138118
|
+
* INTENTIONAL divergence from pruneGhostFiles(), which KEEPS tailed ghosts
|
|
138119
|
+
* so their cached history stays viewable on a background prune. refresh=1
|
|
138120
|
+
* has the opposite contract (mobile relies on removals being reflected), so
|
|
138121
|
+
* do not "unify" the two — they serve different purposes.
|
|
138122
|
+
* - File STILL on disk but absent from `livePaths` → the CRITICAL #2 race:
|
|
138123
|
+
* the scan snapshot predates a just-created (and now live-tailed) file.
|
|
138124
|
+
* A tailed row here is actively maintained from real content, so it is
|
|
138125
|
+
* kept — dropping it would flicker the active conversation out of
|
|
138126
|
+
* /api/conversations. An untailed on-disk row not in the snapshot is a
|
|
138127
|
+
* transient scan/discovery gap; it is left alone (not removed) and the
|
|
138128
|
+
* next reconcile picks it up, rather than risk removing a real file the
|
|
138129
|
+
* scan simply hasn't surfaced yet.
|
|
138130
|
+
* Returns the removed IDs.
|
|
138131
|
+
*/
|
|
138132
|
+
reconcileDeletions(livePaths, opts) {
|
|
138133
|
+
const exists = opts?.exists ?? import_fs16.existsSync;
|
|
138134
|
+
const rows = this.stmts.allFilePaths.all();
|
|
138135
|
+
const removed = [];
|
|
138136
|
+
const drop = this.db.transaction((ids) => {
|
|
138137
|
+
for (const id of ids) {
|
|
138138
|
+
this.stmts.deleteTailById.run(id);
|
|
138139
|
+
this.stmts.deleteById.run(id);
|
|
138140
|
+
}
|
|
138141
|
+
});
|
|
138142
|
+
for (const row of rows) {
|
|
138143
|
+
if (livePaths.has(row.file_path)) continue;
|
|
138144
|
+
if (exists(row.file_path)) continue;
|
|
138145
|
+
removed.push(row.id);
|
|
138146
|
+
}
|
|
138147
|
+
if (removed.length > 0) {
|
|
138148
|
+
drop(removed);
|
|
138149
|
+
if (this.fileIndexLoaded) {
|
|
138150
|
+
for (const id of removed) {
|
|
138151
|
+
for (const [fp, cid] of this.fileIndex) {
|
|
138152
|
+
if (cid === id) {
|
|
138153
|
+
this.fileIndex.delete(fp);
|
|
138154
|
+
break;
|
|
138155
|
+
}
|
|
138156
|
+
}
|
|
138157
|
+
}
|
|
138158
|
+
}
|
|
138159
|
+
}
|
|
138160
|
+
return removed;
|
|
138161
|
+
}
|
|
137907
138162
|
};
|
|
137908
138163
|
|
|
137909
138164
|
// src/db/config.ts
|
|
@@ -139932,11 +140187,11 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
139932
140187
|
// node_modules/chokidar/index.js
|
|
139933
140188
|
var import_node_events2 = require("events");
|
|
139934
140189
|
var import_node_fs7 = require("fs");
|
|
139935
|
-
var
|
|
140190
|
+
var import_promises12 = require("fs/promises");
|
|
139936
140191
|
var sp2 = __toESM(require("path"), 1);
|
|
139937
140192
|
|
|
139938
140193
|
// node_modules/readdirp/index.js
|
|
139939
|
-
var
|
|
140194
|
+
var import_promises10 = require("fs/promises");
|
|
139940
140195
|
var import_node_path9 = require("path");
|
|
139941
140196
|
var import_node_stream2 = require("stream");
|
|
139942
140197
|
var EntryTypes2 = {
|
|
@@ -140017,7 +140272,7 @@ var ReaddirpStream2 = class extends import_node_stream2.Readable {
|
|
|
140017
140272
|
const { root, type } = opts;
|
|
140018
140273
|
this._fileFilter = normalizeFilter2(opts.fileFilter);
|
|
140019
140274
|
this._directoryFilter = normalizeFilter2(opts.directoryFilter);
|
|
140020
|
-
const statMethod = opts.lstat ?
|
|
140275
|
+
const statMethod = opts.lstat ? import_promises10.lstat : import_promises10.stat;
|
|
140021
140276
|
if (wantBigintFsStats2) {
|
|
140022
140277
|
this._stat = (path2) => statMethod(path2, { bigint: true });
|
|
140023
140278
|
} else {
|
|
@@ -140088,7 +140343,7 @@ var ReaddirpStream2 = class extends import_node_stream2.Readable {
|
|
|
140088
140343
|
async _exploreDir(path2, depth) {
|
|
140089
140344
|
let files;
|
|
140090
140345
|
try {
|
|
140091
|
-
files = await (0,
|
|
140346
|
+
files = await (0, import_promises10.readdir)(path2, this._rdOptions);
|
|
140092
140347
|
} catch (error51) {
|
|
140093
140348
|
this._onError(error51);
|
|
140094
140349
|
}
|
|
@@ -140126,8 +140381,8 @@ var ReaddirpStream2 = class extends import_node_stream2.Readable {
|
|
|
140126
140381
|
if (stats && stats.isSymbolicLink()) {
|
|
140127
140382
|
const full = entry.fullPath;
|
|
140128
140383
|
try {
|
|
140129
|
-
const entryRealPath = await (0,
|
|
140130
|
-
const entryRealPathStats = await (0,
|
|
140384
|
+
const entryRealPath = await (0, import_promises10.realpath)(full);
|
|
140385
|
+
const entryRealPathStats = await (0, import_promises10.lstat)(entryRealPath);
|
|
140131
140386
|
if (entryRealPathStats.isFile()) {
|
|
140132
140387
|
return "file";
|
|
140133
140388
|
}
|
|
@@ -140170,7 +140425,7 @@ function readdirp2(root, options = {}) {
|
|
|
140170
140425
|
|
|
140171
140426
|
// node_modules/chokidar/handler.js
|
|
140172
140427
|
var import_node_fs6 = require("fs");
|
|
140173
|
-
var
|
|
140428
|
+
var import_promises11 = require("fs/promises");
|
|
140174
140429
|
var import_node_os3 = require("os");
|
|
140175
140430
|
var sp = __toESM(require("path"), 1);
|
|
140176
140431
|
var STR_DATA2 = "data";
|
|
@@ -140197,7 +140452,7 @@ var EVENTS2 = {
|
|
|
140197
140452
|
};
|
|
140198
140453
|
var EV2 = EVENTS2;
|
|
140199
140454
|
var THROTTLE_MODE_WATCH2 = "watch";
|
|
140200
|
-
var statMethods2 = { lstat:
|
|
140455
|
+
var statMethods2 = { lstat: import_promises11.lstat, stat: import_promises11.stat };
|
|
140201
140456
|
var KEY_LISTENERS2 = "listeners";
|
|
140202
140457
|
var KEY_ERR2 = "errHandlers";
|
|
140203
140458
|
var KEY_RAW2 = "rawEmitters";
|
|
@@ -140554,7 +140809,7 @@ var setFsWatchListener2 = (path2, fullPath, options, handlers) => {
|
|
|
140554
140809
|
cont.watcherUnusable = true;
|
|
140555
140810
|
if (isWindows3 && error51.code === "EPERM") {
|
|
140556
140811
|
try {
|
|
140557
|
-
const fd = await (0,
|
|
140812
|
+
const fd = await (0, import_promises11.open)(path2, "r");
|
|
140558
140813
|
await fd.close();
|
|
140559
140814
|
broadcastErr(error51);
|
|
140560
140815
|
} catch (err) {
|
|
@@ -140685,7 +140940,7 @@ var NodeFsHandler2 = class {
|
|
|
140685
140940
|
return;
|
|
140686
140941
|
if (!newStats || newStats.mtimeMs === 0) {
|
|
140687
140942
|
try {
|
|
140688
|
-
const newStats2 = await (0,
|
|
140943
|
+
const newStats2 = await (0, import_promises11.stat)(file2);
|
|
140689
140944
|
if (this.fsw.closed)
|
|
140690
140945
|
return;
|
|
140691
140946
|
const at2 = newStats2.atimeMs;
|
|
@@ -140740,7 +140995,7 @@ var NodeFsHandler2 = class {
|
|
|
140740
140995
|
this.fsw._incrReadyCount();
|
|
140741
140996
|
let linkPath;
|
|
140742
140997
|
try {
|
|
140743
|
-
linkPath = await (0,
|
|
140998
|
+
linkPath = await (0, import_promises11.realpath)(path2);
|
|
140744
140999
|
} catch (e) {
|
|
140745
141000
|
this.fsw._emitReady();
|
|
140746
141001
|
return true;
|
|
@@ -140889,7 +141144,7 @@ var NodeFsHandler2 = class {
|
|
|
140889
141144
|
let closer;
|
|
140890
141145
|
if (stats.isDirectory()) {
|
|
140891
141146
|
const absPath = sp.resolve(path2);
|
|
140892
|
-
const targetPath = follow ? await (0,
|
|
141147
|
+
const targetPath = follow ? await (0, import_promises11.realpath)(path2) : path2;
|
|
140893
141148
|
if (this.fsw.closed)
|
|
140894
141149
|
return;
|
|
140895
141150
|
closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
|
|
@@ -140899,7 +141154,7 @@ var NodeFsHandler2 = class {
|
|
|
140899
141154
|
this.fsw._symlinkPaths.set(absPath, targetPath);
|
|
140900
141155
|
}
|
|
140901
141156
|
} else if (stats.isSymbolicLink()) {
|
|
140902
|
-
const targetPath = follow ? await (0,
|
|
141157
|
+
const targetPath = follow ? await (0, import_promises11.realpath)(path2) : path2;
|
|
140903
141158
|
if (this.fsw.closed)
|
|
140904
141159
|
return;
|
|
140905
141160
|
const parent = sp.dirname(wh.watchPath);
|
|
@@ -141059,7 +141314,7 @@ var DirEntry2 = class {
|
|
|
141059
141314
|
return;
|
|
141060
141315
|
const dir = this.path;
|
|
141061
141316
|
try {
|
|
141062
|
-
await (0,
|
|
141317
|
+
await (0, import_promises12.readdir)(dir);
|
|
141063
141318
|
} catch (err) {
|
|
141064
141319
|
if (this._removeWatcher) {
|
|
141065
141320
|
this._removeWatcher(sp2.dirname(dir), sp2.basename(dir));
|
|
@@ -141409,7 +141664,7 @@ var FSWatcher2 = class extends import_node_events2.EventEmitter {
|
|
|
141409
141664
|
const fullPath = opts.cwd ? sp2.join(opts.cwd, path2) : path2;
|
|
141410
141665
|
let stats2;
|
|
141411
141666
|
try {
|
|
141412
|
-
stats2 = await (0,
|
|
141667
|
+
stats2 = await (0, import_promises12.stat)(fullPath);
|
|
141413
141668
|
} catch (err) {
|
|
141414
141669
|
}
|
|
141415
141670
|
if (!stats2 || this.closed)
|
|
@@ -141663,7 +141918,7 @@ var chokidar_default = { watch: watch2, FSWatcher: FSWatcher2 };
|
|
|
141663
141918
|
|
|
141664
141919
|
// src/services/conversations/conversationWatcher.ts
|
|
141665
141920
|
var import_fs22 = require("fs");
|
|
141666
|
-
var
|
|
141921
|
+
var import_promises13 = require("fs/promises");
|
|
141667
141922
|
var ConversationWatcher = class {
|
|
141668
141923
|
files = /* @__PURE__ */ new Map();
|
|
141669
141924
|
directories = /* @__PURE__ */ new Map();
|
|
@@ -141767,12 +142022,12 @@ var ConversationWatcher = class {
|
|
|
141767
142022
|
entry.reading = true;
|
|
141768
142023
|
try {
|
|
141769
142024
|
for (; ; ) {
|
|
141770
|
-
const st2 = await (0,
|
|
142025
|
+
const st2 = await (0, import_promises13.stat)(filePath);
|
|
141771
142026
|
if (st2.size <= entry.offset) break;
|
|
141772
142027
|
const readFrom = entry.offset;
|
|
141773
142028
|
const bytesToRead = st2.size - readFrom;
|
|
141774
142029
|
const buf = Buffer.alloc(bytesToRead);
|
|
141775
|
-
const fh = await (0,
|
|
142030
|
+
const fh = await (0, import_promises13.open)(filePath, "r");
|
|
141776
142031
|
try {
|
|
141777
142032
|
await fh.read(buf, 0, bytesToRead, readFrom);
|
|
141778
142033
|
} finally {
|
|
@@ -142172,7 +142427,7 @@ function discoveredToResponse(d, conversationId) {
|
|
|
142172
142427
|
|
|
142173
142428
|
// src/uploads.ts
|
|
142174
142429
|
var import_crypto8 = require("crypto");
|
|
142175
|
-
var
|
|
142430
|
+
var import_promises14 = require("fs/promises");
|
|
142176
142431
|
var import_heic_convert = __toESM(require_heic_convert(), 1);
|
|
142177
142432
|
var import_path20 = require("path");
|
|
142178
142433
|
var UPLOAD_DIR_NAME = ".threadbase-uploads";
|
|
@@ -142208,9 +142463,9 @@ async function saveUploadFile(input) {
|
|
|
142208
142463
|
const id = `up_${(0, import_crypto8.randomBytes)(8).toString("hex")}`;
|
|
142209
142464
|
const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
|
|
142210
142465
|
const dir = (0, import_path20.join)(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
|
|
142211
|
-
await (0,
|
|
142466
|
+
await (0, import_promises14.mkdir)(dir, { recursive: true });
|
|
142212
142467
|
const filePath = (0, import_path20.join)(dir, `${Date.now()}-${id}-${safeName}`);
|
|
142213
|
-
await (0,
|
|
142468
|
+
await (0, import_promises14.writeFile)(filePath, buffer);
|
|
142214
142469
|
return {
|
|
142215
142470
|
id,
|
|
142216
142471
|
filePath,
|
|
@@ -142666,6 +142921,14 @@ var StreamerServer = class {
|
|
|
142666
142921
|
// window so the self-healing kickstart-relaunch race doesn't spam warn.
|
|
142667
142922
|
binding = false;
|
|
142668
142923
|
cacheReady = false;
|
|
142924
|
+
// Every fire-and-forget task that runs a scan and then writes to this.cache
|
|
142925
|
+
// in an async continuation (startup warm-up, background count refresh, …).
|
|
142926
|
+
// close() awaits all of them before closing this.cache, so a scan's post-scan
|
|
142927
|
+
// cache writes (upsertFromScannerMeta / populateTailFromFile / pruneGhostFiles
|
|
142928
|
+
// / reconcileDeletions) can never hit a cache.db that was already closed
|
|
142929
|
+
// ("database connection is not open"), which would otherwise leave the cache
|
|
142930
|
+
// empty. Register via trackCacheWrite(); each entry removes itself on settle.
|
|
142931
|
+
inFlightCacheWrites = /* @__PURE__ */ new Set();
|
|
142669
142932
|
apiKey;
|
|
142670
142933
|
apiKeySource;
|
|
142671
142934
|
localNoAuth;
|
|
@@ -142741,7 +143004,7 @@ var StreamerServer = class {
|
|
|
142741
143004
|
this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);
|
|
142742
143005
|
const rawRoot = process.env.THREADBASE_BROWSE_ROOT ?? loadBrowseRoot() ?? config2.browseRoot;
|
|
142743
143006
|
if (rawRoot) {
|
|
142744
|
-
(0,
|
|
143007
|
+
(0, import_promises15.realpath)(rawRoot).then((resolved) => {
|
|
142745
143008
|
this.browseRoot = resolved;
|
|
142746
143009
|
if (this.verbose) this.log.info(`Browse root: ${resolved}`, { browseRoot: resolved });
|
|
142747
143010
|
}).catch(() => {
|
|
@@ -143234,6 +143497,7 @@ var StreamerServer = class {
|
|
|
143234
143497
|
});
|
|
143235
143498
|
}
|
|
143236
143499
|
});
|
|
143500
|
+
this.trackCacheWrite(warmUp);
|
|
143237
143501
|
if (opts?.awaitReady) await warmUp;
|
|
143238
143502
|
}
|
|
143239
143503
|
// Bind the HTTP listener, retrying on a transient EADDRINUSE. See the call
|
|
@@ -143281,11 +143545,23 @@ var StreamerServer = class {
|
|
|
143281
143545
|
}
|
|
143282
143546
|
}
|
|
143283
143547
|
}
|
|
143548
|
+
// Register a fire-and-forget task that writes to this.cache after a scan, so
|
|
143549
|
+
// close() can await it before closing cache.db. Removes itself on settle. The
|
|
143550
|
+
// caller keeps its own error handling; this wrapper swallows rejections so a
|
|
143551
|
+
// failed task never rejects close()'s Promise.all.
|
|
143552
|
+
trackCacheWrite(task) {
|
|
143553
|
+
const guarded = task.catch(() => void 0);
|
|
143554
|
+
this.inFlightCacheWrites.add(guarded);
|
|
143555
|
+
void guarded.finally(() => {
|
|
143556
|
+
this.inFlightCacheWrites.delete(guarded);
|
|
143557
|
+
});
|
|
143558
|
+
}
|
|
143284
143559
|
async close() {
|
|
143285
143560
|
for (const timer of this.ptyGraceTimers.values()) clearTimeout(timer);
|
|
143286
143561
|
this.ptyGraceTimers.clear();
|
|
143287
143562
|
this.markScannerStaleDebounced.cancel();
|
|
143288
|
-
|
|
143563
|
+
await Promise.all([...this.inFlightCacheWrites]);
|
|
143564
|
+
await Promise.all([...this.allScanners].map((s3) => s3.close()));
|
|
143289
143565
|
this.allScanners.clear();
|
|
143290
143566
|
this.scanner = null;
|
|
143291
143567
|
this.cache?.close();
|
|
@@ -143425,12 +143701,23 @@ var StreamerServer = class {
|
|
|
143425
143701
|
const project = url2.searchParams.get("project") ?? void 0;
|
|
143426
143702
|
const providerFilter = url2.searchParams.get("provider") ?? void 0;
|
|
143427
143703
|
const bustCache = url2.searchParams.get("refresh") === "1";
|
|
143428
|
-
if (bustCache) {
|
|
143429
|
-
this.
|
|
143430
|
-
|
|
143431
|
-
|
|
143704
|
+
if (bustCache && this.cache) {
|
|
143705
|
+
const scanner2 = await this.rescanForRefresh();
|
|
143706
|
+
const metas2 = [...scanner2.getMetadataCache().values()];
|
|
143707
|
+
try {
|
|
143708
|
+
this.cache.upsertFromScannerMeta(metas2);
|
|
143709
|
+
const livePaths = new Set(
|
|
143710
|
+
metas2.map((m2) => m2.filePath).filter((p2) => Boolean(p2))
|
|
143711
|
+
);
|
|
143712
|
+
this.cache.reconcileDeletions(livePaths);
|
|
143713
|
+
} catch (err) {
|
|
143714
|
+
this.log.warn(
|
|
143715
|
+
`refresh reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
143716
|
+
{ event: "conversations.reconcile_failed" }
|
|
143717
|
+
);
|
|
143718
|
+
}
|
|
143432
143719
|
}
|
|
143433
|
-
if (this.cache
|
|
143720
|
+
if (this.cache) {
|
|
143434
143721
|
const { conversations, total: total2 } = this.cache.listConversations({
|
|
143435
143722
|
project,
|
|
143436
143723
|
provider: providerFilter,
|
|
@@ -143495,12 +143782,6 @@ var StreamerServer = class {
|
|
|
143495
143782
|
};
|
|
143496
143783
|
});
|
|
143497
143784
|
json2(res, 200, { conversations: adapted, hasMore: offset + limit < total, offset, total });
|
|
143498
|
-
if (this.cache && bustCache) {
|
|
143499
|
-
try {
|
|
143500
|
-
this.cache.upsertFromScannerMeta([...scanner.getMetadataCache().values()]);
|
|
143501
|
-
} catch {
|
|
143502
|
-
}
|
|
143503
|
-
}
|
|
143504
143785
|
}
|
|
143505
143786
|
async handleConversationsCount(url2, res) {
|
|
143506
143787
|
const project = url2.searchParams.get("project") ?? void 0;
|
|
@@ -143529,19 +143810,21 @@ var StreamerServer = class {
|
|
|
143529
143810
|
// later count reflects new/removed conversations. Never awaited by the request
|
|
143530
143811
|
// path — refresh=1 returns the cached total synchronously and this catches up.
|
|
143531
143812
|
refreshCountInBackground() {
|
|
143532
|
-
|
|
143533
|
-
|
|
143534
|
-
|
|
143535
|
-
|
|
143536
|
-
this.cache
|
|
143813
|
+
this.trackCacheWrite(
|
|
143814
|
+
(async () => {
|
|
143815
|
+
try {
|
|
143816
|
+
const scanner = await this.getFreshScanner();
|
|
143817
|
+
if (this.cache) {
|
|
143818
|
+
this.cache.upsertFromScannerMeta([...scanner.getMetadataCache().values()]);
|
|
143819
|
+
}
|
|
143820
|
+
} catch (err) {
|
|
143821
|
+
this.log.warn(
|
|
143822
|
+
`Background count refresh failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
143823
|
+
{ event: "count.refresh_failed" }
|
|
143824
|
+
);
|
|
143537
143825
|
}
|
|
143538
|
-
}
|
|
143539
|
-
|
|
143540
|
-
`Background count refresh failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
143541
|
-
{ event: "count.refresh_failed" }
|
|
143542
|
-
);
|
|
143543
|
-
}
|
|
143544
|
-
})();
|
|
143826
|
+
})()
|
|
143827
|
+
);
|
|
143545
143828
|
}
|
|
143546
143829
|
handleSessionsCount(res) {
|
|
143547
143830
|
json2(res, 200, { total: this.sessionStore.list(this.ptyAttachedIds()).length });
|
|
@@ -143654,6 +143937,30 @@ var StreamerServer = class {
|
|
|
143654
143937
|
this.scannerReady = null;
|
|
143655
143938
|
return this.getScanner();
|
|
143656
143939
|
}
|
|
143940
|
+
// refresh=1's scan: reuse the WARM persistent scanner (its index.db + cursors
|
|
143941
|
+
// survive, so classify() still skips unchanged files) and re-run its scan
|
|
143942
|
+
// with fullRescan:true — the escape hatch that bypasses the scanner's
|
|
143943
|
+
// dir-mtime discovery gate, since an explicit user pull-to-refresh is exactly
|
|
143944
|
+
// the "don't trust the gate, check disk for real" signal. Unlike
|
|
143945
|
+
// getFreshScanner() this does NOT discard the warm scanner. scannerReady is
|
|
143946
|
+
// only ever reassigned to a live scan promise (never nulled mid-scan), so the
|
|
143947
|
+
// getScanner() anti-infinite-loop guard is preserved.
|
|
143948
|
+
async rescanForRefresh() {
|
|
143949
|
+
if (this.scannerReady) await this.scannerReady;
|
|
143950
|
+
this.scannerStale = false;
|
|
143951
|
+
if (!this.scanner) {
|
|
143952
|
+
this.scanner = new ConversationScanner();
|
|
143953
|
+
this.allScanners.add(this.scanner);
|
|
143954
|
+
}
|
|
143955
|
+
const scanner = this.scanner;
|
|
143956
|
+
this.scannerReady = scanner.scan({
|
|
143957
|
+
...this.scanProfiles ? { profiles: this.scanProfiles } : {},
|
|
143958
|
+
...this.codexScanOpts(),
|
|
143959
|
+
fullRescan: true
|
|
143960
|
+
});
|
|
143961
|
+
await this.scannerReady;
|
|
143962
|
+
return scanner;
|
|
143963
|
+
}
|
|
143657
143964
|
findJsonlPath(uuid3) {
|
|
143658
143965
|
const projectsDir = (0, import_path21.join)((0, import_os10.homedir)(), ".claude", "projects");
|
|
143659
143966
|
if (!(0, import_fs24.existsSync)(projectsDir)) return null;
|
|
@@ -145066,7 +145373,7 @@ function isBrewInstall(scriptPath = process.argv[1] ?? "") {
|
|
|
145066
145373
|
var import_node_crypto4 = require("crypto");
|
|
145067
145374
|
var import_node_fs8 = require("fs");
|
|
145068
145375
|
var import_node_path10 = require("path");
|
|
145069
|
-
var
|
|
145376
|
+
var import_promises16 = require("stream/promises");
|
|
145070
145377
|
|
|
145071
145378
|
// src/updater/manifest.ts
|
|
145072
145379
|
init_zod();
|
|
@@ -145141,7 +145448,7 @@ async function downloadAndVerify(opts) {
|
|
|
145141
145448
|
}
|
|
145142
145449
|
});
|
|
145143
145450
|
try {
|
|
145144
|
-
await (0,
|
|
145451
|
+
await (0, import_promises16.pipeline)(res.body.pipeThrough(measured), out);
|
|
145145
145452
|
} catch (err) {
|
|
145146
145453
|
(0, import_node_fs8.rmSync)(targetPath, { force: true });
|
|
145147
145454
|
throw err;
|
|
@@ -145355,7 +145662,7 @@ var import_fs28 = __toESM(require("fs"), 1);
|
|
|
145355
145662
|
var import_node_fs14 = __toESM(require("fs"), 1);
|
|
145356
145663
|
var import_node_path19 = __toESM(require("path"), 1);
|
|
145357
145664
|
var import_node_fs15 = __toESM(require("fs"), 1);
|
|
145358
|
-
var
|
|
145665
|
+
var import_promises17 = __toESM(require("fs/promises"), 1);
|
|
145359
145666
|
var import_node_path20 = __toESM(require("path"), 1);
|
|
145360
145667
|
var import_node_path21 = require("path");
|
|
145361
145668
|
var import_node_fs16 = __toESM(require("fs"), 1);
|
|
@@ -147587,7 +147894,7 @@ var gr = (s3, t2, e) => {
|
|
|
147587
147894
|
E2 ? e(E2) : x2 && a ? Es(x2, o, h, (Le) => S2(Le)) : n ? import_node_fs15.default.chmod(s3, r, e) : e();
|
|
147588
147895
|
};
|
|
147589
147896
|
if (s3 === d) return no(s3, S2);
|
|
147590
|
-
if (l) return
|
|
147897
|
+
if (l) return import_promises17.default.mkdir(s3, { mode: r, recursive: true }).then((E2) => S2(null, E2 ?? void 0), S2);
|
|
147591
147898
|
let D2 = f(import_node_path20.default.relative(d, s3)).split("/");
|
|
147592
147899
|
Ss(d, D2, r, c, d, void 0, S2);
|
|
147593
147900
|
};
|