@threadbase-sh/streamer 1.29.0 → 1.29.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 +7155 -5562
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +165 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +165 -26
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1076,6 +1076,23 @@ declare class StreamerServer {
|
|
|
1076
1076
|
private rescanForRefresh;
|
|
1077
1077
|
private findJsonlPath;
|
|
1078
1078
|
private readCwdFromJsonl;
|
|
1079
|
+
/**
|
|
1080
|
+
* Live Codex sessions keep `SessionResponse.conversationId === managed.id`
|
|
1081
|
+
* (stable deep-link / PTY key) and store the rollout UUID separately as
|
|
1082
|
+
* `boundConversationId`. REST history is indexed under the rollout UUID, so
|
|
1083
|
+
* resolve the placeholder → bound id before looking up the scanner.
|
|
1084
|
+
*/
|
|
1085
|
+
private resolveConversationLookupId;
|
|
1086
|
+
/** File path for a live managed session (placeholder id or bound Codex id). */
|
|
1087
|
+
private findLiveSessionFilePath;
|
|
1088
|
+
/** True when a conversation UUID is the bound rollout of a live PTY session. */
|
|
1089
|
+
private isBoundConversationLive;
|
|
1090
|
+
/**
|
|
1091
|
+
* Broadcast conversation JSONL lines to WS clients. Codex rollout lines are
|
|
1092
|
+
* normalized to the Claude `type:user|assistant` shape mobile understands;
|
|
1093
|
+
* Claude lines pass through unchanged so seq alignment stays intact.
|
|
1094
|
+
*/
|
|
1095
|
+
private broadcastConversationLines;
|
|
1079
1096
|
private findConversationByUuid;
|
|
1080
1097
|
private isConversationSnapshotStale;
|
|
1081
1098
|
private handleGetConversation;
|
package/dist/index.d.ts
CHANGED
|
@@ -1076,6 +1076,23 @@ declare class StreamerServer {
|
|
|
1076
1076
|
private rescanForRefresh;
|
|
1077
1077
|
private findJsonlPath;
|
|
1078
1078
|
private readCwdFromJsonl;
|
|
1079
|
+
/**
|
|
1080
|
+
* Live Codex sessions keep `SessionResponse.conversationId === managed.id`
|
|
1081
|
+
* (stable deep-link / PTY key) and store the rollout UUID separately as
|
|
1082
|
+
* `boundConversationId`. REST history is indexed under the rollout UUID, so
|
|
1083
|
+
* resolve the placeholder → bound id before looking up the scanner.
|
|
1084
|
+
*/
|
|
1085
|
+
private resolveConversationLookupId;
|
|
1086
|
+
/** File path for a live managed session (placeholder id or bound Codex id). */
|
|
1087
|
+
private findLiveSessionFilePath;
|
|
1088
|
+
/** True when a conversation UUID is the bound rollout of a live PTY session. */
|
|
1089
|
+
private isBoundConversationLive;
|
|
1090
|
+
/**
|
|
1091
|
+
* Broadcast conversation JSONL lines to WS clients. Codex rollout lines are
|
|
1092
|
+
* normalized to the Claude `type:user|assistant` shape mobile understands;
|
|
1093
|
+
* Claude lines pass through unchanged so seq alignment stays intact.
|
|
1094
|
+
*/
|
|
1095
|
+
private broadcastConversationLines;
|
|
1079
1096
|
private findConversationByUuid;
|
|
1080
1097
|
private isConversationSnapshotStale;
|
|
1081
1098
|
private handleGetConversation;
|
package/dist/index.js
CHANGED
|
@@ -5292,6 +5292,89 @@ function sanitizeFilename(name) {
|
|
|
5292
5292
|
return cleaned;
|
|
5293
5293
|
}
|
|
5294
5294
|
|
|
5295
|
+
// src/utils/codexConversationLine.ts
|
|
5296
|
+
function extractCodexText(content) {
|
|
5297
|
+
if (typeof content === "string") return content.trim();
|
|
5298
|
+
if (!Array.isArray(content)) return "";
|
|
5299
|
+
return content.map((item) => {
|
|
5300
|
+
if (typeof item === "string") return item;
|
|
5301
|
+
const block = item;
|
|
5302
|
+
const t = block?.type;
|
|
5303
|
+
if ((t === "input_text" || t === "output_text" || t === "text") && typeof block.text === "string") {
|
|
5304
|
+
return block.text;
|
|
5305
|
+
}
|
|
5306
|
+
return "";
|
|
5307
|
+
}).filter(Boolean).join("").trim();
|
|
5308
|
+
}
|
|
5309
|
+
function normalizeCodexLineToClaudeShape(line) {
|
|
5310
|
+
let entry;
|
|
5311
|
+
try {
|
|
5312
|
+
entry = JSON.parse(line);
|
|
5313
|
+
} catch {
|
|
5314
|
+
return null;
|
|
5315
|
+
}
|
|
5316
|
+
if (entry.type !== "response_item") return null;
|
|
5317
|
+
const payload = entry.payload;
|
|
5318
|
+
if (payload?.type !== "message") return null;
|
|
5319
|
+
const role = payload.role;
|
|
5320
|
+
if (role !== "user" && role !== "assistant") return null;
|
|
5321
|
+
const text = extractCodexText(payload.content);
|
|
5322
|
+
if (!text) return null;
|
|
5323
|
+
if (role === "user" && isCodexInjectedContext(text)) return null;
|
|
5324
|
+
const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : (/* @__PURE__ */ new Date()).toISOString();
|
|
5325
|
+
const uuid = typeof payload.id === "string" && payload.id.length > 0 ? payload.id : `codex-${role}-${timestamp}-${hashPrefix(text)}`;
|
|
5326
|
+
return JSON.stringify({
|
|
5327
|
+
type: role,
|
|
5328
|
+
uuid,
|
|
5329
|
+
timestamp,
|
|
5330
|
+
message: {
|
|
5331
|
+
role,
|
|
5332
|
+
content: [{ type: "text", text }]
|
|
5333
|
+
}
|
|
5334
|
+
});
|
|
5335
|
+
}
|
|
5336
|
+
function isCodexRolloutLine(line) {
|
|
5337
|
+
try {
|
|
5338
|
+
const entry = JSON.parse(line);
|
|
5339
|
+
return entry.type === "response_item" || entry.type === "event_msg" || entry.type === "session_meta" || entry.type === "turn_context";
|
|
5340
|
+
} catch {
|
|
5341
|
+
return false;
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
5344
|
+
function toClientConversationLines(lines) {
|
|
5345
|
+
if (lines.length === 0) return lines;
|
|
5346
|
+
const codex = lines.some(isCodexRolloutLine);
|
|
5347
|
+
if (!codex) return lines;
|
|
5348
|
+
const out = [];
|
|
5349
|
+
for (const line of lines) {
|
|
5350
|
+
const normalized = normalizeCodexLineToClaudeShape(line);
|
|
5351
|
+
if (normalized) out.push(normalized);
|
|
5352
|
+
}
|
|
5353
|
+
return out;
|
|
5354
|
+
}
|
|
5355
|
+
function isCodexInjectedContext(text) {
|
|
5356
|
+
if (text.startsWith("# AGENTS.md") || text.includes("<INSTRUCTIONS>")) return true;
|
|
5357
|
+
if (text.startsWith("<permissions instructions>") || text.includes("Filesystem sandboxing defines")) {
|
|
5358
|
+
return true;
|
|
5359
|
+
}
|
|
5360
|
+
if (text.includes("limit the options to at most 3")) return true;
|
|
5361
|
+
if (text.includes("You are working within the project boundary:")) return true;
|
|
5362
|
+
if (text.includes(
|
|
5363
|
+
"Do not read, write, or execute commands that access files or directories outside this boundary"
|
|
5364
|
+
)) {
|
|
5365
|
+
return true;
|
|
5366
|
+
}
|
|
5367
|
+
return false;
|
|
5368
|
+
}
|
|
5369
|
+
function hashPrefix(text) {
|
|
5370
|
+
let h = 0;
|
|
5371
|
+
const sample = text.slice(0, 64);
|
|
5372
|
+
for (let i = 0; i < sample.length; i++) {
|
|
5373
|
+
h = h * 31 + sample.charCodeAt(i) | 0;
|
|
5374
|
+
}
|
|
5375
|
+
return Math.abs(h).toString(36);
|
|
5376
|
+
}
|
|
5377
|
+
|
|
5295
5378
|
// src/utils/conversationEtag.ts
|
|
5296
5379
|
import { createHash as createHash2 } from "crypto";
|
|
5297
5380
|
function computeConversationEtag({
|
|
@@ -5676,15 +5759,7 @@ var StreamerServer = class {
|
|
|
5676
5759
|
if (broadcast) this.wsHub.broadcast(m);
|
|
5677
5760
|
}
|
|
5678
5761
|
const seqs = this.pendingLineSeqs.get(filePath);
|
|
5679
|
-
this.
|
|
5680
|
-
type: "conversation_events",
|
|
5681
|
-
sessionId,
|
|
5682
|
-
lines,
|
|
5683
|
-
...seqs && seqs.length === lines.length ? { seqs } : {}
|
|
5684
|
-
});
|
|
5685
|
-
for (const line of lines) {
|
|
5686
|
-
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
5687
|
-
}
|
|
5762
|
+
this.broadcastConversationLines(sessionId, lines, seqs);
|
|
5688
5763
|
break;
|
|
5689
5764
|
}
|
|
5690
5765
|
}
|
|
@@ -6665,11 +6740,62 @@ var StreamerServer = class {
|
|
|
6665
6740
|
rl.on("error", () => resolve2(null));
|
|
6666
6741
|
});
|
|
6667
6742
|
}
|
|
6743
|
+
/**
|
|
6744
|
+
* Live Codex sessions keep `SessionResponse.conversationId === managed.id`
|
|
6745
|
+
* (stable deep-link / PTY key) and store the rollout UUID separately as
|
|
6746
|
+
* `boundConversationId`. REST history is indexed under the rollout UUID, so
|
|
6747
|
+
* resolve the placeholder → bound id before looking up the scanner.
|
|
6748
|
+
*/
|
|
6749
|
+
resolveConversationLookupId(uuid) {
|
|
6750
|
+
const managed = this.sessionStore.getManaged(uuid);
|
|
6751
|
+
if (managed?.boundConversationId) return managed.boundConversationId;
|
|
6752
|
+
return uuid;
|
|
6753
|
+
}
|
|
6754
|
+
/** File path for a live managed session (placeholder id or bound Codex id). */
|
|
6755
|
+
findLiveSessionFilePath(uuid) {
|
|
6756
|
+
const direct = this.sessionFileMap.get(uuid);
|
|
6757
|
+
if (direct) return direct;
|
|
6758
|
+
for (const s of this.sessionStore.listManaged()) {
|
|
6759
|
+
if (s.boundConversationId === uuid) {
|
|
6760
|
+
return this.sessionFileMap.get(s.id) ?? null;
|
|
6761
|
+
}
|
|
6762
|
+
}
|
|
6763
|
+
return null;
|
|
6764
|
+
}
|
|
6765
|
+
/** True when a conversation UUID is the bound rollout of a live PTY session. */
|
|
6766
|
+
isBoundConversationLive(boundId) {
|
|
6767
|
+
for (const s of this.sessionStore.listManaged()) {
|
|
6768
|
+
if (s.boundConversationId === boundId && this.ptyManager.hasSession(s.id)) {
|
|
6769
|
+
return true;
|
|
6770
|
+
}
|
|
6771
|
+
}
|
|
6772
|
+
return false;
|
|
6773
|
+
}
|
|
6774
|
+
/**
|
|
6775
|
+
* Broadcast conversation JSONL lines to WS clients. Codex rollout lines are
|
|
6776
|
+
* normalized to the Claude `type:user|assistant` shape mobile understands;
|
|
6777
|
+
* Claude lines pass through unchanged so seq alignment stays intact.
|
|
6778
|
+
*/
|
|
6779
|
+
broadcastConversationLines(sessionId, lines, seqs) {
|
|
6780
|
+
const clientLines = toClientConversationLines(lines);
|
|
6781
|
+
if (clientLines.length === 0) return;
|
|
6782
|
+
const seqsOk = !!seqs && seqs.length === lines.length && clientLines.length === lines.length;
|
|
6783
|
+
this.wsHub.broadcast({
|
|
6784
|
+
type: "conversation_events",
|
|
6785
|
+
sessionId,
|
|
6786
|
+
lines: clientLines,
|
|
6787
|
+
...seqsOk ? { seqs } : {}
|
|
6788
|
+
});
|
|
6789
|
+
for (const line of clientLines) {
|
|
6790
|
+
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
6791
|
+
}
|
|
6792
|
+
}
|
|
6668
6793
|
async findConversationByUuid(uuid) {
|
|
6794
|
+
const lookupId = this.resolveConversationLookupId(uuid);
|
|
6669
6795
|
if (!this.scannerReady && !this.scanProfiles) {
|
|
6670
|
-
const filePath2 = this.findJsonlPath(uuid);
|
|
6796
|
+
const filePath2 = this.findJsonlPath(lookupId) ?? this.findLiveSessionFilePath(uuid) ?? this.findLiveSessionFilePath(lookupId);
|
|
6671
6797
|
if (filePath2) {
|
|
6672
|
-
const account = this.cache?.getMetaById(
|
|
6798
|
+
const account = this.cache?.getMetaById(lookupId)?.account ?? void 0;
|
|
6673
6799
|
const coldScanner = this.scanner ?? this.newScanner();
|
|
6674
6800
|
const page = await coldScanner.parseSingleFilePage(filePath2, account, {
|
|
6675
6801
|
limit: Number.MAX_SAFE_INTEGER
|
|
@@ -6679,9 +6805,27 @@ var StreamerServer = class {
|
|
|
6679
6805
|
return null;
|
|
6680
6806
|
}
|
|
6681
6807
|
const scanner = await this.getScanner(true);
|
|
6682
|
-
const fromIndex = await scanner.getConversation(
|
|
6808
|
+
const fromIndex = await scanner.getConversation(lookupId);
|
|
6683
6809
|
if (fromIndex) {
|
|
6684
|
-
if (this.ptyManager.hasSession(uuid)) {
|
|
6810
|
+
if (this.ptyManager.hasSession(uuid) || this.ptyManager.hasSession(lookupId) || this.isBoundConversationLive(lookupId)) {
|
|
6811
|
+
const livePath = this.findLiveSessionFilePath(uuid) ?? this.findLiveSessionFilePath(lookupId) ?? fromIndex.filePath ?? null;
|
|
6812
|
+
const isCodexLive = this.isBoundConversationLive(lookupId) || this.sessionStore.getManaged(uuid)?.provider === CODEX_CLI_PROVIDER;
|
|
6813
|
+
if (isCodexLive && livePath) {
|
|
6814
|
+
try {
|
|
6815
|
+
const account = this.cache?.getMetaById(lookupId)?.account ?? fromIndex.account ?? void 0;
|
|
6816
|
+
const page = await scanner.parseSingleFilePage(livePath, account, {
|
|
6817
|
+
limit: Number.MAX_SAFE_INTEGER
|
|
6818
|
+
});
|
|
6819
|
+
if (page?.conversation) return page.conversation;
|
|
6820
|
+
} catch (err) {
|
|
6821
|
+
this.log.warn("codex.live_reparse_failed", {
|
|
6822
|
+
event: "codex.live_reparse_failed",
|
|
6823
|
+
conversationId: lookupId,
|
|
6824
|
+
filePath: livePath,
|
|
6825
|
+
err
|
|
6826
|
+
});
|
|
6827
|
+
}
|
|
6828
|
+
}
|
|
6685
6829
|
return fromIndex;
|
|
6686
6830
|
}
|
|
6687
6831
|
if (fromIndex.filePath && this.isConversationSnapshotStale(fromIndex)) {
|
|
@@ -6701,12 +6845,12 @@ var StreamerServer = class {
|
|
|
6701
6845
|
return fromIndex;
|
|
6702
6846
|
}
|
|
6703
6847
|
if (this.scanProfiles) return null;
|
|
6704
|
-
const filePath = this.findJsonlPath(uuid);
|
|
6848
|
+
const filePath = this.findJsonlPath(lookupId) ?? this.findLiveSessionFilePath(uuid) ?? this.findLiveSessionFilePath(lookupId);
|
|
6705
6849
|
if (!filePath) return null;
|
|
6706
6850
|
this.scanner = null;
|
|
6707
6851
|
this.scannerReady = null;
|
|
6708
6852
|
const freshScanner = await this.getScanner();
|
|
6709
|
-
return freshScanner.getConversation(
|
|
6853
|
+
return freshScanner.getConversation(lookupId);
|
|
6710
6854
|
}
|
|
6711
6855
|
// True when the JSONL on disk is meaningfully newer than the scanned
|
|
6712
6856
|
// snapshot's last-activity timestamp — i.e. the file grew after the scan.
|
|
@@ -6787,7 +6931,9 @@ var StreamerServer = class {
|
|
|
6787
6931
|
res.end();
|
|
6788
6932
|
return;
|
|
6789
6933
|
}
|
|
6790
|
-
const filtered = conversation.messages
|
|
6934
|
+
const filtered = conversation.messages.filter(
|
|
6935
|
+
(m) => !(m.role === "user" && typeof m.text === "string" && isCodexInjectedContext(m.text))
|
|
6936
|
+
);
|
|
6791
6937
|
const total = filtered.length;
|
|
6792
6938
|
const hasAnchor = url.searchParams.has("anchor_index");
|
|
6793
6939
|
const hasAfter = url.searchParams.has("after_index");
|
|
@@ -6845,8 +6991,7 @@ var StreamerServer = class {
|
|
|
6845
6991
|
})
|
|
6846
6992
|
);
|
|
6847
6993
|
}
|
|
6848
|
-
const
|
|
6849
|
-
const page = indexWindow ?? (scanLimit > 0 && pagedScanner && typeof pagedScanner.getConversationPage === "function" ? await pagedScanner.getConversationPage(id, { beforeIndex, limit: scanLimit }) : null);
|
|
6994
|
+
const page = indexWindow;
|
|
6850
6995
|
if (indexWindow) indexTotal = indexWindow.total;
|
|
6851
6996
|
const start = page?.fromIndex ?? windowStart;
|
|
6852
6997
|
slice = page?.messages ?? filtered.slice(start, beforeIndex);
|
|
@@ -7654,10 +7799,7 @@ var StreamerServer = class {
|
|
|
7654
7799
|
try {
|
|
7655
7800
|
const existing = readFileSync6(resolvedFilePath, "utf8").split("\n").filter(Boolean);
|
|
7656
7801
|
if (existing.length > 0) {
|
|
7657
|
-
this.
|
|
7658
|
-
for (const line of existing) {
|
|
7659
|
-
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
7660
|
-
}
|
|
7802
|
+
this.broadcastConversationLines(sessionId, existing);
|
|
7661
7803
|
}
|
|
7662
7804
|
} catch {
|
|
7663
7805
|
}
|
|
@@ -7758,10 +7900,7 @@ var StreamerServer = class {
|
|
|
7758
7900
|
try {
|
|
7759
7901
|
const existing = readFileSync6(candidatePath, "utf8").split("\n").filter(Boolean);
|
|
7760
7902
|
if (existing.length > 0) {
|
|
7761
|
-
this.
|
|
7762
|
-
for (const line of existing) {
|
|
7763
|
-
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
7764
|
-
}
|
|
7903
|
+
this.broadcastConversationLines(sessionId, existing);
|
|
7765
7904
|
}
|
|
7766
7905
|
} catch {
|
|
7767
7906
|
}
|