@threadbase-sh/streamer 1.27.3 → 1.28.0
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 +480 -54
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +449 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +116 -1
- package/dist/index.d.ts +116 -1
- package/dist/index.js +430 -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,258 @@ 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
|
+
/**
|
|
137146
|
+
* Incremental offset-index writer: extend the index for a burst of appended
|
|
137147
|
+
* lines (one watcher read) using their byte spans. Each line is classified
|
|
137148
|
+
* with the scanner's parseJsonlLine (a running per-file reducer state), so the
|
|
137149
|
+
* message ordering can never drift from the scanner's. Message lines get an
|
|
137150
|
+
* index row at the next message_index; non-message lines (summary/sidecar)
|
|
137151
|
+
* get no row but still advance byte_offset. file_state is updated to the end
|
|
137152
|
+
* of the last consumed span.
|
|
137153
|
+
*
|
|
137154
|
+
* Requires an up-to-date `stat` (identity/size/mtime) for the file so the read
|
|
137155
|
+
* path can detect truncation/replacement.
|
|
137156
|
+
*
|
|
137157
|
+
* `readFrom` is the absolute byte offset the watcher read started at, and
|
|
137158
|
+
* `endOffset` is where it ended (readFrom + consumed, i.e. the watcher's new
|
|
137159
|
+
* entry.offset). CONTIGUITY GUARD: the read must begin exactly where the index
|
|
137160
|
+
* left off (`readFrom === existing.byte_offset`, or 0 with no state). If it
|
|
137161
|
+
* doesn't — the watcher attached at EOF after the server was down, or an
|
|
137162
|
+
* append raced an in-flight backfill — extending would assign wrong
|
|
137163
|
+
* message_index values over a hole. In that case this writes nothing and
|
|
137164
|
+
* returns null so the caller drops the index and backfills.
|
|
137165
|
+
*
|
|
137166
|
+
* On success returns the message_index assigned to each input span (null for a
|
|
137167
|
+
* non-message line) so the caller can stamp WS `seq`. Empty array when spans
|
|
137168
|
+
* is empty. `endOffset` is stored verbatim as byte_offset so the watcher's
|
|
137169
|
+
* offset and file_state.byte_offset are the same number by construction.
|
|
137170
|
+
*/
|
|
137171
|
+
extendMessageIndex(filePath, spans, stat10, readFrom, endOffset) {
|
|
137172
|
+
const existing = this.getFileState(filePath);
|
|
137173
|
+
const expectedStart = existing?.byte_offset ?? 0;
|
|
137174
|
+
if (readFrom !== expectedStart) return null;
|
|
137175
|
+
if (spans.length === 0) return [];
|
|
137176
|
+
const convId = _ConversationCache.conversationIdForFile(filePath);
|
|
137177
|
+
let state = this.indexParseState.get(filePath);
|
|
137178
|
+
if (!state) {
|
|
137179
|
+
state = initialConvState();
|
|
137180
|
+
this.indexParseState.set(filePath, state);
|
|
137181
|
+
}
|
|
137182
|
+
let nextIndex = existing ? existing.last_message_index + 1 : 0;
|
|
137183
|
+
const rows = [];
|
|
137184
|
+
const seqs = [];
|
|
137185
|
+
for (const span of spans) {
|
|
137186
|
+
const msg = parseJsonlLine(span.text, state);
|
|
137187
|
+
if (!msg) {
|
|
137188
|
+
seqs.push(null);
|
|
137189
|
+
continue;
|
|
137190
|
+
}
|
|
137191
|
+
rows.push({
|
|
137192
|
+
conversation_id: convId,
|
|
137193
|
+
message_index: nextIndex,
|
|
137194
|
+
byte_offset: span.byteOffset,
|
|
137195
|
+
byte_length: span.byteLength,
|
|
137196
|
+
uuid: msg.uuid ?? null,
|
|
137197
|
+
role: msg.role ?? null,
|
|
137198
|
+
ts: msg.timestamp ? Date.parse(msg.timestamp) || null : null
|
|
137199
|
+
});
|
|
137200
|
+
seqs.push(nextIndex);
|
|
137201
|
+
nextIndex++;
|
|
137202
|
+
}
|
|
137203
|
+
const tx = this.db.transaction(() => {
|
|
137204
|
+
for (const r of rows) this.stmts.insertMessageIndexRow.run(r);
|
|
137205
|
+
this.stmts.upsertFileState.run({
|
|
137206
|
+
path: filePath,
|
|
137207
|
+
identity: fileIdentity(stat10),
|
|
137208
|
+
size: stat10.size,
|
|
137209
|
+
mtime_ms: Math.round(stat10.mtimeMs),
|
|
137210
|
+
// Store the watcher's end offset verbatim — same number as entry.offset,
|
|
137211
|
+
// so the next read's contiguity check compares like-for-like (never
|
|
137212
|
+
// false-positive on a read that ended in trailing empty lines).
|
|
137213
|
+
byte_offset: endOffset,
|
|
137214
|
+
last_message_index: nextIndex - 1
|
|
137215
|
+
});
|
|
137216
|
+
});
|
|
137217
|
+
tx();
|
|
137218
|
+
return seqs;
|
|
137219
|
+
}
|
|
137220
|
+
clearIndexParseState(filePath) {
|
|
137221
|
+
this.indexParseState.delete(filePath);
|
|
137222
|
+
}
|
|
137223
|
+
/**
|
|
137224
|
+
* On-demand full backfill of the offset index for a file with no/stale
|
|
137225
|
+
* file_state (cold conversation, or after a truncation/replacement). Rebuilds
|
|
137226
|
+
* from byte 0: drops any existing rows, walks the whole file in chunks with a
|
|
137227
|
+
* running parse state, yields to the event loop every ~1000 lines so a large
|
|
137228
|
+
* file never blocks, and writes index rows + file_state.
|
|
137229
|
+
*
|
|
137230
|
+
* Single-flighted per path: concurrent callers await the same walk. The
|
|
137231
|
+
* triggering detail request is served by the scanner fallback while this runs.
|
|
137232
|
+
*/
|
|
137233
|
+
backfillIndex(filePath) {
|
|
137234
|
+
const inFlight = this.backfillInFlight.get(filePath);
|
|
137235
|
+
if (inFlight) return inFlight;
|
|
137236
|
+
const walk = this.runBackfill(filePath).finally(() => {
|
|
137237
|
+
this.backfillInFlight.delete(filePath);
|
|
137238
|
+
});
|
|
137239
|
+
this.backfillInFlight.set(filePath, walk);
|
|
137240
|
+
return walk;
|
|
137241
|
+
}
|
|
137242
|
+
async runBackfill(filePath) {
|
|
137243
|
+
const convId = _ConversationCache.conversationIdForFile(filePath);
|
|
137244
|
+
this.deleteFileIndex(filePath, convId);
|
|
137245
|
+
this.indexParseState.delete(filePath);
|
|
137246
|
+
const CHUNK = 256 * 1024;
|
|
137247
|
+
const YIELD_EVERY = 1e3;
|
|
137248
|
+
const state = initialConvState();
|
|
137249
|
+
const fh = await (0, import_promises12.open)(filePath, "r");
|
|
137250
|
+
let fileOffset = 0;
|
|
137251
|
+
let carry = Buffer.alloc(0);
|
|
137252
|
+
let nextIndex = 0;
|
|
137253
|
+
let linesSinceYield = 0;
|
|
137254
|
+
let lastConsumedEnd = 0;
|
|
137255
|
+
let stat10;
|
|
137256
|
+
try {
|
|
137257
|
+
stat10 = await fh.stat();
|
|
137258
|
+
const buf = Buffer.alloc(CHUNK);
|
|
137259
|
+
for (; ; ) {
|
|
137260
|
+
const { bytesRead } = await fh.read(buf, 0, CHUNK, null);
|
|
137261
|
+
if (bytesRead === 0) break;
|
|
137262
|
+
const combined = carry.length > 0 ? Buffer.concat([carry, buf.subarray(0, bytesRead)]) : buf.subarray(0, bytesRead);
|
|
137263
|
+
const { spans, consumed } = splitCompleteLines(combined, fileOffset);
|
|
137264
|
+
const rows = [];
|
|
137265
|
+
for (const span of spans) {
|
|
137266
|
+
const msg = parseJsonlLine(span.text, state);
|
|
137267
|
+
linesSinceYield++;
|
|
137268
|
+
if (msg) {
|
|
137269
|
+
rows.push({
|
|
137270
|
+
conversation_id: convId,
|
|
137271
|
+
message_index: nextIndex,
|
|
137272
|
+
byte_offset: span.byteOffset,
|
|
137273
|
+
byte_length: span.byteLength,
|
|
137274
|
+
uuid: msg.uuid ?? null,
|
|
137275
|
+
role: msg.role ?? null,
|
|
137276
|
+
ts: msg.timestamp ? Date.parse(msg.timestamp) || null : null
|
|
137277
|
+
});
|
|
137278
|
+
nextIndex++;
|
|
137279
|
+
}
|
|
137280
|
+
if (linesSinceYield >= YIELD_EVERY) {
|
|
137281
|
+
linesSinceYield = 0;
|
|
137282
|
+
await (0, import_promises13.setImmediate)();
|
|
137283
|
+
}
|
|
137284
|
+
}
|
|
137285
|
+
if (rows.length > 0) this.appendMessageIndexRows(rows);
|
|
137286
|
+
lastConsumedEnd = fileOffset + consumed;
|
|
137287
|
+
carry = Buffer.from(combined.subarray(consumed));
|
|
137288
|
+
fileOffset += consumed;
|
|
137289
|
+
}
|
|
137290
|
+
} finally {
|
|
137291
|
+
await fh.close();
|
|
137292
|
+
}
|
|
137293
|
+
this.upsertFileState({
|
|
137294
|
+
path: filePath,
|
|
137295
|
+
identity: fileIdentity(stat10),
|
|
137296
|
+
size: stat10.size,
|
|
137297
|
+
mtime_ms: Math.round(stat10.mtimeMs),
|
|
137298
|
+
byte_offset: lastConsumedEnd,
|
|
137299
|
+
last_message_index: nextIndex - 1
|
|
137300
|
+
});
|
|
137301
|
+
this.indexParseState.set(filePath, state);
|
|
137302
|
+
}
|
|
137303
|
+
/**
|
|
137304
|
+
* Windowed detail read straight from the offset index — the hot path.
|
|
137305
|
+
* Returns the parsed messages for message_index in [fromIndex, toIndex) plus
|
|
137306
|
+
* the total indexed count, or null when the index can't serve this file (no
|
|
137307
|
+
* file_state, identity/size mismatch = truncation/replacement, or cold index)
|
|
137308
|
+
* so the caller falls back to the scanner and enqueues a backfill.
|
|
137309
|
+
*
|
|
137310
|
+
* On a match it SQL-selects the window's byte ranges and preads exactly those
|
|
137311
|
+
* ranges from the JSONL (never the whole file), parsing only the sliced lines.
|
|
137312
|
+
* Returns messages in the same ConversationMessage shape parseJsonlLine
|
|
137313
|
+
* produces during a scan, so the payload is identical to the scanner path.
|
|
137314
|
+
*/
|
|
137315
|
+
readMessageWindow(filePath, fromIndex, toIndex) {
|
|
137316
|
+
const fileState = this.getFileState(filePath);
|
|
137317
|
+
if (!fileState) return null;
|
|
137318
|
+
let stat10;
|
|
137319
|
+
try {
|
|
137320
|
+
stat10 = (0, import_fs17.statSync)(filePath);
|
|
137321
|
+
} catch {
|
|
137322
|
+
return null;
|
|
137323
|
+
}
|
|
137324
|
+
if (fileIdentity(stat10) !== fileState.identity || stat10.size !== fileState.byte_offset) {
|
|
137325
|
+
return null;
|
|
137326
|
+
}
|
|
137327
|
+
const total = fileState.last_message_index + 1;
|
|
137328
|
+
const from = Math.max(0, fromIndex);
|
|
137329
|
+
const to2 = Math.min(toIndex, total);
|
|
137330
|
+
if (to2 <= from) return { messages: [], total, fromIndex: from };
|
|
137331
|
+
const rows = this.getMessageIndexWindow(
|
|
137332
|
+
_ConversationCache.conversationIdForFile(filePath),
|
|
137333
|
+
from,
|
|
137334
|
+
to2
|
|
137335
|
+
);
|
|
137336
|
+
if (rows.length === 0) return { messages: [], total, fromIndex: from };
|
|
137337
|
+
const messages = [];
|
|
137338
|
+
const fd = (0, import_fs17.openSync)(filePath, "r");
|
|
137339
|
+
try {
|
|
137340
|
+
const state = initialConvState();
|
|
137341
|
+
for (const row of rows) {
|
|
137342
|
+
const buf = Buffer.alloc(row.byte_length);
|
|
137343
|
+
(0, import_fs17.readSync)(fd, buf, 0, row.byte_length, row.byte_offset);
|
|
137344
|
+
const msg = parseJsonlLine(buf.toString("utf-8"), state);
|
|
137345
|
+
if (msg) messages.push(msg);
|
|
137346
|
+
}
|
|
137347
|
+
} finally {
|
|
137348
|
+
(0, import_fs17.closeSync)(fd);
|
|
137349
|
+
}
|
|
137350
|
+
return { messages, total, fromIndex: from };
|
|
137351
|
+
}
|
|
137016
137352
|
agentEntrypointsKey() {
|
|
137017
137353
|
return [...this.agentEntrypoints].sort().join(",");
|
|
137018
137354
|
}
|
|
@@ -137736,7 +138072,7 @@ var ConversationsRepository = class {
|
|
|
137736
138072
|
};
|
|
137737
138073
|
|
|
137738
138074
|
// src/db/repositories/projects.repository.ts
|
|
137739
|
-
var
|
|
138075
|
+
var import_crypto6 = require("crypto");
|
|
137740
138076
|
|
|
137741
138077
|
// src/utils/canonicalizeProjectPath.ts
|
|
137742
138078
|
function canonicalizeProjectPath(projectPath) {
|
|
@@ -137829,7 +138165,7 @@ var ProjectsRepository = class {
|
|
|
137829
138165
|
});
|
|
137830
138166
|
return rowToProject(this.getById.get(existing.id));
|
|
137831
138167
|
}
|
|
137832
|
-
const id = (0,
|
|
138168
|
+
const id = (0, import_crypto6.randomUUID)();
|
|
137833
138169
|
this.insert.run({
|
|
137834
138170
|
id,
|
|
137835
138171
|
path: path2,
|
|
@@ -137924,7 +138260,7 @@ var import_path19 = require("path");
|
|
|
137924
138260
|
|
|
137925
138261
|
// src/codex-pty-runner.ts
|
|
137926
138262
|
var import_headless = __toESM(require_xterm_headless(), 1);
|
|
137927
|
-
var
|
|
138263
|
+
var import_crypto7 = require("crypto");
|
|
137928
138264
|
var import_fs21 = require("fs");
|
|
137929
138265
|
var import_path17 = require("path");
|
|
137930
138266
|
init_logger();
|
|
@@ -138179,7 +138515,7 @@ var CodexPtyRunner = class {
|
|
|
138179
138515
|
// ManagedSession handle only.
|
|
138180
138516
|
async startFresh(options) {
|
|
138181
138517
|
const nodePty = await loadPty();
|
|
138182
|
-
const sessionId = (0,
|
|
138518
|
+
const sessionId = (0, import_crypto7.randomUUID)();
|
|
138183
138519
|
const projectName = options.projectName ?? (0, import_path17.basename)(options.projectPath);
|
|
138184
138520
|
const args = ["--cd", options.projectPath, "--no-alt-screen"];
|
|
138185
138521
|
if (options.systemPrompt) {
|
|
@@ -138508,7 +138844,7 @@ function stripAnsi(str) {
|
|
|
138508
138844
|
|
|
138509
138845
|
// src/pty-manager.ts
|
|
138510
138846
|
var import_headless2 = __toESM(require_xterm_headless(), 1);
|
|
138511
|
-
var
|
|
138847
|
+
var import_crypto8 = require("crypto");
|
|
138512
138848
|
var import_fs22 = require("fs");
|
|
138513
138849
|
var import_path18 = require("path");
|
|
138514
138850
|
init_logger();
|
|
@@ -138912,7 +139248,7 @@ var PTYManager = class {
|
|
|
138912
139248
|
// onReady fires once Claude reaches its first prompt (waiting_input).
|
|
138913
139249
|
async startFresh(options) {
|
|
138914
139250
|
const nodePty = await loadPty2();
|
|
138915
|
-
const sessionId = (0,
|
|
139251
|
+
const sessionId = (0, import_crypto8.randomUUID)();
|
|
138916
139252
|
const projectName = options.projectName ?? (0, import_path18.basename)(options.projectPath);
|
|
138917
139253
|
const args = [
|
|
138918
139254
|
"--permission-mode",
|
|
@@ -139496,7 +139832,7 @@ var LiveSessionManager = class {
|
|
|
139496
139832
|
init_logger();
|
|
139497
139833
|
|
|
139498
139834
|
// src/pair-store.ts
|
|
139499
|
-
var
|
|
139835
|
+
var import_crypto9 = require("crypto");
|
|
139500
139836
|
var DEFAULT_TTL_SECONDS = 180;
|
|
139501
139837
|
var SWEEP_INTERVAL_MS = 6e4;
|
|
139502
139838
|
var PairTokenStore = class {
|
|
@@ -139511,7 +139847,7 @@ var PairTokenStore = class {
|
|
|
139511
139847
|
}
|
|
139512
139848
|
}
|
|
139513
139849
|
mint() {
|
|
139514
|
-
const token = `pt_${(0,
|
|
139850
|
+
const token = `pt_${(0, import_crypto9.randomBytes)(16).toString("hex")}`;
|
|
139515
139851
|
const expiresAt = Date.now() + this.ttlMs;
|
|
139516
139852
|
this.current = { token, expiresAt, used: false };
|
|
139517
139853
|
return {
|
|
@@ -139716,11 +140052,11 @@ function seal(plaintext, recipientPublicKeyBase64) {
|
|
|
139716
140052
|
// node_modules/chokidar/index.js
|
|
139717
140053
|
var import_node_events2 = require("events");
|
|
139718
140054
|
var import_node_fs7 = require("fs");
|
|
139719
|
-
var
|
|
140055
|
+
var import_promises16 = require("fs/promises");
|
|
139720
140056
|
var sp2 = __toESM(require("path"), 1);
|
|
139721
140057
|
|
|
139722
140058
|
// node_modules/readdirp/index.js
|
|
139723
|
-
var
|
|
140059
|
+
var import_promises14 = require("fs/promises");
|
|
139724
140060
|
var import_node_path9 = require("path");
|
|
139725
140061
|
var import_node_stream2 = require("stream");
|
|
139726
140062
|
var EntryTypes2 = {
|
|
@@ -139801,7 +140137,7 @@ var ReaddirpStream2 = class extends import_node_stream2.Readable {
|
|
|
139801
140137
|
const { root, type } = opts;
|
|
139802
140138
|
this._fileFilter = normalizeFilter2(opts.fileFilter);
|
|
139803
140139
|
this._directoryFilter = normalizeFilter2(opts.directoryFilter);
|
|
139804
|
-
const statMethod = opts.lstat ?
|
|
140140
|
+
const statMethod = opts.lstat ? import_promises14.lstat : import_promises14.stat;
|
|
139805
140141
|
if (wantBigintFsStats2) {
|
|
139806
140142
|
this._stat = (path2) => statMethod(path2, { bigint: true });
|
|
139807
140143
|
} else {
|
|
@@ -139872,7 +140208,7 @@ var ReaddirpStream2 = class extends import_node_stream2.Readable {
|
|
|
139872
140208
|
async _exploreDir(path2, depth) {
|
|
139873
140209
|
let files;
|
|
139874
140210
|
try {
|
|
139875
|
-
files = await (0,
|
|
140211
|
+
files = await (0, import_promises14.readdir)(path2, this._rdOptions);
|
|
139876
140212
|
} catch (error51) {
|
|
139877
140213
|
this._onError(error51);
|
|
139878
140214
|
}
|
|
@@ -139910,8 +140246,8 @@ var ReaddirpStream2 = class extends import_node_stream2.Readable {
|
|
|
139910
140246
|
if (stats && stats.isSymbolicLink()) {
|
|
139911
140247
|
const full = entry.fullPath;
|
|
139912
140248
|
try {
|
|
139913
|
-
const entryRealPath = await (0,
|
|
139914
|
-
const entryRealPathStats = await (0,
|
|
140249
|
+
const entryRealPath = await (0, import_promises14.realpath)(full);
|
|
140250
|
+
const entryRealPathStats = await (0, import_promises14.lstat)(entryRealPath);
|
|
139915
140251
|
if (entryRealPathStats.isFile()) {
|
|
139916
140252
|
return "file";
|
|
139917
140253
|
}
|
|
@@ -139954,7 +140290,7 @@ function readdirp2(root, options = {}) {
|
|
|
139954
140290
|
|
|
139955
140291
|
// node_modules/chokidar/handler.js
|
|
139956
140292
|
var import_node_fs6 = require("fs");
|
|
139957
|
-
var
|
|
140293
|
+
var import_promises15 = require("fs/promises");
|
|
139958
140294
|
var import_node_os3 = require("os");
|
|
139959
140295
|
var sp = __toESM(require("path"), 1);
|
|
139960
140296
|
var STR_DATA2 = "data";
|
|
@@ -139981,7 +140317,7 @@ var EVENTS2 = {
|
|
|
139981
140317
|
};
|
|
139982
140318
|
var EV2 = EVENTS2;
|
|
139983
140319
|
var THROTTLE_MODE_WATCH2 = "watch";
|
|
139984
|
-
var statMethods2 = { lstat:
|
|
140320
|
+
var statMethods2 = { lstat: import_promises15.lstat, stat: import_promises15.stat };
|
|
139985
140321
|
var KEY_LISTENERS2 = "listeners";
|
|
139986
140322
|
var KEY_ERR2 = "errHandlers";
|
|
139987
140323
|
var KEY_RAW2 = "rawEmitters";
|
|
@@ -140338,7 +140674,7 @@ var setFsWatchListener2 = (path2, fullPath, options, handlers) => {
|
|
|
140338
140674
|
cont.watcherUnusable = true;
|
|
140339
140675
|
if (isWindows3 && error51.code === "EPERM") {
|
|
140340
140676
|
try {
|
|
140341
|
-
const fd = await (0,
|
|
140677
|
+
const fd = await (0, import_promises15.open)(path2, "r");
|
|
140342
140678
|
await fd.close();
|
|
140343
140679
|
broadcastErr(error51);
|
|
140344
140680
|
} catch (err) {
|
|
@@ -140469,7 +140805,7 @@ var NodeFsHandler2 = class {
|
|
|
140469
140805
|
return;
|
|
140470
140806
|
if (!newStats || newStats.mtimeMs === 0) {
|
|
140471
140807
|
try {
|
|
140472
|
-
const newStats2 = await (0,
|
|
140808
|
+
const newStats2 = await (0, import_promises15.stat)(file2);
|
|
140473
140809
|
if (this.fsw.closed)
|
|
140474
140810
|
return;
|
|
140475
140811
|
const at2 = newStats2.atimeMs;
|
|
@@ -140524,7 +140860,7 @@ var NodeFsHandler2 = class {
|
|
|
140524
140860
|
this.fsw._incrReadyCount();
|
|
140525
140861
|
let linkPath;
|
|
140526
140862
|
try {
|
|
140527
|
-
linkPath = await (0,
|
|
140863
|
+
linkPath = await (0, import_promises15.realpath)(path2);
|
|
140528
140864
|
} catch (e) {
|
|
140529
140865
|
this.fsw._emitReady();
|
|
140530
140866
|
return true;
|
|
@@ -140673,7 +141009,7 @@ var NodeFsHandler2 = class {
|
|
|
140673
141009
|
let closer;
|
|
140674
141010
|
if (stats.isDirectory()) {
|
|
140675
141011
|
const absPath = sp.resolve(path2);
|
|
140676
|
-
const targetPath = follow ? await (0,
|
|
141012
|
+
const targetPath = follow ? await (0, import_promises15.realpath)(path2) : path2;
|
|
140677
141013
|
if (this.fsw.closed)
|
|
140678
141014
|
return;
|
|
140679
141015
|
closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
|
|
@@ -140683,7 +141019,7 @@ var NodeFsHandler2 = class {
|
|
|
140683
141019
|
this.fsw._symlinkPaths.set(absPath, targetPath);
|
|
140684
141020
|
}
|
|
140685
141021
|
} else if (stats.isSymbolicLink()) {
|
|
140686
|
-
const targetPath = follow ? await (0,
|
|
141022
|
+
const targetPath = follow ? await (0, import_promises15.realpath)(path2) : path2;
|
|
140687
141023
|
if (this.fsw.closed)
|
|
140688
141024
|
return;
|
|
140689
141025
|
const parent = sp.dirname(wh.watchPath);
|
|
@@ -140843,7 +141179,7 @@ var DirEntry2 = class {
|
|
|
140843
141179
|
return;
|
|
140844
141180
|
const dir = this.path;
|
|
140845
141181
|
try {
|
|
140846
|
-
await (0,
|
|
141182
|
+
await (0, import_promises16.readdir)(dir);
|
|
140847
141183
|
} catch (err) {
|
|
140848
141184
|
if (this._removeWatcher) {
|
|
140849
141185
|
this._removeWatcher(sp2.dirname(dir), sp2.basename(dir));
|
|
@@ -141193,7 +141529,7 @@ var FSWatcher2 = class extends import_node_events2.EventEmitter {
|
|
|
141193
141529
|
const fullPath = opts.cwd ? sp2.join(opts.cwd, path2) : path2;
|
|
141194
141530
|
let stats2;
|
|
141195
141531
|
try {
|
|
141196
|
-
stats2 = await (0,
|
|
141532
|
+
stats2 = await (0, import_promises16.stat)(fullPath);
|
|
141197
141533
|
} catch (err) {
|
|
141198
141534
|
}
|
|
141199
141535
|
if (!stats2 || this.closed)
|
|
@@ -141447,18 +141783,20 @@ var chokidar_default = { watch: watch2, FSWatcher: FSWatcher2 };
|
|
|
141447
141783
|
|
|
141448
141784
|
// src/services/conversations/conversationWatcher.ts
|
|
141449
141785
|
var import_fs23 = require("fs");
|
|
141450
|
-
var
|
|
141786
|
+
var import_promises17 = require("fs/promises");
|
|
141451
141787
|
var ConversationWatcher = class {
|
|
141452
141788
|
files = /* @__PURE__ */ new Map();
|
|
141453
141789
|
directories = /* @__PURE__ */ new Map();
|
|
141454
141790
|
onNewLine;
|
|
141455
141791
|
onNewLines;
|
|
141792
|
+
onNewLineSpans;
|
|
141456
141793
|
onConversationChanged;
|
|
141457
141794
|
onFileDeleted;
|
|
141458
141795
|
onError;
|
|
141459
141796
|
constructor(events = {}) {
|
|
141460
141797
|
this.onNewLine = events.onNewLine;
|
|
141461
141798
|
this.onNewLines = events.onNewLines;
|
|
141799
|
+
this.onNewLineSpans = events.onNewLineSpans;
|
|
141462
141800
|
this.onConversationChanged = events.onConversationChanged;
|
|
141463
141801
|
this.onFileDeleted = events.onFileDeleted;
|
|
141464
141802
|
this.onError = events.onError;
|
|
@@ -141551,20 +141889,24 @@ var ConversationWatcher = class {
|
|
|
141551
141889
|
entry.reading = true;
|
|
141552
141890
|
try {
|
|
141553
141891
|
for (; ; ) {
|
|
141554
|
-
const st2 = await (0,
|
|
141892
|
+
const st2 = await (0, import_promises17.stat)(filePath);
|
|
141555
141893
|
if (st2.size <= entry.offset) break;
|
|
141556
141894
|
const readFrom = entry.offset;
|
|
141557
141895
|
const bytesToRead = st2.size - readFrom;
|
|
141558
141896
|
const buf = Buffer.alloc(bytesToRead);
|
|
141559
|
-
const fh = await (0,
|
|
141897
|
+
const fh = await (0, import_promises17.open)(filePath, "r");
|
|
141560
141898
|
try {
|
|
141561
141899
|
await fh.read(buf, 0, bytesToRead, readFrom);
|
|
141562
141900
|
} finally {
|
|
141563
141901
|
await fh.close();
|
|
141564
141902
|
}
|
|
141565
|
-
|
|
141903
|
+
const { spans, consumed } = splitCompleteLines(buf, readFrom);
|
|
141904
|
+
entry.offset = readFrom + consumed;
|
|
141566
141905
|
if (!this.files.has(filePath)) return;
|
|
141567
|
-
const lines =
|
|
141906
|
+
const lines = spans.map((s3) => s3.text);
|
|
141907
|
+
if (spans.length > 0) {
|
|
141908
|
+
this.onNewLineSpans?.(filePath, spans, readFrom, entry.offset);
|
|
141909
|
+
}
|
|
141568
141910
|
if (this.onNewLines) {
|
|
141569
141911
|
this.onNewLines(filePath, lines);
|
|
141570
141912
|
} else {
|
|
@@ -142004,8 +142346,8 @@ function discoveredToResponse(d, conversationId) {
|
|
|
142004
142346
|
}
|
|
142005
142347
|
|
|
142006
142348
|
// src/uploads.ts
|
|
142007
|
-
var
|
|
142008
|
-
var
|
|
142349
|
+
var import_crypto10 = require("crypto");
|
|
142350
|
+
var import_promises18 = require("fs/promises");
|
|
142009
142351
|
var import_heic_convert = __toESM(require_heic_convert(), 1);
|
|
142010
142352
|
var import_path21 = require("path");
|
|
142011
142353
|
var UPLOAD_DIR_NAME = ".threadbase-uploads";
|
|
@@ -142038,12 +142380,12 @@ async function saveUploadFile(input) {
|
|
|
142038
142380
|
mimeType = "image/jpeg";
|
|
142039
142381
|
originalName = originalName.replace(/\.(heic|heif)$/i, ".jpg");
|
|
142040
142382
|
}
|
|
142041
|
-
const id = `up_${(0,
|
|
142383
|
+
const id = `up_${(0, import_crypto10.randomBytes)(8).toString("hex")}`;
|
|
142042
142384
|
const safeName = sanitizeFilename(originalName) || `file${MIME_TO_EXT[mimeType] ?? ""}`;
|
|
142043
142385
|
const dir = (0, import_path21.join)(input.projectPath, UPLOAD_DIR_NAME, input.sessionId);
|
|
142044
|
-
await (0,
|
|
142386
|
+
await (0, import_promises18.mkdir)(dir, { recursive: true });
|
|
142045
142387
|
const filePath = (0, import_path21.join)(dir, `${Date.now()}-${id}-${safeName}`);
|
|
142046
|
-
await (0,
|
|
142388
|
+
await (0, import_promises18.writeFile)(filePath, buffer);
|
|
142047
142389
|
return {
|
|
142048
142390
|
id,
|
|
142049
142391
|
filePath,
|
|
@@ -142442,6 +142784,10 @@ var StreamerServer = class {
|
|
|
142442
142784
|
fileWatcher;
|
|
142443
142785
|
sessionFileMap = /* @__PURE__ */ new Map();
|
|
142444
142786
|
// sessionId → JSONL filePath
|
|
142787
|
+
// Per-file seq assignments from the most recent onNewLineSpans (offset index),
|
|
142788
|
+
// handed to the immediately-following onNewLines so it can stamp WS `seq` on
|
|
142789
|
+
// the matching conversation_events entries. Same read → same lines order.
|
|
142790
|
+
pendingLineSeqs = /* @__PURE__ */ new Map();
|
|
142445
142791
|
pendingQuestions = /* @__PURE__ */ new Map();
|
|
142446
142792
|
// Content key of the AskUserQuestion currently broadcast for a session (from
|
|
142447
142793
|
// either the rendered screen or JSONL), used to de-dupe the two paths: when
|
|
@@ -142565,7 +142911,7 @@ var StreamerServer = class {
|
|
|
142565
142911
|
this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);
|
|
142566
142912
|
const rawRoot = process.env.THREADBASE_BROWSE_ROOT ?? loadBrowseRoot() ?? config2.browseRoot;
|
|
142567
142913
|
if (rawRoot) {
|
|
142568
|
-
(0,
|
|
142914
|
+
(0, import_promises19.realpath)(rawRoot).then((resolved) => {
|
|
142569
142915
|
this.browseRoot = resolved;
|
|
142570
142916
|
if (this.verbose) this.log.info(`Browse root: ${resolved}`, { browseRoot: resolved });
|
|
142571
142917
|
}).catch(() => {
|
|
@@ -142587,6 +142933,42 @@ var StreamerServer = class {
|
|
|
142587
142933
|
this.sessionStore = new SessionStore();
|
|
142588
142934
|
this.wsHub = new WSHub();
|
|
142589
142935
|
this.fileWatcher = new ConversationWatcher({
|
|
142936
|
+
onNewLineSpans: (filePath, spans, readFrom, endOffset) => {
|
|
142937
|
+
if (!this.cache) return;
|
|
142938
|
+
const cache = this.cache;
|
|
142939
|
+
this.pendingLineSeqs.delete(filePath);
|
|
142940
|
+
try {
|
|
142941
|
+
const seqs = cache.extendMessageIndex(
|
|
142942
|
+
filePath,
|
|
142943
|
+
spans,
|
|
142944
|
+
(0, import_fs25.statSync)(filePath),
|
|
142945
|
+
readFrom,
|
|
142946
|
+
endOffset
|
|
142947
|
+
);
|
|
142948
|
+
if (seqs === null) {
|
|
142949
|
+
cache.deleteFileIndex(filePath, ConversationCache.conversationIdForFile(filePath));
|
|
142950
|
+
cache.clearIndexParseState(filePath);
|
|
142951
|
+
this.trackCacheWrite(
|
|
142952
|
+
cache.backfillIndex(filePath).catch((err) => {
|
|
142953
|
+
this.log.warn("offset-index.backfill_failed", {
|
|
142954
|
+
event: "offset_index.backfill_failed",
|
|
142955
|
+
filePath,
|
|
142956
|
+
trigger: "noncontiguous-append",
|
|
142957
|
+
err
|
|
142958
|
+
});
|
|
142959
|
+
})
|
|
142960
|
+
);
|
|
142961
|
+
return;
|
|
142962
|
+
}
|
|
142963
|
+
this.pendingLineSeqs.set(filePath, seqs);
|
|
142964
|
+
} catch (err) {
|
|
142965
|
+
this.log.warn("offset-index.extend_failed", {
|
|
142966
|
+
event: "offset_index.extend_failed",
|
|
142967
|
+
filePath,
|
|
142968
|
+
err
|
|
142969
|
+
});
|
|
142970
|
+
}
|
|
142971
|
+
},
|
|
142590
142972
|
onNewLines: (filePath, lines) => {
|
|
142591
142973
|
this.cache?.updateFromLines(filePath, lines);
|
|
142592
142974
|
for (const [sessionId, watchedPath] of this.sessionFileMap) {
|
|
@@ -142613,13 +142995,20 @@ var StreamerServer = class {
|
|
|
142613
142995
|
this.pendingQuestionKey.set(sessionId, key);
|
|
142614
142996
|
if (broadcast) this.wsHub.broadcast(m2);
|
|
142615
142997
|
}
|
|
142616
|
-
this.
|
|
142998
|
+
const seqs = this.pendingLineSeqs.get(filePath);
|
|
142999
|
+
this.wsHub.broadcast({
|
|
143000
|
+
type: "conversation_events",
|
|
143001
|
+
sessionId,
|
|
143002
|
+
lines,
|
|
143003
|
+
...seqs && seqs.length === lines.length ? { seqs } : {}
|
|
143004
|
+
});
|
|
142617
143005
|
for (const line of lines) {
|
|
142618
143006
|
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
142619
143007
|
}
|
|
142620
143008
|
break;
|
|
142621
143009
|
}
|
|
142622
143010
|
}
|
|
143011
|
+
this.pendingLineSeqs.delete(filePath);
|
|
142623
143012
|
},
|
|
142624
143013
|
onConversationChanged: (filePath) => {
|
|
142625
143014
|
this.fileWatcher.poke(filePath);
|
|
@@ -143703,9 +144092,13 @@ var StreamerServer = class {
|
|
|
143703
144092
|
return;
|
|
143704
144093
|
}
|
|
143705
144094
|
const etagSource = conversation;
|
|
144095
|
+
const indexedCount = etagSource.filePath && this.cache ? this.cache.getIndexedMessageCount(
|
|
144096
|
+
ConversationCache.conversationIdForFile(etagSource.filePath)
|
|
144097
|
+
) : 0;
|
|
144098
|
+
const etagMessageCount = Math.max(etagSource.messageCount, indexedCount);
|
|
143706
144099
|
const etag = computeConversationEtag({
|
|
143707
144100
|
filePath: etagSource.filePath,
|
|
143708
|
-
messageCount:
|
|
144101
|
+
messageCount: etagMessageCount,
|
|
143709
144102
|
timestamp: etagSource.timestamp
|
|
143710
144103
|
});
|
|
143711
144104
|
const isFirstPage = !url2.searchParams.has("before_index") && !url2.searchParams.has("anchor_index") && !url2.searchParams.has("after_index");
|
|
@@ -143722,12 +144115,14 @@ var StreamerServer = class {
|
|
|
143722
144115
|
let slice = filtered;
|
|
143723
144116
|
let fromIdx = 0;
|
|
143724
144117
|
let messagePagination;
|
|
144118
|
+
let indexTotal = null;
|
|
143725
144119
|
if (usePaging) {
|
|
143726
144120
|
const limit = Math.min(Math.max(intParam(url2, "msg_limit", 80), 1), 500);
|
|
143727
144121
|
let beforeIndex = total;
|
|
143728
144122
|
let scanLimit = limit;
|
|
143729
144123
|
let anchorIndex = null;
|
|
143730
144124
|
let newerPaging = false;
|
|
144125
|
+
let usedAfterIndex = false;
|
|
143731
144126
|
if (url2.searchParams.has("before_index")) {
|
|
143732
144127
|
beforeIndex = intParam(url2, "before_index", total);
|
|
143733
144128
|
beforeIndex = Math.min(Math.max(beforeIndex, 0), total);
|
|
@@ -143736,6 +144131,7 @@ var StreamerServer = class {
|
|
|
143736
144131
|
beforeIndex = Math.min(total, from + limit);
|
|
143737
144132
|
scanLimit = beforeIndex - from;
|
|
143738
144133
|
newerPaging = true;
|
|
144134
|
+
usedAfterIndex = true;
|
|
143739
144135
|
} else if (hasAnchor) {
|
|
143740
144136
|
anchorIndex = Math.min(
|
|
143741
144137
|
Math.max(intParam(url2, "anchor_index", 0), 0),
|
|
@@ -143745,9 +144141,34 @@ var StreamerServer = class {
|
|
|
143745
144141
|
beforeIndex = Math.min(total, from + limit);
|
|
143746
144142
|
newerPaging = true;
|
|
143747
144143
|
}
|
|
143748
|
-
const
|
|
143749
|
-
const
|
|
143750
|
-
|
|
144144
|
+
const isTailRequest = !url2.searchParams.has("before_index") && !hasAfter && !hasAnchor;
|
|
144145
|
+
const indexFilePath = conversation.filePath;
|
|
144146
|
+
if (isTailRequest && indexFilePath && this.cache) {
|
|
144147
|
+
const indexed = this.cache.getIndexedMessageCount(
|
|
144148
|
+
ConversationCache.conversationIdForFile(indexFilePath)
|
|
144149
|
+
);
|
|
144150
|
+
if (indexed > beforeIndex) {
|
|
144151
|
+
beforeIndex = indexed;
|
|
144152
|
+
}
|
|
144153
|
+
}
|
|
144154
|
+
const windowStart = Math.max(0, beforeIndex - scanLimit);
|
|
144155
|
+
const indexWindow = scanLimit > 0 && !hasAnchor && indexFilePath && this.cache ? this.cache.readMessageWindow(indexFilePath, windowStart, beforeIndex) : null;
|
|
144156
|
+
if (!indexWindow && indexFilePath && this.cache && !hasAnchor) {
|
|
144157
|
+
this.trackCacheWrite(
|
|
144158
|
+
this.cache.backfillIndex(indexFilePath).catch((err) => {
|
|
144159
|
+
this.log.warn("offset-index.backfill_failed", {
|
|
144160
|
+
event: "offset_index.backfill_failed",
|
|
144161
|
+
conversationId: id,
|
|
144162
|
+
filePath: indexFilePath,
|
|
144163
|
+
err
|
|
144164
|
+
});
|
|
144165
|
+
})
|
|
144166
|
+
);
|
|
144167
|
+
}
|
|
144168
|
+
const pagedScanner = !indexWindow && this.scannerReady ? await this.getScanner(true) : null;
|
|
144169
|
+
const page = indexWindow ?? (scanLimit > 0 && pagedScanner && typeof pagedScanner.getConversationPage === "function" ? await pagedScanner.getConversationPage(id, { beforeIndex, limit: scanLimit }) : null);
|
|
144170
|
+
if (indexWindow) indexTotal = indexWindow.total;
|
|
144171
|
+
const start = page?.fromIndex ?? windowStart;
|
|
143751
144172
|
slice = page?.messages ?? filtered.slice(start, beforeIndex);
|
|
143752
144173
|
fromIdx = start;
|
|
143753
144174
|
const effectiveTotal = page?.total ?? total;
|
|
@@ -143763,6 +144184,9 @@ var StreamerServer = class {
|
|
|
143763
144184
|
messagePagination.has_more_newer = beforeIndex < effectiveTotal;
|
|
143764
144185
|
messagePagination.next_after_index = beforeIndex < effectiveTotal ? beforeIndex : null;
|
|
143765
144186
|
}
|
|
144187
|
+
if (usedAfterIndex) {
|
|
144188
|
+
messagePagination.etag = etag;
|
|
144189
|
+
}
|
|
143766
144190
|
}
|
|
143767
144191
|
const messagesPayload = slice.map((m2, localIdx) => {
|
|
143768
144192
|
const content = [];
|
|
@@ -143804,6 +144228,8 @@ var StreamerServer = class {
|
|
|
143804
144228
|
const cachedConvMeta = this.cache?.getMetaById(id);
|
|
143805
144229
|
const convProvider = coerceProviderForRunner(conv.provider ?? cachedConvMeta?.provider);
|
|
143806
144230
|
const availability = classifyResumability(conv.projectPath);
|
|
144231
|
+
const metaMessageCount = indexTotal != null && indexTotal > conv.messageCount ? indexTotal : conv.messageCount;
|
|
144232
|
+
const metaLastUpdatedAt = indexTotal != null && indexTotal > conv.messageCount ? slice.at(-1)?.timestamp ?? conv.timestamp : conv.timestamp;
|
|
143807
144233
|
const body = {
|
|
143808
144234
|
meta: {
|
|
143809
144235
|
id,
|
|
@@ -143811,8 +144237,8 @@ var StreamerServer = class {
|
|
|
143811
144237
|
project_name: conv.projectName,
|
|
143812
144238
|
project_path: conv.projectPath,
|
|
143813
144239
|
file_path: conv.filePath,
|
|
143814
|
-
last_updated_at:
|
|
143815
|
-
message_count:
|
|
144240
|
+
last_updated_at: metaLastUpdatedAt,
|
|
144241
|
+
message_count: metaMessageCount,
|
|
143816
144242
|
last_prompt: conv.lastPrompt ?? void 0,
|
|
143817
144243
|
provider: convProvider,
|
|
143818
144244
|
resumable: isProviderResumable(convProvider, availability.resumable),
|
|
@@ -145094,7 +145520,7 @@ function isBrewInstall(scriptPath = process.argv[1] ?? "") {
|
|
|
145094
145520
|
var import_node_crypto4 = require("crypto");
|
|
145095
145521
|
var import_node_fs8 = require("fs");
|
|
145096
145522
|
var import_node_path10 = require("path");
|
|
145097
|
-
var
|
|
145523
|
+
var import_promises20 = require("stream/promises");
|
|
145098
145524
|
|
|
145099
145525
|
// src/updater/manifest.ts
|
|
145100
145526
|
init_zod();
|
|
@@ -145169,7 +145595,7 @@ async function downloadAndVerify(opts) {
|
|
|
145169
145595
|
}
|
|
145170
145596
|
});
|
|
145171
145597
|
try {
|
|
145172
|
-
await (0,
|
|
145598
|
+
await (0, import_promises20.pipeline)(res.body.pipeThrough(measured), out);
|
|
145173
145599
|
} catch (err) {
|
|
145174
145600
|
(0, import_node_fs8.rmSync)(targetPath, { force: true });
|
|
145175
145601
|
throw err;
|
|
@@ -145383,7 +145809,7 @@ var import_fs29 = __toESM(require("fs"), 1);
|
|
|
145383
145809
|
var import_node_fs14 = __toESM(require("fs"), 1);
|
|
145384
145810
|
var import_node_path19 = __toESM(require("path"), 1);
|
|
145385
145811
|
var import_node_fs15 = __toESM(require("fs"), 1);
|
|
145386
|
-
var
|
|
145812
|
+
var import_promises21 = __toESM(require("fs/promises"), 1);
|
|
145387
145813
|
var import_node_path20 = __toESM(require("path"), 1);
|
|
145388
145814
|
var import_node_path21 = require("path");
|
|
145389
145815
|
var import_node_fs16 = __toESM(require("fs"), 1);
|
|
@@ -147615,7 +148041,7 @@ var gr = (s3, t2, e) => {
|
|
|
147615
148041
|
E2 ? e(E2) : x2 && a ? Es(x2, o, h, (Le) => S2(Le)) : n ? import_node_fs15.default.chmod(s3, r, e) : e();
|
|
147616
148042
|
};
|
|
147617
148043
|
if (s3 === d) return no(s3, S2);
|
|
147618
|
-
if (l) return
|
|
148044
|
+
if (l) return import_promises21.default.mkdir(s3, { mode: r, recursive: true }).then((E2) => S2(null, E2 ?? void 0), S2);
|
|
147619
148045
|
let D2 = f(import_node_path20.default.relative(d, s3)).split("/");
|
|
147620
148046
|
Ss(d, D2, r, c, d, void 0, S2);
|
|
147621
148047
|
};
|