@threadbase-sh/streamer 1.27.3 → 1.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +495 -54
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +464 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -1
- package/dist/index.d.ts +117 -1
- package/dist/index.js +445 -11
- package/dist/index.js.map +1 -1
- package/dist/migrations/009_create_offset_index.sql +28 -0
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -30931,7 +30931,7 @@ var require_websocket = __commonJS({
|
|
|
30931
30931
|
var http = require("http");
|
|
30932
30932
|
var net = require("net");
|
|
30933
30933
|
var tls = require("tls");
|
|
30934
|
-
var { randomBytes: randomBytes4, createHash:
|
|
30934
|
+
var { randomBytes: randomBytes4, createHash: createHash5 } = require("crypto");
|
|
30935
30935
|
var { Duplex, Readable: Readable3 } = require("stream");
|
|
30936
30936
|
var { URL: URL2 } = require("url");
|
|
30937
30937
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -31599,7 +31599,7 @@ var require_websocket = __commonJS({
|
|
|
31599
31599
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
31600
31600
|
return;
|
|
31601
31601
|
}
|
|
31602
|
-
const digest =
|
|
31602
|
+
const digest = createHash5("sha1").update(key + GUID).digest("base64");
|
|
31603
31603
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
31604
31604
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
31605
31605
|
return;
|
|
@@ -31968,7 +31968,7 @@ var require_websocket_server = __commonJS({
|
|
|
31968
31968
|
var EventEmitter6 = require("events");
|
|
31969
31969
|
var http = require("http");
|
|
31970
31970
|
var { Duplex } = require("stream");
|
|
31971
|
-
var { createHash:
|
|
31971
|
+
var { createHash: createHash5 } = require("crypto");
|
|
31972
31972
|
var extension2 = require_extension();
|
|
31973
31973
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
31974
31974
|
var subprotocol2 = require_subprotocol();
|
|
@@ -32275,7 +32275,7 @@ var require_websocket_server = __commonJS({
|
|
|
32275
32275
|
);
|
|
32276
32276
|
}
|
|
32277
32277
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
32278
|
-
const digest =
|
|
32278
|
+
const digest = createHash5("sha1").update(key + GUID).digest("base64");
|
|
32279
32279
|
const headers = [
|
|
32280
32280
|
"HTTP/1.1 101 Switching Protocols",
|
|
32281
32281
|
"Upgrade: websocket",
|
|
@@ -123750,14 +123750,14 @@ __export(prompt_exports, {
|
|
|
123750
123750
|
interactivePermissionModePrompt: () => interactivePermissionModePrompt,
|
|
123751
123751
|
interactivePrompt: () => interactivePrompt
|
|
123752
123752
|
});
|
|
123753
|
-
var import_node_process2,
|
|
123753
|
+
var import_node_process2, import_promises22, interactivePrompt, interactivePermissionModePrompt;
|
|
123754
123754
|
var init_prompt = __esm({
|
|
123755
123755
|
"src/lifecycle/prompt.ts"() {
|
|
123756
123756
|
"use strict";
|
|
123757
123757
|
import_node_process2 = require("process");
|
|
123758
|
-
|
|
123758
|
+
import_promises22 = require("readline/promises");
|
|
123759
123759
|
interactivePrompt = async ({ prodPort, suggestedAltPort, prodActive }) => {
|
|
123760
|
-
const rl = (0,
|
|
123760
|
+
const rl = (0, import_promises22.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
|
|
123761
123761
|
try {
|
|
123762
123762
|
if (!prodActive) {
|
|
123763
123763
|
import_node_process2.stdout.write(
|
|
@@ -123792,7 +123792,7 @@ The supervised prod streamer is already holding port ${prodPort}.
|
|
|
123792
123792
|
}
|
|
123793
123793
|
};
|
|
123794
123794
|
interactivePermissionModePrompt = async () => {
|
|
123795
|
-
const rl = (0,
|
|
123795
|
+
const rl = (0, import_promises22.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
|
|
123796
123796
|
try {
|
|
123797
123797
|
import_node_process2.stdout.write(
|
|
123798
123798
|
"\nWhich permission mode should spawned Claude Code sessions use?\n [a] acceptEdits \u2014 auto-approve file edits, still prompt for shell commands (default)\n [m] manual \u2014 prompt for every edit and command\n"
|
|
@@ -130940,6 +130940,17 @@ function reduceConvLine(state, entry) {
|
|
|
130940
130940
|
attachment: entry.attachment !== void 0 ? entry.attachment : void 0
|
|
130941
130941
|
};
|
|
130942
130942
|
}
|
|
130943
|
+
function parseJsonlLine(line, state = initialConvState()) {
|
|
130944
|
+
const text = line.trimEnd();
|
|
130945
|
+
if (text.trim().length === 0) return null;
|
|
130946
|
+
let entry;
|
|
130947
|
+
try {
|
|
130948
|
+
entry = JSON.parse(text);
|
|
130949
|
+
} catch {
|
|
130950
|
+
return null;
|
|
130951
|
+
}
|
|
130952
|
+
return reduceConvLine(state, entry);
|
|
130953
|
+
}
|
|
130943
130954
|
function applyTeamInfo(messages, state) {
|
|
130944
130955
|
if (Object.keys(state.teamInfo).length === 0) return;
|
|
130945
130956
|
for (const m2 of messages) {
|
|
@@ -133520,7 +133531,7 @@ async function search(query, options, scanner) {
|
|
|
133520
133531
|
// src/server.ts
|
|
133521
133532
|
var import_events3 = require("events");
|
|
133522
133533
|
var import_fs25 = require("fs");
|
|
133523
|
-
var
|
|
133534
|
+
var import_promises19 = require("fs/promises");
|
|
133524
133535
|
var import_http = require("http");
|
|
133525
133536
|
var import_os10 = require("os");
|
|
133526
133537
|
var import_path22 = require("path");
|
|
@@ -136666,7 +136677,9 @@ async function createDirectory(parentAbsolutePath, name) {
|
|
|
136666
136677
|
// src/conversation-cache.ts
|
|
136667
136678
|
var import_better_sqlite32 = __toESM(require("better-sqlite3"), 1);
|
|
136668
136679
|
var import_fs17 = require("fs");
|
|
136680
|
+
var import_promises12 = require("fs/promises");
|
|
136669
136681
|
var import_path13 = require("path");
|
|
136682
|
+
var import_promises13 = require("timers/promises");
|
|
136670
136683
|
|
|
136671
136684
|
// src/db/sqlite-migrate.ts
|
|
136672
136685
|
var import_fs15 = require("fs");
|
|
@@ -136792,6 +136805,33 @@ function parseAgentEntrypointsEnv(raw2) {
|
|
|
136792
136805
|
return new Set(parts);
|
|
136793
136806
|
}
|
|
136794
136807
|
|
|
136808
|
+
// src/utils/fileIdentity.ts
|
|
136809
|
+
var import_crypto5 = require("crypto");
|
|
136810
|
+
function fileIdentity(stat10, headBytes) {
|
|
136811
|
+
if (stat10.ino && stat10.ino > 0) return `inode:${stat10.dev}:${stat10.ino}`;
|
|
136812
|
+
const head = headBytes ?? Buffer.alloc(0);
|
|
136813
|
+
return `fp:${(0, import_crypto5.createHash)("sha1").update(head).digest("hex")}`;
|
|
136814
|
+
}
|
|
136815
|
+
function splitCompleteLines(buf, baseOffset) {
|
|
136816
|
+
const spans = [];
|
|
136817
|
+
let lineStart = 0;
|
|
136818
|
+
let consumed = 0;
|
|
136819
|
+
for (let i = 0; i < buf.length; i++) {
|
|
136820
|
+
if (buf[i] !== 10) continue;
|
|
136821
|
+
const lineLen = i - lineStart;
|
|
136822
|
+
if (lineLen > 0) {
|
|
136823
|
+
spans.push({
|
|
136824
|
+
byteOffset: baseOffset + lineStart,
|
|
136825
|
+
byteLength: lineLen,
|
|
136826
|
+
text: buf.toString("utf-8", lineStart, i)
|
|
136827
|
+
});
|
|
136828
|
+
}
|
|
136829
|
+
lineStart = i + 1;
|
|
136830
|
+
consumed = lineStart;
|
|
136831
|
+
}
|
|
136832
|
+
return { spans, consumed };
|
|
136833
|
+
}
|
|
136834
|
+
|
|
136795
136835
|
// src/conversation-cache.ts
|
|
136796
136836
|
function shortProjectName(fullPath) {
|
|
136797
136837
|
const parts = fullPath.split(/[/\\]/).filter(Boolean);
|
|
@@ -136840,6 +136880,14 @@ var ConversationCache = class _ConversationCache {
|
|
|
136840
136880
|
tailSize;
|
|
136841
136881
|
fileIndex = /* @__PURE__ */ new Map();
|
|
136842
136882
|
fileIndexLoaded = false;
|
|
136883
|
+
// Per-file scanner parse state for the incremental offset-index writer. The
|
|
136884
|
+
// reducer is stateful across lines (pending tool_uses, latest timestamp), so
|
|
136885
|
+
// it must persist between watcher reads of the same file. Cleared on
|
|
136886
|
+
// truncation/backfill.
|
|
136887
|
+
indexParseState = /* @__PURE__ */ new Map();
|
|
136888
|
+
// Single-flight guard for backfillIndex — concurrent detail requests for the
|
|
136889
|
+
// same cold file await one walk, not N. Entry dropped on settle.
|
|
136890
|
+
backfillInFlight = /* @__PURE__ */ new Map();
|
|
136843
136891
|
// Monotonically increasing counter for tail updated_at — guarantees strict
|
|
136844
136892
|
// ordering even when multiple updateFromLine() calls land within the same ms.
|
|
136845
136893
|
tailSeq = Date.now();
|
|
@@ -137003,6 +137051,42 @@ var ConversationCache = class _ConversationCache {
|
|
|
137003
137051
|
GROUP BY project_path
|
|
137004
137052
|
ORDER BY cnt DESC
|
|
137005
137053
|
LIMIT ?`
|
|
137054
|
+
),
|
|
137055
|
+
getFileState: db.prepare("SELECT * FROM conversation_file_state WHERE path = ?"),
|
|
137056
|
+
upsertFileState: db.prepare(
|
|
137057
|
+
`INSERT INTO conversation_file_state
|
|
137058
|
+
(path, identity, size, mtime_ms, byte_offset, last_message_index)
|
|
137059
|
+
VALUES (@path, @identity, @size, @mtime_ms, @byte_offset, @last_message_index)
|
|
137060
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
137061
|
+
identity = excluded.identity,
|
|
137062
|
+
size = excluded.size,
|
|
137063
|
+
mtime_ms = excluded.mtime_ms,
|
|
137064
|
+
byte_offset = excluded.byte_offset,
|
|
137065
|
+
last_message_index = excluded.last_message_index`
|
|
137066
|
+
),
|
|
137067
|
+
deleteFileState: db.prepare("DELETE FROM conversation_file_state WHERE path = ?"),
|
|
137068
|
+
deleteMessageIndex: db.prepare(
|
|
137069
|
+
"DELETE FROM conversation_message_index WHERE conversation_id = ?"
|
|
137070
|
+
),
|
|
137071
|
+
insertMessageIndexRow: db.prepare(
|
|
137072
|
+
`INSERT INTO conversation_message_index
|
|
137073
|
+
(conversation_id, message_index, byte_offset, byte_length, uuid, role, ts)
|
|
137074
|
+
VALUES (@conversation_id, @message_index, @byte_offset, @byte_length, @uuid, @role, @ts)
|
|
137075
|
+
ON CONFLICT(conversation_id, message_index) DO UPDATE SET
|
|
137076
|
+
byte_offset = excluded.byte_offset,
|
|
137077
|
+
byte_length = excluded.byte_length,
|
|
137078
|
+
uuid = excluded.uuid,
|
|
137079
|
+
role = excluded.role,
|
|
137080
|
+
ts = excluded.ts`
|
|
137081
|
+
),
|
|
137082
|
+
getMessageIndexWindow: db.prepare(
|
|
137083
|
+
`SELECT message_index, byte_offset, byte_length, uuid, role, ts
|
|
137084
|
+
FROM conversation_message_index
|
|
137085
|
+
WHERE conversation_id = ? AND message_index >= ? AND message_index < ?
|
|
137086
|
+
ORDER BY message_index ASC`
|
|
137087
|
+
),
|
|
137088
|
+
getIndexedMessageCount: db.prepare(
|
|
137089
|
+
"SELECT COUNT(*) as cnt FROM conversation_message_index WHERE conversation_id = ?"
|
|
137006
137090
|
)
|
|
137007
137091
|
};
|
|
137008
137092
|
}
|
|
@@ -137013,6 +137097,273 @@ var ConversationCache = class _ConversationCache {
|
|
|
137013
137097
|
getDatabase() {
|
|
137014
137098
|
return this.db;
|
|
137015
137099
|
}
|
|
137100
|
+
// ── Offset index (design 1b) ────────────────────────────────────────────
|
|
137101
|
+
// conversation_file_state + conversation_message_index back the windowed
|
|
137102
|
+
// detail read path (SQL window select + pread of byte ranges). All methods
|
|
137103
|
+
// are thin wrappers over the prepared statements above.
|
|
137104
|
+
getFileState(path2) {
|
|
137105
|
+
return this.stmts.getFileState.get(path2) ?? null;
|
|
137106
|
+
}
|
|
137107
|
+
upsertFileState(row) {
|
|
137108
|
+
this.stmts.upsertFileState.run(row);
|
|
137109
|
+
}
|
|
137110
|
+
/** Drop a file's index rows + file_state (truncation / identity change). */
|
|
137111
|
+
deleteFileIndex(path2, conversationId) {
|
|
137112
|
+
const tx = this.db.transaction(() => {
|
|
137113
|
+
this.stmts.deleteMessageIndex.run(conversationId);
|
|
137114
|
+
this.stmts.deleteFileState.run(path2);
|
|
137115
|
+
});
|
|
137116
|
+
tx();
|
|
137117
|
+
this.indexParseState.delete(path2);
|
|
137118
|
+
}
|
|
137119
|
+
/** Append/replace index rows in one transaction. */
|
|
137120
|
+
appendMessageIndexRows(rows) {
|
|
137121
|
+
const tx = this.db.transaction((batch) => {
|
|
137122
|
+
for (const r of batch) this.stmts.insertMessageIndexRow.run(r);
|
|
137123
|
+
});
|
|
137124
|
+
tx(rows);
|
|
137125
|
+
}
|
|
137126
|
+
/** Rows for message_index in [fromIndex, toIndex), ordered ascending. */
|
|
137127
|
+
getMessageIndexWindow(conversationId, fromIndex, toIndex) {
|
|
137128
|
+
return this.stmts.getMessageIndexWindow.all(
|
|
137129
|
+
conversationId,
|
|
137130
|
+
fromIndex,
|
|
137131
|
+
toIndex
|
|
137132
|
+
);
|
|
137133
|
+
}
|
|
137134
|
+
getIndexedMessageCount(conversationId) {
|
|
137135
|
+
return this.stmts.getIndexedMessageCount.get(conversationId).cnt;
|
|
137136
|
+
}
|
|
137137
|
+
/**
|
|
137138
|
+
* Conversation id for a JSONL path — the filename stem (matches the pseudo-id
|
|
137139
|
+
* updateFromLine derives and the uuid the detail read path resolves). The
|
|
137140
|
+
* offset index keys on this so the window select and the cursor agree.
|
|
137141
|
+
*/
|
|
137142
|
+
static conversationIdForFile(filePath) {
|
|
137143
|
+
return filePath.split(/[/\\]/).pop()?.replace(/\.jsonl$/, "") ?? filePath;
|
|
137144
|
+
}
|
|
137145
|
+
// The offset index understands only claude-code JSONL: parseJsonlLine is the
|
|
137146
|
+
// claude-code reducer, so a codex file "indexes" as zero messages and the
|
|
137147
|
+
// resulting file_state serves empty windows for a real conversation (the
|
|
137148
|
+
// silent-wrong-data bug hotfixed after 1.28.0). Non-claude providers are
|
|
137149
|
+
// excluded from the index entirely; the scanner serves them (it routes each
|
|
137150
|
+
// provider to its own parser).
|
|
137151
|
+
isIndexableFile(filePath) {
|
|
137152
|
+
const meta3 = this.getMetaById(_ConversationCache.conversationIdForFile(filePath));
|
|
137153
|
+
return (meta3?.provider ?? CLAUDE_CODE_PROVIDER2) === CLAUDE_CODE_PROVIDER2;
|
|
137154
|
+
}
|
|
137155
|
+
/**
|
|
137156
|
+
* Incremental offset-index writer: extend the index for a burst of appended
|
|
137157
|
+
* lines (one watcher read) using their byte spans. Each line is classified
|
|
137158
|
+
* with the scanner's parseJsonlLine (a running per-file reducer state), so the
|
|
137159
|
+
* message ordering can never drift from the scanner's. Message lines get an
|
|
137160
|
+
* index row at the next message_index; non-message lines (summary/sidecar)
|
|
137161
|
+
* get no row but still advance byte_offset. file_state is updated to the end
|
|
137162
|
+
* of the last consumed span.
|
|
137163
|
+
*
|
|
137164
|
+
* Requires an up-to-date `stat` (identity/size/mtime) for the file so the read
|
|
137165
|
+
* path can detect truncation/replacement.
|
|
137166
|
+
*
|
|
137167
|
+
* `readFrom` is the absolute byte offset the watcher read started at, and
|
|
137168
|
+
* `endOffset` is where it ended (readFrom + consumed, i.e. the watcher's new
|
|
137169
|
+
* entry.offset). CONTIGUITY GUARD: the read must begin exactly where the index
|
|
137170
|
+
* left off (`readFrom === existing.byte_offset`, or 0 with no state). If it
|
|
137171
|
+
* doesn't — the watcher attached at EOF after the server was down, or an
|
|
137172
|
+
* append raced an in-flight backfill — extending would assign wrong
|
|
137173
|
+
* message_index values over a hole. In that case this writes nothing and
|
|
137174
|
+
* returns null so the caller drops the index and backfills.
|
|
137175
|
+
*
|
|
137176
|
+
* On success returns the message_index assigned to each input span (null for a
|
|
137177
|
+
* non-message line) so the caller can stamp WS `seq`. Empty array when spans
|
|
137178
|
+
* is empty. `endOffset` is stored verbatim as byte_offset so the watcher's
|
|
137179
|
+
* offset and file_state.byte_offset are the same number by construction.
|
|
137180
|
+
*/
|
|
137181
|
+
extendMessageIndex(filePath, spans, stat10, readFrom, endOffset) {
|
|
137182
|
+
if (!this.isIndexableFile(filePath)) return spans.map(() => null);
|
|
137183
|
+
const existing = this.getFileState(filePath);
|
|
137184
|
+
const expectedStart = existing?.byte_offset ?? 0;
|
|
137185
|
+
if (readFrom !== expectedStart) return null;
|
|
137186
|
+
if (spans.length === 0) return [];
|
|
137187
|
+
const convId = _ConversationCache.conversationIdForFile(filePath);
|
|
137188
|
+
let state = this.indexParseState.get(filePath);
|
|
137189
|
+
if (!state) {
|
|
137190
|
+
state = initialConvState();
|
|
137191
|
+
this.indexParseState.set(filePath, state);
|
|
137192
|
+
}
|
|
137193
|
+
let nextIndex = existing ? existing.last_message_index + 1 : 0;
|
|
137194
|
+
const rows = [];
|
|
137195
|
+
const seqs = [];
|
|
137196
|
+
for (const span of spans) {
|
|
137197
|
+
const msg = parseJsonlLine(span.text, state);
|
|
137198
|
+
if (!msg) {
|
|
137199
|
+
seqs.push(null);
|
|
137200
|
+
continue;
|
|
137201
|
+
}
|
|
137202
|
+
rows.push({
|
|
137203
|
+
conversation_id: convId,
|
|
137204
|
+
message_index: nextIndex,
|
|
137205
|
+
byte_offset: span.byteOffset,
|
|
137206
|
+
byte_length: span.byteLength,
|
|
137207
|
+
uuid: msg.uuid ?? null,
|
|
137208
|
+
role: msg.role ?? null,
|
|
137209
|
+
ts: msg.timestamp ? Date.parse(msg.timestamp) || null : null
|
|
137210
|
+
});
|
|
137211
|
+
seqs.push(nextIndex);
|
|
137212
|
+
nextIndex++;
|
|
137213
|
+
}
|
|
137214
|
+
const tx = this.db.transaction(() => {
|
|
137215
|
+
for (const r of rows) this.stmts.insertMessageIndexRow.run(r);
|
|
137216
|
+
this.stmts.upsertFileState.run({
|
|
137217
|
+
path: filePath,
|
|
137218
|
+
identity: fileIdentity(stat10),
|
|
137219
|
+
size: stat10.size,
|
|
137220
|
+
mtime_ms: Math.round(stat10.mtimeMs),
|
|
137221
|
+
// Store the watcher's end offset verbatim — same number as entry.offset,
|
|
137222
|
+
// so the next read's contiguity check compares like-for-like (never
|
|
137223
|
+
// false-positive on a read that ended in trailing empty lines).
|
|
137224
|
+
byte_offset: endOffset,
|
|
137225
|
+
last_message_index: nextIndex - 1
|
|
137226
|
+
});
|
|
137227
|
+
});
|
|
137228
|
+
tx();
|
|
137229
|
+
return seqs;
|
|
137230
|
+
}
|
|
137231
|
+
clearIndexParseState(filePath) {
|
|
137232
|
+
this.indexParseState.delete(filePath);
|
|
137233
|
+
}
|
|
137234
|
+
/**
|
|
137235
|
+
* On-demand full backfill of the offset index for a file with no/stale
|
|
137236
|
+
* file_state (cold conversation, or after a truncation/replacement). Rebuilds
|
|
137237
|
+
* from byte 0: drops any existing rows, walks the whole file in chunks with a
|
|
137238
|
+
* running parse state, yields to the event loop every ~1000 lines so a large
|
|
137239
|
+
* file never blocks, and writes index rows + file_state.
|
|
137240
|
+
*
|
|
137241
|
+
* Single-flighted per path: concurrent callers await the same walk. The
|
|
137242
|
+
* triggering detail request is served by the scanner fallback while this runs.
|
|
137243
|
+
*/
|
|
137244
|
+
backfillIndex(filePath) {
|
|
137245
|
+
const inFlight = this.backfillInFlight.get(filePath);
|
|
137246
|
+
if (inFlight) return inFlight;
|
|
137247
|
+
const walk = this.runBackfill(filePath).finally(() => {
|
|
137248
|
+
this.backfillInFlight.delete(filePath);
|
|
137249
|
+
});
|
|
137250
|
+
this.backfillInFlight.set(filePath, walk);
|
|
137251
|
+
return walk;
|
|
137252
|
+
}
|
|
137253
|
+
async runBackfill(filePath) {
|
|
137254
|
+
const convId = _ConversationCache.conversationIdForFile(filePath);
|
|
137255
|
+
this.deleteFileIndex(filePath, convId);
|
|
137256
|
+
this.indexParseState.delete(filePath);
|
|
137257
|
+
if (!this.isIndexableFile(filePath)) return;
|
|
137258
|
+
const CHUNK = 256 * 1024;
|
|
137259
|
+
const YIELD_EVERY = 1e3;
|
|
137260
|
+
const state = initialConvState();
|
|
137261
|
+
const fh = await (0, import_promises12.open)(filePath, "r");
|
|
137262
|
+
let fileOffset = 0;
|
|
137263
|
+
let carry = Buffer.alloc(0);
|
|
137264
|
+
let nextIndex = 0;
|
|
137265
|
+
let linesSinceYield = 0;
|
|
137266
|
+
let lastConsumedEnd = 0;
|
|
137267
|
+
let stat10;
|
|
137268
|
+
try {
|
|
137269
|
+
stat10 = await fh.stat();
|
|
137270
|
+
const buf = Buffer.alloc(CHUNK);
|
|
137271
|
+
for (; ; ) {
|
|
137272
|
+
const { bytesRead } = await fh.read(buf, 0, CHUNK, null);
|
|
137273
|
+
if (bytesRead === 0) break;
|
|
137274
|
+
const combined = carry.length > 0 ? Buffer.concat([carry, buf.subarray(0, bytesRead)]) : buf.subarray(0, bytesRead);
|
|
137275
|
+
const { spans, consumed } = splitCompleteLines(combined, fileOffset);
|
|
137276
|
+
const rows = [];
|
|
137277
|
+
for (const span of spans) {
|
|
137278
|
+
const msg = parseJsonlLine(span.text, state);
|
|
137279
|
+
linesSinceYield++;
|
|
137280
|
+
if (msg) {
|
|
137281
|
+
rows.push({
|
|
137282
|
+
conversation_id: convId,
|
|
137283
|
+
message_index: nextIndex,
|
|
137284
|
+
byte_offset: span.byteOffset,
|
|
137285
|
+
byte_length: span.byteLength,
|
|
137286
|
+
uuid: msg.uuid ?? null,
|
|
137287
|
+
role: msg.role ?? null,
|
|
137288
|
+
ts: msg.timestamp ? Date.parse(msg.timestamp) || null : null
|
|
137289
|
+
});
|
|
137290
|
+
nextIndex++;
|
|
137291
|
+
}
|
|
137292
|
+
if (linesSinceYield >= YIELD_EVERY) {
|
|
137293
|
+
linesSinceYield = 0;
|
|
137294
|
+
await (0, import_promises13.setImmediate)();
|
|
137295
|
+
}
|
|
137296
|
+
}
|
|
137297
|
+
if (rows.length > 0) this.appendMessageIndexRows(rows);
|
|
137298
|
+
lastConsumedEnd = fileOffset + consumed;
|
|
137299
|
+
carry = Buffer.from(combined.subarray(consumed));
|
|
137300
|
+
fileOffset += consumed;
|
|
137301
|
+
}
|
|
137302
|
+
} finally {
|
|
137303
|
+
await fh.close();
|
|
137304
|
+
}
|
|
137305
|
+
this.upsertFileState({
|
|
137306
|
+
path: filePath,
|
|
137307
|
+
identity: fileIdentity(stat10),
|
|
137308
|
+
size: stat10.size,
|
|
137309
|
+
mtime_ms: Math.round(stat10.mtimeMs),
|
|
137310
|
+
byte_offset: lastConsumedEnd,
|
|
137311
|
+
last_message_index: nextIndex - 1
|
|
137312
|
+
});
|
|
137313
|
+
this.indexParseState.set(filePath, state);
|
|
137314
|
+
}
|
|
137315
|
+
/**
|
|
137316
|
+
* Windowed detail read straight from the offset index — the hot path.
|
|
137317
|
+
* Returns the parsed messages for message_index in [fromIndex, toIndex) plus
|
|
137318
|
+
* the total indexed count, or null when the index can't serve this file (no
|
|
137319
|
+
* file_state, identity/size mismatch = truncation/replacement, or cold index)
|
|
137320
|
+
* so the caller falls back to the scanner and enqueues a backfill.
|
|
137321
|
+
*
|
|
137322
|
+
* On a match it SQL-selects the window's byte ranges and preads exactly those
|
|
137323
|
+
* ranges from the JSONL (never the whole file), parsing only the sliced lines.
|
|
137324
|
+
* Returns messages in the same ConversationMessage shape parseJsonlLine
|
|
137325
|
+
* produces during a scan, so the payload is identical to the scanner path.
|
|
137326
|
+
*/
|
|
137327
|
+
readMessageWindow(filePath, fromIndex, toIndex) {
|
|
137328
|
+
const fileState = this.getFileState(filePath);
|
|
137329
|
+
if (!fileState) return null;
|
|
137330
|
+
let stat10;
|
|
137331
|
+
try {
|
|
137332
|
+
stat10 = (0, import_fs17.statSync)(filePath);
|
|
137333
|
+
} catch {
|
|
137334
|
+
return null;
|
|
137335
|
+
}
|
|
137336
|
+
if (fileIdentity(stat10) !== fileState.identity || stat10.size !== fileState.byte_offset) {
|
|
137337
|
+
return null;
|
|
137338
|
+
}
|
|
137339
|
+
if (fileState.last_message_index < 0 || !this.isIndexableFile(filePath)) {
|
|
137340
|
+
return null;
|
|
137341
|
+
}
|
|
137342
|
+
const total = fileState.last_message_index + 1;
|
|
137343
|
+
const from = Math.max(0, fromIndex);
|
|
137344
|
+
const to2 = Math.min(toIndex, total);
|
|
137345
|
+
if (to2 <= from) return { messages: [], total, fromIndex: from };
|
|
137346
|
+
const rows = this.getMessageIndexWindow(
|
|
137347
|
+
_ConversationCache.conversationIdForFile(filePath),
|
|
137348
|
+
from,
|
|
137349
|
+
to2
|
|
137350
|
+
);
|
|
137351
|
+
if (rows.length === 0) return { messages: [], total, fromIndex: from };
|
|
137352
|
+
const messages = [];
|
|
137353
|
+
const fd = (0, import_fs17.openSync)(filePath, "r");
|
|
137354
|
+
try {
|
|
137355
|
+
const state = initialConvState();
|
|
137356
|
+
for (const row of rows) {
|
|
137357
|
+
const buf = Buffer.alloc(row.byte_length);
|
|
137358
|
+
(0, import_fs17.readSync)(fd, buf, 0, row.byte_length, row.byte_offset);
|
|
137359
|
+
const msg = parseJsonlLine(buf.toString("utf-8"), state);
|
|
137360
|
+
if (msg) messages.push(msg);
|
|
137361
|
+
}
|
|
137362
|
+
} finally {
|
|
137363
|
+
(0, import_fs17.closeSync)(fd);
|
|
137364
|
+
}
|
|
137365
|
+
return { messages, total, fromIndex: from };
|
|
137366
|
+
}
|
|
137016
137367
|
agentEntrypointsKey() {
|
|
137017
137368
|
return [...this.agentEntrypoints].sort().join(",");
|
|
137018
137369
|
}
|
|
@@ -137736,7 +138087,7 @@ var ConversationsRepository = class {
|
|
|
137736
138087
|
};
|
|
137737
138088
|
|
|
137738
138089
|
// src/db/repositories/projects.repository.ts
|
|
137739
|
-
var
|
|
138090
|
+
var import_crypto6 = require("crypto");
|
|
137740
138091
|
|
|
137741
138092
|
// src/utils/canonicalizeProjectPath.ts
|
|
137742
138093
|
function canonicalizeProjectPath(projectPath) {
|
|
@@ -137829,7 +138180,7 @@ var ProjectsRepository = class {
|
|
|
137829
138180
|
});
|
|
137830
138181
|
return rowToProject(this.getById.get(existing.id));
|
|
137831
138182
|
}
|
|
137832
|
-
const id = (0,
|
|
138183
|
+
const id = (0, import_crypto6.randomUUID)();
|
|
137833
138184
|
this.insert.run({
|
|
137834
138185
|
id,
|
|
137835
138186
|
path: path2,
|
|
@@ -137924,7 +138275,7 @@ var import_path19 = require("path");
|
|
|
137924
138275
|
|
|
137925
138276
|
// src/codex-pty-runner.ts
|
|
137926
138277
|
var import_headless = __toESM(require_xterm_headless(), 1);
|
|
137927
|
-
var
|
|
138278
|
+
var import_crypto7 = require("crypto");
|
|
137928
138279
|
var import_fs21 = require("fs");
|
|
137929
138280
|
var import_path17 = require("path");
|
|
137930
138281
|
init_logger();
|
|
@@ -138179,7 +138530,7 @@ var CodexPtyRunner = class {
|
|
|
138179
138530
|
// ManagedSession handle only.
|
|
138180
138531
|
async startFresh(options) {
|
|
138181
138532
|
const nodePty = await loadPty();
|
|
138182
|
-
const sessionId = (0,
|
|
138533
|
+
const sessionId = (0, import_crypto7.randomUUID)();
|
|
138183
138534
|
const projectName = options.projectName ?? (0, import_path17.basename)(options.projectPath);
|
|
138184
138535
|
const args = ["--cd", options.projectPath, "--no-alt-screen"];
|
|
138185
138536
|
if (options.systemPrompt) {
|
|
@@ -138508,7 +138859,7 @@ function stripAnsi(str) {
|
|
|
138508
138859
|
|
|
138509
138860
|
// src/pty-manager.ts
|
|
138510
138861
|
var import_headless2 = __toESM(require_xterm_headless(), 1);
|
|
138511
|
-
var
|
|
138862
|
+
var import_crypto8 = require("crypto");
|
|
138512
138863
|
var import_fs22 = require("fs");
|
|
138513
138864
|
var import_path18 = require("path");
|
|
138514
138865
|
init_logger();
|
|
@@ -138912,7 +139263,7 @@ var PTYManager = class {
|
|
|
138912
139263
|
// onReady fires once Claude reaches its first prompt (waiting_input).
|
|
138913
139264
|
async startFresh(options) {
|
|
138914
139265
|
const nodePty = await loadPty2();
|
|
138915
|
-
const sessionId = (0,
|
|
139266
|
+
const sessionId = (0, import_crypto8.randomUUID)();
|
|
138916
139267
|
const projectName = options.projectName ?? (0, import_path18.basename)(options.projectPath);
|
|
138917
139268
|
const args = [
|
|
138918
139269
|
"--permission-mode",
|
|
@@ -139496,7 +139847,7 @@ var LiveSessionManager = class {
|
|
|
139496
139847
|
init_logger();
|
|
139497
139848
|
|
|
139498
139849
|
// src/pair-store.ts
|
|
139499
|
-
var
|
|
139850
|
+
var import_crypto9 = require("crypto");
|
|
139500
139851
|
var DEFAULT_TTL_SECONDS = 180;
|
|
139501
139852
|
var SWEEP_INTERVAL_MS = 6e4;
|
|
139502
139853
|
var PairTokenStore = class {
|
|
@@ -139511,7 +139862,7 @@ var PairTokenStore = class {
|
|
|
139511
139862
|
}
|
|
139512
139863
|
}
|
|
139513
139864
|
mint() {
|
|
139514
|
-
const token = `pt_${(0,
|
|
139865
|
+
const token = `pt_${(0, import_crypto9.randomBytes)(16).toString("hex")}`;
|
|
139515
139866
|
const expiresAt = Date.now() + this.ttlMs;
|
|
139516
139867
|
this.current = { token, expiresAt, used: false };
|
|
139517
139868
|
return {
|
|
@@ -139716,11 +140067,11 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
139716
140067
|
// node_modules/chokidar/index.js
|
|
139717
140068
|
var import_node_events2 = require("events");
|
|
139718
140069
|
var import_node_fs7 = require("fs");
|
|
139719
|
-
var
|
|
140070
|
+
var import_promises16 = require("fs/promises");
|
|
139720
140071
|
var sp2 = __toESM(require("path"), 1);
|
|
139721
140072
|
|
|
139722
140073
|
// node_modules/readdirp/index.js
|
|
139723
|
-
var
|
|
140074
|
+
var import_promises14 = require("fs/promises");
|
|
139724
140075
|
var import_node_path9 = require("path");
|
|
139725
140076
|
var import_node_stream2 = require("stream");
|
|
139726
140077
|
var EntryTypes2 = {
|
|
@@ -139801,7 +140152,7 @@ var ReaddirpStream2 = class extends import_node_stream2.Readable {
|
|
|
139801
140152
|
const { root, type } = opts;
|
|
139802
140153
|
this._fileFilter = normalizeFilter2(opts.fileFilter);
|
|
139803
140154
|
this._directoryFilter = normalizeFilter2(opts.directoryFilter);
|
|
139804
|
-
const statMethod = opts.lstat ?
|
|
140155
|
+
const statMethod = opts.lstat ? import_promises14.lstat : import_promises14.stat;
|
|
139805
140156
|
if (wantBigintFsStats2) {
|
|
139806
140157
|
this._stat = (path2) => statMethod(path2, { bigint: true });
|
|
139807
140158
|
} else {
|
|
@@ -139872,7 +140223,7 @@ var ReaddirpStream2 = class extends import_node_stream2.Readable {
|
|
|
139872
140223
|
async _exploreDir(path2, depth) {
|
|
139873
140224
|
let files;
|
|
139874
140225
|
try {
|
|
139875
|
-
files = await (0,
|
|
140226
|
+
files = await (0, import_promises14.readdir)(path2, this._rdOptions);
|
|
139876
140227
|
} catch (error51) {
|
|
139877
140228
|
this._onError(error51);
|
|
139878
140229
|
}
|
|
@@ -139910,8 +140261,8 @@ var ReaddirpStream2 = class extends import_node_stream2.Readable {
|
|
|
139910
140261
|
if (stats && stats.isSymbolicLink()) {
|
|
139911
140262
|
const full = entry.fullPath;
|
|
139912
140263
|
try {
|
|
139913
|
-
const entryRealPath = await (0,
|
|
139914
|
-
const entryRealPathStats = await (0,
|
|
140264
|
+
const entryRealPath = await (0, import_promises14.realpath)(full);
|
|
140265
|
+
const entryRealPathStats = await (0, import_promises14.lstat)(entryRealPath);
|
|
139915
140266
|
if (entryRealPathStats.isFile()) {
|
|
139916
140267
|
return "file";
|
|
139917
140268
|
}
|
|
@@ -139954,7 +140305,7 @@ function readdirp2(root, options = {}) {
|
|
|
139954
140305
|
|
|
139955
140306
|
// node_modules/chokidar/handler.js
|
|
139956
140307
|
var import_node_fs6 = require("fs");
|
|
139957
|
-
var
|
|
140308
|
+
var import_promises15 = require("fs/promises");
|
|
139958
140309
|
var import_node_os3 = require("os");
|
|
139959
140310
|
var sp = __toESM(require("path"), 1);
|
|
139960
140311
|
var STR_DATA2 = "data";
|
|
@@ -139981,7 +140332,7 @@ var EVENTS2 = {
|
|
|
139981
140332
|
};
|
|
139982
140333
|
var EV2 = EVENTS2;
|
|
139983
140334
|
var THROTTLE_MODE_WATCH2 = "watch";
|
|
139984
|
-
var statMethods2 = { lstat:
|
|
140335
|
+
var statMethods2 = { lstat: import_promises15.lstat, stat: import_promises15.stat };
|
|
139985
140336
|
var KEY_LISTENERS2 = "listeners";
|
|
139986
140337
|
var KEY_ERR2 = "errHandlers";
|
|
139987
140338
|
var KEY_RAW2 = "rawEmitters";
|
|
@@ -140338,7 +140689,7 @@ var setFsWatchListener2 = (path2, fullPath, options, handlers) => {
|
|
|
140338
140689
|
cont.watcherUnusable = true;
|
|
140339
140690
|
if (isWindows3 && error51.code === "EPERM") {
|
|
140340
140691
|
try {
|
|
140341
|
-
const fd = await (0,
|
|
140692
|
+
const fd = await (0, import_promises15.open)(path2, "r");
|
|
140342
140693
|
await fd.close();
|
|
140343
140694
|
broadcastErr(error51);
|
|
140344
140695
|
} catch (err) {
|
|
@@ -140469,7 +140820,7 @@ var NodeFsHandler2 = class {
|
|
|
140469
140820
|
return;
|
|
140470
140821
|
if (!newStats || newStats.mtimeMs === 0) {
|
|
140471
140822
|
try {
|
|
140472
|
-
const newStats2 = await (0,
|
|
140823
|
+
const newStats2 = await (0, import_promises15.stat)(file2);
|
|
140473
140824
|
if (this.fsw.closed)
|
|
140474
140825
|
return;
|
|
140475
140826
|
const at2 = newStats2.atimeMs;
|
|
@@ -140524,7 +140875,7 @@ var NodeFsHandler2 = class {
|
|
|
140524
140875
|
this.fsw._incrReadyCount();
|
|
140525
140876
|
let linkPath;
|
|
140526
140877
|
try {
|
|
140527
|
-
linkPath = await (0,
|
|
140878
|
+
linkPath = await (0, import_promises15.realpath)(path2);
|
|
140528
140879
|
} catch (e) {
|
|
140529
140880
|
this.fsw._emitReady();
|
|
140530
140881
|
return true;
|
|
@@ -140673,7 +141024,7 @@ var NodeFsHandler2 = class {
|
|
|
140673
141024
|
let closer;
|
|
140674
141025
|
if (stats.isDirectory()) {
|
|
140675
141026
|
const absPath = sp.resolve(path2);
|
|
140676
|
-
const targetPath = follow ? await (0,
|
|
141027
|
+
const targetPath = follow ? await (0, import_promises15.realpath)(path2) : path2;
|
|
140677
141028
|
if (this.fsw.closed)
|
|
140678
141029
|
return;
|
|
140679
141030
|
closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
|
|
@@ -140683,7 +141034,7 @@ var NodeFsHandler2 = class {
|
|
|
140683
141034
|
this.fsw._symlinkPaths.set(absPath, targetPath);
|
|
140684
141035
|
}
|
|
140685
141036
|
} else if (stats.isSymbolicLink()) {
|
|
140686
|
-
const targetPath = follow ? await (0,
|
|
141037
|
+
const targetPath = follow ? await (0, import_promises15.realpath)(path2) : path2;
|
|
140687
141038
|
if (this.fsw.closed)
|
|
140688
141039
|
return;
|
|
140689
141040
|
const parent = sp.dirname(wh.watchPath);
|
|
@@ -140843,7 +141194,7 @@ var DirEntry2 = class {
|
|
|
140843
141194
|
return;
|
|
140844
141195
|
const dir = this.path;
|
|
140845
141196
|
try {
|
|
140846
|
-
await (0,
|
|
141197
|
+
await (0, import_promises16.readdir)(dir);
|
|
140847
141198
|
} catch (err) {
|
|
140848
141199
|
if (this._removeWatcher) {
|
|
140849
141200
|
this._removeWatcher(sp2.dirname(dir), sp2.basename(dir));
|
|
@@ -141193,7 +141544,7 @@ var FSWatcher2 = class extends import_node_events2.EventEmitter {
|
|
|
141193
141544
|
const fullPath = opts.cwd ? sp2.join(opts.cwd, path2) : path2;
|
|
141194
141545
|
let stats2;
|
|
141195
141546
|
try {
|
|
141196
|
-
stats2 = await (0,
|
|
141547
|
+
stats2 = await (0, import_promises16.stat)(fullPath);
|
|
141197
141548
|
} catch (err) {
|
|
141198
141549
|
}
|
|
141199
141550
|
if (!stats2 || this.closed)
|
|
@@ -141447,18 +141798,20 @@ var chokidar_default = { watch: watch2, FSWatcher: FSWatcher2 };
|
|
|
141447
141798
|
|
|
141448
141799
|
// src/services/conversations/conversationWatcher.ts
|
|
141449
141800
|
var import_fs23 = require("fs");
|
|
141450
|
-
var
|
|
141801
|
+
var import_promises17 = require("fs/promises");
|
|
141451
141802
|
var ConversationWatcher = class {
|
|
141452
141803
|
files = /* @__PURE__ */ new Map();
|
|
141453
141804
|
directories = /* @__PURE__ */ new Map();
|
|
141454
141805
|
onNewLine;
|
|
141455
141806
|
onNewLines;
|
|
141807
|
+
onNewLineSpans;
|
|
141456
141808
|
onConversationChanged;
|
|
141457
141809
|
onFileDeleted;
|
|
141458
141810
|
onError;
|
|
141459
141811
|
constructor(events = {}) {
|
|
141460
141812
|
this.onNewLine = events.onNewLine;
|
|
141461
141813
|
this.onNewLines = events.onNewLines;
|
|
141814
|
+
this.onNewLineSpans = events.onNewLineSpans;
|
|
141462
141815
|
this.onConversationChanged = events.onConversationChanged;
|
|
141463
141816
|
this.onFileDeleted = events.onFileDeleted;
|
|
141464
141817
|
this.onError = events.onError;
|
|
@@ -141551,20 +141904,24 @@ var ConversationWatcher = class {
|
|
|
141551
141904
|
entry.reading = true;
|
|
141552
141905
|
try {
|
|
141553
141906
|
for (; ; ) {
|
|
141554
|
-
const st2 = await (0,
|
|
141907
|
+
const st2 = await (0, import_promises17.stat)(filePath);
|
|
141555
141908
|
if (st2.size <= entry.offset) break;
|
|
141556
141909
|
const readFrom = entry.offset;
|
|
141557
141910
|
const bytesToRead = st2.size - readFrom;
|
|
141558
141911
|
const buf = Buffer.alloc(bytesToRead);
|
|
141559
|
-
const fh = await (0,
|
|
141912
|
+
const fh = await (0, import_promises17.open)(filePath, "r");
|
|
141560
141913
|
try {
|
|
141561
141914
|
await fh.read(buf, 0, bytesToRead, readFrom);
|
|
141562
141915
|
} finally {
|
|
141563
141916
|
await fh.close();
|
|
141564
141917
|
}
|
|
141565
|
-
|
|
141918
|
+
const { spans, consumed } = splitCompleteLines(buf, readFrom);
|
|
141919
|
+
entry.offset = readFrom + consumed;
|
|
141566
141920
|
if (!this.files.has(filePath)) return;
|
|
141567
|
-
const lines =
|
|
141921
|
+
const lines = spans.map((s3) => s3.text);
|
|
141922
|
+
if (spans.length > 0) {
|
|
141923
|
+
this.onNewLineSpans?.(filePath, spans, readFrom, entry.offset);
|
|
141924
|
+
}
|
|
141568
141925
|
if (this.onNewLines) {
|
|
141569
141926
|
this.onNewLines(filePath, lines);
|
|
141570
141927
|
} else {
|
|
@@ -142004,8 +142361,8 @@ function discoveredToResponse(d, conversationId) {
|
|
|
142004
142361
|
}
|
|
142005
142362
|
|
|
142006
142363
|
// src/uploads.ts
|
|
142007
|
-
var
|
|
142008
|
-
var
|
|
142364
|
+
var import_crypto10 = require("crypto");
|
|
142365
|
+
var import_promises18 = require("fs/promises");
|
|
142009
142366
|
var import_heic_convert = __toESM(require_heic_convert(), 1);
|
|
142010
142367
|
var import_path21 = require("path");
|
|
142011
142368
|
var UPLOAD_DIR_NAME = ".threadbase-uploads";
|
|
@@ -142038,12 +142395,12 @@ async function saveUploadFile(input) {
|
|
|
142038
142395
|
mimeType = "image/jpeg";
|
|
142039
142396
|
originalName = originalName.replace(/\.(heic|heif)$/i, ".jpg");
|
|
142040
142397
|
}
|
|
142041
|
-
const id = `up_${(0,
|
|
142398
|
+
const id = `up_${(0, import_crypto10.randomBytes)(8).toString("hex")}`;
|
|
142042
142399
|
const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
|
|
142043
142400
|
const dir = (0, import_path21.join)(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
|
|
142044
|
-
await (0,
|
|
142401
|
+
await (0, import_promises18.mkdir)(dir, { recursive: true });
|
|
142045
142402
|
const filePath = (0, import_path21.join)(dir, `${Date.now()}-${id}-${safeName}`);
|
|
142046
|
-
await (0,
|
|
142403
|
+
await (0, import_promises18.writeFile)(filePath, buffer);
|
|
142047
142404
|
return {
|
|
142048
142405
|
id,
|
|
142049
142406
|
filePath,
|
|
@@ -142442,6 +142799,10 @@ var StreamerServer = class {
|
|
|
142442
142799
|
fileWatcher;
|
|
142443
142800
|
sessionFileMap = /* @__PURE__ */ new Map();
|
|
142444
142801
|
// sessionId → JSONL filePath
|
|
142802
|
+
// Per-file seq assignments from the most recent onNewLineSpans (offset index),
|
|
142803
|
+
// handed to the immediately-following onNewLines so it can stamp WS `seq` on
|
|
142804
|
+
// the matching conversation_events entries. Same read → same lines order.
|
|
142805
|
+
pendingLineSeqs = /* @__PURE__ */ new Map();
|
|
142445
142806
|
pendingQuestions = /* @__PURE__ */ new Map();
|
|
142446
142807
|
// Content key of the AskUserQuestion currently broadcast for a session (from
|
|
142447
142808
|
// either the rendered screen or JSONL), used to de-dupe the two paths: when
|
|
@@ -142565,7 +142926,7 @@ var StreamerServer = class {
|
|
|
142565
142926
|
this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);
|
|
142566
142927
|
const rawRoot = process.env.THREADBASE_BROWSE_ROOT ?? loadBrowseRoot() ?? config2.browseRoot;
|
|
142567
142928
|
if (rawRoot) {
|
|
142568
|
-
(0,
|
|
142929
|
+
(0, import_promises19.realpath)(rawRoot).then((resolved) => {
|
|
142569
142930
|
this.browseRoot = resolved;
|
|
142570
142931
|
if (this.verbose) this.log.info(`Browse root: ${resolved}`, { browseRoot: resolved });
|
|
142571
142932
|
}).catch(() => {
|
|
@@ -142587,6 +142948,42 @@ var StreamerServer = class {
|
|
|
142587
142948
|
this.sessionStore = new SessionStore();
|
|
142588
142949
|
this.wsHub = new WSHub();
|
|
142589
142950
|
this.fileWatcher = new ConversationWatcher({
|
|
142951
|
+
onNewLineSpans: (filePath, spans, readFrom, endOffset) => {
|
|
142952
|
+
if (!this.cache) return;
|
|
142953
|
+
const cache = this.cache;
|
|
142954
|
+
this.pendingLineSeqs.delete(filePath);
|
|
142955
|
+
try {
|
|
142956
|
+
const seqs = cache.extendMessageIndex(
|
|
142957
|
+
filePath,
|
|
142958
|
+
spans,
|
|
142959
|
+
(0, import_fs25.statSync)(filePath),
|
|
142960
|
+
readFrom,
|
|
142961
|
+
endOffset
|
|
142962
|
+
);
|
|
142963
|
+
if (seqs === null) {
|
|
142964
|
+
cache.deleteFileIndex(filePath, ConversationCache.conversationIdForFile(filePath));
|
|
142965
|
+
cache.clearIndexParseState(filePath);
|
|
142966
|
+
this.trackCacheWrite(
|
|
142967
|
+
cache.backfillIndex(filePath).catch((err) => {
|
|
142968
|
+
this.log.warn("offset-index.backfill_failed", {
|
|
142969
|
+
event: "offset_index.backfill_failed",
|
|
142970
|
+
filePath,
|
|
142971
|
+
trigger: "noncontiguous-append",
|
|
142972
|
+
err
|
|
142973
|
+
});
|
|
142974
|
+
})
|
|
142975
|
+
);
|
|
142976
|
+
return;
|
|
142977
|
+
}
|
|
142978
|
+
this.pendingLineSeqs.set(filePath, seqs);
|
|
142979
|
+
} catch (err) {
|
|
142980
|
+
this.log.warn("offset-index.extend_failed", {
|
|
142981
|
+
event: "offset_index.extend_failed",
|
|
142982
|
+
filePath,
|
|
142983
|
+
err
|
|
142984
|
+
});
|
|
142985
|
+
}
|
|
142986
|
+
},
|
|
142590
142987
|
onNewLines: (filePath, lines) => {
|
|
142591
142988
|
this.cache?.updateFromLines(filePath, lines);
|
|
142592
142989
|
for (const [sessionId, watchedPath] of this.sessionFileMap) {
|
|
@@ -142613,13 +143010,20 @@ var StreamerServer = class {
|
|
|
142613
143010
|
this.pendingQuestionKey.set(sessionId, key);
|
|
142614
143011
|
if (broadcast) this.wsHub.broadcast(m2);
|
|
142615
143012
|
}
|
|
142616
|
-
this.
|
|
143013
|
+
const seqs = this.pendingLineSeqs.get(filePath);
|
|
143014
|
+
this.wsHub.broadcast({
|
|
143015
|
+
type: "conversation_events",
|
|
143016
|
+
sessionId,
|
|
143017
|
+
lines,
|
|
143018
|
+
...seqs && seqs.length === lines.length ? { seqs } : {}
|
|
143019
|
+
});
|
|
142617
143020
|
for (const line of lines) {
|
|
142618
143021
|
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
142619
143022
|
}
|
|
142620
143023
|
break;
|
|
142621
143024
|
}
|
|
142622
143025
|
}
|
|
143026
|
+
this.pendingLineSeqs.delete(filePath);
|
|
142623
143027
|
},
|
|
142624
143028
|
onConversationChanged: (filePath) => {
|
|
142625
143029
|
this.fileWatcher.poke(filePath);
|
|
@@ -143703,9 +144107,13 @@ var StreamerServer = class {
|
|
|
143703
144107
|
return;
|
|
143704
144108
|
}
|
|
143705
144109
|
const etagSource = conversation;
|
|
144110
|
+
const indexedCount = etagSource.filePath && this.cache ? this.cache.getIndexedMessageCount(
|
|
144111
|
+
ConversationCache.conversationIdForFile(etagSource.filePath)
|
|
144112
|
+
) : 0;
|
|
144113
|
+
const etagMessageCount = Math.max(etagSource.messageCount, indexedCount);
|
|
143706
144114
|
const etag = computeConversationEtag({
|
|
143707
144115
|
filePath: etagSource.filePath,
|
|
143708
|
-
messageCount:
|
|
144116
|
+
messageCount: etagMessageCount,
|
|
143709
144117
|
timestamp: etagSource.timestamp
|
|
143710
144118
|
});
|
|
143711
144119
|
const isFirstPage = !url2.searchParams.has("before_index") && !url2.searchParams.has("anchor_index") && !url2.searchParams.has("after_index");
|
|
@@ -143722,12 +144130,14 @@ var StreamerServer = class {
|
|
|
143722
144130
|
let slice = filtered;
|
|
143723
144131
|
let fromIdx = 0;
|
|
143724
144132
|
let messagePagination;
|
|
144133
|
+
let indexTotal = null;
|
|
143725
144134
|
if (usePaging) {
|
|
143726
144135
|
const limit = Math.min(Math.max(intParam(url2, "msg_limit", 80), 1), 500);
|
|
143727
144136
|
let beforeIndex = total;
|
|
143728
144137
|
let scanLimit = limit;
|
|
143729
144138
|
let anchorIndex = null;
|
|
143730
144139
|
let newerPaging = false;
|
|
144140
|
+
let usedAfterIndex = false;
|
|
143731
144141
|
if (url2.searchParams.has("before_index")) {
|
|
143732
144142
|
beforeIndex = intParam(url2, "before_index", total);
|
|
143733
144143
|
beforeIndex = Math.min(Math.max(beforeIndex, 0), total);
|
|
@@ -143736,6 +144146,7 @@ var StreamerServer = class {
|
|
|
143736
144146
|
beforeIndex = Math.min(total, from + limit);
|
|
143737
144147
|
scanLimit = beforeIndex - from;
|
|
143738
144148
|
newerPaging = true;
|
|
144149
|
+
usedAfterIndex = true;
|
|
143739
144150
|
} else if (hasAnchor) {
|
|
143740
144151
|
anchorIndex = Math.min(
|
|
143741
144152
|
Math.max(intParam(url2, "anchor_index", 0), 0),
|
|
@@ -143745,9 +144156,34 @@ var StreamerServer = class {
|
|
|
143745
144156
|
beforeIndex = Math.min(total, from + limit);
|
|
143746
144157
|
newerPaging = true;
|
|
143747
144158
|
}
|
|
143748
|
-
const
|
|
143749
|
-
const
|
|
143750
|
-
|
|
144159
|
+
const isTailRequest = !url2.searchParams.has("before_index") && !hasAfter && !hasAnchor;
|
|
144160
|
+
const indexFilePath = conversation.filePath;
|
|
144161
|
+
if (isTailRequest && indexFilePath && this.cache) {
|
|
144162
|
+
const indexed = this.cache.getIndexedMessageCount(
|
|
144163
|
+
ConversationCache.conversationIdForFile(indexFilePath)
|
|
144164
|
+
);
|
|
144165
|
+
if (indexed > beforeIndex) {
|
|
144166
|
+
beforeIndex = indexed;
|
|
144167
|
+
}
|
|
144168
|
+
}
|
|
144169
|
+
const windowStart = Math.max(0, beforeIndex - scanLimit);
|
|
144170
|
+
const indexWindow = scanLimit > 0 && !hasAnchor && indexFilePath && this.cache ? this.cache.readMessageWindow(indexFilePath, windowStart, beforeIndex) : null;
|
|
144171
|
+
if (!indexWindow && indexFilePath && this.cache && !hasAnchor) {
|
|
144172
|
+
this.trackCacheWrite(
|
|
144173
|
+
this.cache.backfillIndex(indexFilePath).catch((err) => {
|
|
144174
|
+
this.log.warn("offset-index.backfill_failed", {
|
|
144175
|
+
event: "offset_index.backfill_failed",
|
|
144176
|
+
conversationId: id,
|
|
144177
|
+
filePath: indexFilePath,
|
|
144178
|
+
err
|
|
144179
|
+
});
|
|
144180
|
+
})
|
|
144181
|
+
);
|
|
144182
|
+
}
|
|
144183
|
+
const pagedScanner = !indexWindow && this.scannerReady ? await this.getScanner(true) : null;
|
|
144184
|
+
const page = indexWindow ?? (scanLimit > 0 && pagedScanner && typeof pagedScanner.getConversationPage === "function" ? await pagedScanner.getConversationPage(id, { beforeIndex, limit: scanLimit }) : null);
|
|
144185
|
+
if (indexWindow) indexTotal = indexWindow.total;
|
|
144186
|
+
const start = page?.fromIndex ?? windowStart;
|
|
143751
144187
|
slice = page?.messages ?? filtered.slice(start, beforeIndex);
|
|
143752
144188
|
fromIdx = start;
|
|
143753
144189
|
const effectiveTotal = page?.total ?? total;
|
|
@@ -143763,6 +144199,9 @@ var StreamerServer = class {
|
|
|
143763
144199
|
messagePagination.has_more_newer = beforeIndex < effectiveTotal;
|
|
143764
144200
|
messagePagination.next_after_index = beforeIndex < effectiveTotal ? beforeIndex : null;
|
|
143765
144201
|
}
|
|
144202
|
+
if (usedAfterIndex) {
|
|
144203
|
+
messagePagination.etag = etag;
|
|
144204
|
+
}
|
|
143766
144205
|
}
|
|
143767
144206
|
const messagesPayload = slice.map((m2, localIdx) => {
|
|
143768
144207
|
const content = [];
|
|
@@ -143804,6 +144243,8 @@ var StreamerServer = class {
|
|
|
143804
144243
|
const cachedConvMeta = this.cache?.getMetaById(id);
|
|
143805
144244
|
const convProvider = coerceProviderForRunner(conv.provider ?? cachedConvMeta?.provider);
|
|
143806
144245
|
const availability = classifyResumability(conv.projectPath);
|
|
144246
|
+
const metaMessageCount = indexTotal != null && indexTotal > conv.messageCount ? indexTotal : conv.messageCount;
|
|
144247
|
+
const metaLastUpdatedAt = indexTotal != null && indexTotal > conv.messageCount ? slice.at(-1)?.timestamp ?? conv.timestamp : conv.timestamp;
|
|
143807
144248
|
const body = {
|
|
143808
144249
|
meta: {
|
|
143809
144250
|
id,
|
|
@@ -143811,8 +144252,8 @@ var StreamerServer = class {
|
|
|
143811
144252
|
project_name: conv.projectName,
|
|
143812
144253
|
project_path: conv.projectPath,
|
|
143813
144254
|
file_path: conv.filePath,
|
|
143814
|
-
last_updated_at:
|
|
143815
|
-
message_count:
|
|
144255
|
+
last_updated_at: metaLastUpdatedAt,
|
|
144256
|
+
message_count: metaMessageCount,
|
|
143816
144257
|
last_prompt: conv.lastPrompt ?? void 0,
|
|
143817
144258
|
provider: convProvider,
|
|
143818
144259
|
resumable: isProviderResumable(convProvider, availability.resumable),
|
|
@@ -145094,7 +145535,7 @@ function isBrewInstall(scriptPath = process.argv[1] ?? "") {
|
|
|
145094
145535
|
var import_node_crypto4 = require("crypto");
|
|
145095
145536
|
var import_node_fs8 = require("fs");
|
|
145096
145537
|
var import_node_path10 = require("path");
|
|
145097
|
-
var
|
|
145538
|
+
var import_promises20 = require("stream/promises");
|
|
145098
145539
|
|
|
145099
145540
|
// src/updater/manifest.ts
|
|
145100
145541
|
init_zod();
|
|
@@ -145169,7 +145610,7 @@ async function downloadAndVerify(opts) {
|
|
|
145169
145610
|
}
|
|
145170
145611
|
});
|
|
145171
145612
|
try {
|
|
145172
|
-
await (0,
|
|
145613
|
+
await (0, import_promises20.pipeline)(res.body.pipeThrough(measured), out);
|
|
145173
145614
|
} catch (err) {
|
|
145174
145615
|
(0, import_node_fs8.rmSync)(targetPath, { force: true });
|
|
145175
145616
|
throw err;
|
|
@@ -145383,7 +145824,7 @@ var import_fs29 = __toESM(require("fs"), 1);
|
|
|
145383
145824
|
var import_node_fs14 = __toESM(require("fs"), 1);
|
|
145384
145825
|
var import_node_path19 = __toESM(require("path"), 1);
|
|
145385
145826
|
var import_node_fs15 = __toESM(require("fs"), 1);
|
|
145386
|
-
var
|
|
145827
|
+
var import_promises21 = __toESM(require("fs/promises"), 1);
|
|
145387
145828
|
var import_node_path20 = __toESM(require("path"), 1);
|
|
145388
145829
|
var import_node_path21 = require("path");
|
|
145389
145830
|
var import_node_fs16 = __toESM(require("fs"), 1);
|
|
@@ -147615,7 +148056,7 @@ var gr = (s3, t2, e) => {
|
|
|
147615
148056
|
E2 ? e(E2) : x2 && a ? Es(x2, o, h, (Le) => S2(Le)) : n ? import_node_fs15.default.chmod(s3, r, e) : e();
|
|
147616
148057
|
};
|
|
147617
148058
|
if (s3 === d) return no(s3, S2);
|
|
147618
|
-
if (l) return
|
|
148059
|
+
if (l) return import_promises21.default.mkdir(s3, { mode: r, recursive: true }).then((E2) => S2(null, E2 ?? void 0), S2);
|
|
147619
148060
|
let D2 = f(import_node_path20.default.relative(d, s3)).split("/");
|
|
147620
148061
|
Ss(d, D2, r, c, d, void 0, S2);
|
|
147621
148062
|
};
|