@threadbase-sh/streamer 1.29.0 → 1.29.2
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 +7239 -5574
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +248 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +248 -38
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -215,7 +215,7 @@ function verifySignature(rawBody, signature, secret) {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
function isWithinSkew(timestampHeader, skewSeconds) {
|
|
218
|
-
if (!timestampHeader) return
|
|
218
|
+
if (!timestampHeader) return false;
|
|
219
219
|
const t = Number(timestampHeader);
|
|
220
220
|
if (!Number.isFinite(t)) return false;
|
|
221
221
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -2589,7 +2589,7 @@ function isLocalRequest(remoteAddr) {
|
|
|
2589
2589
|
const addr = remoteAddr ?? "";
|
|
2590
2590
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
2591
2591
|
}
|
|
2592
|
-
var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz"]);
|
|
2592
|
+
var PUBLIC_PATHS = /* @__PURE__ */ new Set(["/healthz", "/ws"]);
|
|
2593
2593
|
var PUBLIC_POST_PATHS = /* @__PURE__ */ new Set(["/api/pair/exchange", "/api/__update"]);
|
|
2594
2594
|
var PUBLIC_POST_PREFIXES = ["/internal/sessions/"];
|
|
2595
2595
|
var authMiddleware = (deps) => async (c, next) => {
|
|
@@ -3052,14 +3052,16 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
|
|
|
3052
3052
|
const app = new import_hono10.Hono();
|
|
3053
3053
|
app.get(
|
|
3054
3054
|
"/ws",
|
|
3055
|
-
upgradeWebSocket(() => {
|
|
3055
|
+
upgradeWebSocket((c) => {
|
|
3056
|
+
const key = c.req.query("key");
|
|
3057
|
+
const preAuthed = typeof key === "string" && validateApiKey(key, deps.apiKey);
|
|
3056
3058
|
let openWs = null;
|
|
3057
3059
|
return {
|
|
3058
3060
|
onOpen(_evt, ws) {
|
|
3059
3061
|
const raw = ws.raw;
|
|
3060
3062
|
if (!raw) return;
|
|
3061
3063
|
openWs = raw;
|
|
3062
|
-
deps.handleWsOpen(raw);
|
|
3064
|
+
deps.handleWsOpen(raw, preAuthed);
|
|
3063
3065
|
},
|
|
3064
3066
|
onMessage(evt, _ws) {
|
|
3065
3067
|
if (openWs) deps.handleWsMessage(openWs, evt.data);
|
|
@@ -4964,6 +4966,13 @@ function deriveProjectChatTitle(input) {
|
|
|
4964
4966
|
return `Untitled \xB7 ${input.id.slice(0, 8)}`;
|
|
4965
4967
|
}
|
|
4966
4968
|
|
|
4969
|
+
// src/services/questions/permissionAnswerKeys.ts
|
|
4970
|
+
var ANSWER_KEYS_ALLOWLIST = /^(?:\r|[yn]\r|\x03|\d+\r)$/;
|
|
4971
|
+
function sanitizeAnswerKeys(keys) {
|
|
4972
|
+
if (keys === void 0) return void 0;
|
|
4973
|
+
return ANSWER_KEYS_ALLOWLIST.test(keys) ? keys : void 0;
|
|
4974
|
+
}
|
|
4975
|
+
|
|
4967
4976
|
// src/services/questions/detectAskUserQuestion.ts
|
|
4968
4977
|
function normalizeContent2(raw) {
|
|
4969
4978
|
if (Array.isArray(raw)) return raw;
|
|
@@ -5328,6 +5337,89 @@ function sanitizeFilename(name) {
|
|
|
5328
5337
|
return cleaned;
|
|
5329
5338
|
}
|
|
5330
5339
|
|
|
5340
|
+
// src/utils/codexConversationLine.ts
|
|
5341
|
+
function extractCodexText(content) {
|
|
5342
|
+
if (typeof content === "string") return content.trim();
|
|
5343
|
+
if (!Array.isArray(content)) return "";
|
|
5344
|
+
return content.map((item) => {
|
|
5345
|
+
if (typeof item === "string") return item;
|
|
5346
|
+
const block = item;
|
|
5347
|
+
const t = block?.type;
|
|
5348
|
+
if ((t === "input_text" || t === "output_text" || t === "text") && typeof block.text === "string") {
|
|
5349
|
+
return block.text;
|
|
5350
|
+
}
|
|
5351
|
+
return "";
|
|
5352
|
+
}).filter(Boolean).join("").trim();
|
|
5353
|
+
}
|
|
5354
|
+
function normalizeCodexLineToClaudeShape(line) {
|
|
5355
|
+
let entry;
|
|
5356
|
+
try {
|
|
5357
|
+
entry = JSON.parse(line);
|
|
5358
|
+
} catch {
|
|
5359
|
+
return null;
|
|
5360
|
+
}
|
|
5361
|
+
if (entry.type !== "response_item") return null;
|
|
5362
|
+
const payload = entry.payload;
|
|
5363
|
+
if (payload?.type !== "message") return null;
|
|
5364
|
+
const role = payload.role;
|
|
5365
|
+
if (role !== "user" && role !== "assistant") return null;
|
|
5366
|
+
const text = extractCodexText(payload.content);
|
|
5367
|
+
if (!text) return null;
|
|
5368
|
+
if (role === "user" && isCodexInjectedContext(text)) return null;
|
|
5369
|
+
const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : (/* @__PURE__ */ new Date()).toISOString();
|
|
5370
|
+
const uuid = typeof payload.id === "string" && payload.id.length > 0 ? payload.id : `codex-${role}-${timestamp}-${hashPrefix(text)}`;
|
|
5371
|
+
return JSON.stringify({
|
|
5372
|
+
type: role,
|
|
5373
|
+
uuid,
|
|
5374
|
+
timestamp,
|
|
5375
|
+
message: {
|
|
5376
|
+
role,
|
|
5377
|
+
content: [{ type: "text", text }]
|
|
5378
|
+
}
|
|
5379
|
+
});
|
|
5380
|
+
}
|
|
5381
|
+
function isCodexRolloutLine(line) {
|
|
5382
|
+
try {
|
|
5383
|
+
const entry = JSON.parse(line);
|
|
5384
|
+
return entry.type === "response_item" || entry.type === "event_msg" || entry.type === "session_meta" || entry.type === "turn_context";
|
|
5385
|
+
} catch {
|
|
5386
|
+
return false;
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
function toClientConversationLines(lines) {
|
|
5390
|
+
if (lines.length === 0) return lines;
|
|
5391
|
+
const codex = lines.some(isCodexRolloutLine);
|
|
5392
|
+
if (!codex) return lines;
|
|
5393
|
+
const out = [];
|
|
5394
|
+
for (const line of lines) {
|
|
5395
|
+
const normalized = normalizeCodexLineToClaudeShape(line);
|
|
5396
|
+
if (normalized) out.push(normalized);
|
|
5397
|
+
}
|
|
5398
|
+
return out;
|
|
5399
|
+
}
|
|
5400
|
+
function isCodexInjectedContext(text) {
|
|
5401
|
+
if (text.startsWith("# AGENTS.md") || text.includes("<INSTRUCTIONS>")) return true;
|
|
5402
|
+
if (text.startsWith("<permissions instructions>") || text.includes("Filesystem sandboxing defines")) {
|
|
5403
|
+
return true;
|
|
5404
|
+
}
|
|
5405
|
+
if (text.includes("limit the options to at most 3")) return true;
|
|
5406
|
+
if (text.includes("You are working within the project boundary:")) return true;
|
|
5407
|
+
if (text.includes(
|
|
5408
|
+
"Do not read, write, or execute commands that access files or directories outside this boundary"
|
|
5409
|
+
)) {
|
|
5410
|
+
return true;
|
|
5411
|
+
}
|
|
5412
|
+
return false;
|
|
5413
|
+
}
|
|
5414
|
+
function hashPrefix(text) {
|
|
5415
|
+
let h = 0;
|
|
5416
|
+
const sample = text.slice(0, 64);
|
|
5417
|
+
for (let i = 0; i < sample.length; i++) {
|
|
5418
|
+
h = h * 31 + sample.charCodeAt(i) | 0;
|
|
5419
|
+
}
|
|
5420
|
+
return Math.abs(h).toString(36);
|
|
5421
|
+
}
|
|
5422
|
+
|
|
5331
5423
|
// src/utils/conversationEtag.ts
|
|
5332
5424
|
var import_node_crypto3 = require("crypto");
|
|
5333
5425
|
function computeConversationEtag({
|
|
@@ -5485,6 +5577,8 @@ var WSHub = class {
|
|
|
5485
5577
|
var BROWSE_SYSTEM_PROMPT = (browseRoot) => `You are working within the project boundary: ${browseRoot}. Do not read, write, or execute commands that access files or directories outside this boundary.`;
|
|
5486
5578
|
var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
|
|
5487
5579
|
var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
|
|
5580
|
+
var DEFAULT_WS_AUTH_TIMEOUT_MS = 5e3;
|
|
5581
|
+
var WS_CLOSE_UNAUTHORIZED = 4401;
|
|
5488
5582
|
var REFRESH_TTL_MS = 2e3;
|
|
5489
5583
|
var START_READY_TIMEOUT_MS = 15e3;
|
|
5490
5584
|
function parseIncludeAgentsEnv(raw) {
|
|
@@ -5575,6 +5669,14 @@ var StreamerServer = class {
|
|
|
5575
5669
|
clientIdToWs = /* @__PURE__ */ new Map();
|
|
5576
5670
|
// Reverse map for cleanup on close
|
|
5577
5671
|
wsToClientId = /* @__PURE__ */ new Map();
|
|
5672
|
+
// M1 — WS auth. Sockets that have authenticated (via ?key= at upgrade OR a
|
|
5673
|
+
// { type: "auth", token } first message). Only authed sockets are added to
|
|
5674
|
+
// the hub and receive broadcasts.
|
|
5675
|
+
// Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
|
|
5676
|
+
wsAuthed = /* @__PURE__ */ new Set();
|
|
5677
|
+
// Keyless sockets awaiting their first-message auth handshake → close timer.
|
|
5678
|
+
wsAuthPending = /* @__PURE__ */ new Map();
|
|
5679
|
+
wsAuthTimeoutMs;
|
|
5578
5680
|
cache = null;
|
|
5579
5681
|
projectsRepo = null;
|
|
5580
5682
|
conversationsRepo = null;
|
|
@@ -5614,6 +5716,7 @@ var StreamerServer = class {
|
|
|
5614
5716
|
this.scanProfiles = config.scanProfiles;
|
|
5615
5717
|
this.codexRoots = config.codexRoots ?? [(0, import_path13.join)((0, import_os6.homedir)(), ".codex", "sessions")];
|
|
5616
5718
|
this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
|
|
5719
|
+
this.wsAuthTimeoutMs = config.wsAuthTimeoutMs ?? DEFAULT_WS_AUTH_TIMEOUT_MS;
|
|
5617
5720
|
this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
5618
5721
|
this.defaultPermissionMode = config.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
|
|
5619
5722
|
this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path13.join)((0, import_os6.homedir)(), ".threadbase", "cache");
|
|
@@ -5712,15 +5815,7 @@ var StreamerServer = class {
|
|
|
5712
5815
|
if (broadcast) this.wsHub.broadcast(m);
|
|
5713
5816
|
}
|
|
5714
5817
|
const seqs = this.pendingLineSeqs.get(filePath);
|
|
5715
|
-
this.
|
|
5716
|
-
type: "conversation_events",
|
|
5717
|
-
sessionId,
|
|
5718
|
-
lines,
|
|
5719
|
-
...seqs && seqs.length === lines.length ? { seqs } : {}
|
|
5720
|
-
});
|
|
5721
|
-
for (const line of lines) {
|
|
5722
|
-
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
5723
|
-
}
|
|
5818
|
+
this.broadcastConversationLines(sessionId, lines, seqs);
|
|
5724
5819
|
break;
|
|
5725
5820
|
}
|
|
5726
5821
|
}
|
|
@@ -5879,17 +5974,39 @@ var StreamerServer = class {
|
|
|
5879
5974
|
handlePairExchange: (req, res) => this.handlePairExchange(req, res),
|
|
5880
5975
|
handleBrowse: (url, res) => this.handleBrowse(url, res),
|
|
5881
5976
|
handleMkdir: (req, res) => this.handleMkdir(req, res),
|
|
5882
|
-
handleWsOpen: (ws) => {
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
if (this.cacheReady) {
|
|
5887
|
-
ws.send(JSON.stringify({ type: "cache_ready" }));
|
|
5977
|
+
handleWsOpen: (ws, preAuthed) => {
|
|
5978
|
+
if (preAuthed) {
|
|
5979
|
+
this.completeWsAuth(ws);
|
|
5980
|
+
return;
|
|
5888
5981
|
}
|
|
5982
|
+
const timer = setTimeout(() => {
|
|
5983
|
+
this.wsAuthPending.delete(ws);
|
|
5984
|
+
try {
|
|
5985
|
+
ws.close(WS_CLOSE_UNAUTHORIZED, "auth timeout");
|
|
5986
|
+
} catch {
|
|
5987
|
+
}
|
|
5988
|
+
}, this.wsAuthTimeoutMs);
|
|
5989
|
+
this.wsAuthPending.set(ws, timer);
|
|
5889
5990
|
},
|
|
5890
5991
|
handleWsMessage: async (ws, raw) => {
|
|
5891
5992
|
try {
|
|
5892
5993
|
const msg = JSON.parse(String(raw));
|
|
5994
|
+
if (!this.wsAuthed.has(ws)) {
|
|
5995
|
+
if (msg.type === "auth" && typeof msg.token === "string") {
|
|
5996
|
+
const t = this.wsAuthPending.get(ws);
|
|
5997
|
+
if (t) clearTimeout(t);
|
|
5998
|
+
this.wsAuthPending.delete(ws);
|
|
5999
|
+
if (validateApiKey(msg.token, this.apiKey)) {
|
|
6000
|
+
this.completeWsAuth(ws);
|
|
6001
|
+
} else {
|
|
6002
|
+
try {
|
|
6003
|
+
ws.close(WS_CLOSE_UNAUTHORIZED, "unauthorized");
|
|
6004
|
+
} catch {
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
6007
|
+
}
|
|
6008
|
+
return;
|
|
6009
|
+
}
|
|
5893
6010
|
if (msg.type === "register" && typeof msg.clientId === "string") {
|
|
5894
6011
|
const oldClientId = this.wsToClientId.get(ws);
|
|
5895
6012
|
if (oldClientId) this.clientIdToWs.delete(oldClientId);
|
|
@@ -5904,12 +6021,20 @@ var StreamerServer = class {
|
|
|
5904
6021
|
}
|
|
5905
6022
|
}
|
|
5906
6023
|
if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
|
|
5907
|
-
this.
|
|
6024
|
+
if (this.sessionSubscribers.get(msg.sessionId)?.has(ws)) {
|
|
6025
|
+
this.startGraceTimer(msg.sessionId, 0);
|
|
6026
|
+
}
|
|
5908
6027
|
}
|
|
5909
6028
|
} catch {
|
|
5910
6029
|
}
|
|
5911
6030
|
},
|
|
5912
6031
|
handleWsClose: (ws) => {
|
|
6032
|
+
const pendingTimer = this.wsAuthPending.get(ws);
|
|
6033
|
+
if (pendingTimer) {
|
|
6034
|
+
clearTimeout(pendingTimer);
|
|
6035
|
+
this.wsAuthPending.delete(ws);
|
|
6036
|
+
}
|
|
6037
|
+
this.wsAuthed.delete(ws);
|
|
5913
6038
|
const clientId = this.wsToClientId.get(ws);
|
|
5914
6039
|
if (clientId) {
|
|
5915
6040
|
this.clientIdToWs.delete(clientId);
|
|
@@ -5979,6 +6104,20 @@ var StreamerServer = class {
|
|
|
5979
6104
|
this.wsHub.broadcast(payload);
|
|
5980
6105
|
}
|
|
5981
6106
|
}
|
|
6107
|
+
// M1: finalize a WebSocket auth (via ?key= at upgrade or a first-message
|
|
6108
|
+
// handshake) — register it with the hub and send the initial snapshot. Only
|
|
6109
|
+
// authed sockets reach this, so no unauthenticated client ever receives a
|
|
6110
|
+
// broadcast.
|
|
6111
|
+
// Plan: https://github.com/RonenMars/threadbase-streamer/blob/a251353bfa417bd48ce3f15086bc336a2c622629/docs/plans/2026-06-24-security-hardening.md#L40
|
|
6112
|
+
completeWsAuth(ws) {
|
|
6113
|
+
this.wsAuthed.add(ws);
|
|
6114
|
+
this.wsHub.addClient(ws);
|
|
6115
|
+
const sessions = this.sessionStore.list(this.ptyAttachedIds());
|
|
6116
|
+
ws.send(JSON.stringify({ type: "session_list", sessions }));
|
|
6117
|
+
if (this.cacheReady) {
|
|
6118
|
+
ws.send(JSON.stringify({ type: "cache_ready" }));
|
|
6119
|
+
}
|
|
6120
|
+
}
|
|
5982
6121
|
addSessionSubscriber(sessionId, ws) {
|
|
5983
6122
|
let subs = this.sessionSubscribers.get(sessionId);
|
|
5984
6123
|
if (!subs) {
|
|
@@ -6265,6 +6404,9 @@ var StreamerServer = class {
|
|
|
6265
6404
|
this.ptyManager.dispose();
|
|
6266
6405
|
this.fileWatcher.dispose();
|
|
6267
6406
|
this.wsHub.dispose();
|
|
6407
|
+
for (const timer of this.wsAuthPending.values()) clearTimeout(timer);
|
|
6408
|
+
this.wsAuthPending.clear();
|
|
6409
|
+
this.wsAuthed.clear();
|
|
6268
6410
|
this.pairTokens.dispose();
|
|
6269
6411
|
if (this.dbPool) {
|
|
6270
6412
|
await this.dbPool.end();
|
|
@@ -6701,11 +6843,62 @@ var StreamerServer = class {
|
|
|
6701
6843
|
rl.on("error", () => resolve2(null));
|
|
6702
6844
|
});
|
|
6703
6845
|
}
|
|
6846
|
+
/**
|
|
6847
|
+
* Live Codex sessions keep `SessionResponse.conversationId === managed.id`
|
|
6848
|
+
* (stable deep-link / PTY key) and store the rollout UUID separately as
|
|
6849
|
+
* `boundConversationId`. REST history is indexed under the rollout UUID, so
|
|
6850
|
+
* resolve the placeholder → bound id before looking up the scanner.
|
|
6851
|
+
*/
|
|
6852
|
+
resolveConversationLookupId(uuid) {
|
|
6853
|
+
const managed = this.sessionStore.getManaged(uuid);
|
|
6854
|
+
if (managed?.boundConversationId) return managed.boundConversationId;
|
|
6855
|
+
return uuid;
|
|
6856
|
+
}
|
|
6857
|
+
/** File path for a live managed session (placeholder id or bound Codex id). */
|
|
6858
|
+
findLiveSessionFilePath(uuid) {
|
|
6859
|
+
const direct = this.sessionFileMap.get(uuid);
|
|
6860
|
+
if (direct) return direct;
|
|
6861
|
+
for (const s of this.sessionStore.listManaged()) {
|
|
6862
|
+
if (s.boundConversationId === uuid) {
|
|
6863
|
+
return this.sessionFileMap.get(s.id) ?? null;
|
|
6864
|
+
}
|
|
6865
|
+
}
|
|
6866
|
+
return null;
|
|
6867
|
+
}
|
|
6868
|
+
/** True when a conversation UUID is the bound rollout of a live PTY session. */
|
|
6869
|
+
isBoundConversationLive(boundId) {
|
|
6870
|
+
for (const s of this.sessionStore.listManaged()) {
|
|
6871
|
+
if (s.boundConversationId === boundId && this.ptyManager.hasSession(s.id)) {
|
|
6872
|
+
return true;
|
|
6873
|
+
}
|
|
6874
|
+
}
|
|
6875
|
+
return false;
|
|
6876
|
+
}
|
|
6877
|
+
/**
|
|
6878
|
+
* Broadcast conversation JSONL lines to WS clients. Codex rollout lines are
|
|
6879
|
+
* normalized to the Claude `type:user|assistant` shape mobile understands;
|
|
6880
|
+
* Claude lines pass through unchanged so seq alignment stays intact.
|
|
6881
|
+
*/
|
|
6882
|
+
broadcastConversationLines(sessionId, lines, seqs) {
|
|
6883
|
+
const clientLines = toClientConversationLines(lines);
|
|
6884
|
+
if (clientLines.length === 0) return;
|
|
6885
|
+
const seqsOk = !!seqs && seqs.length === lines.length && clientLines.length === lines.length;
|
|
6886
|
+
this.wsHub.broadcast({
|
|
6887
|
+
type: "conversation_events",
|
|
6888
|
+
sessionId,
|
|
6889
|
+
lines: clientLines,
|
|
6890
|
+
...seqsOk ? { seqs } : {}
|
|
6891
|
+
});
|
|
6892
|
+
for (const line of clientLines) {
|
|
6893
|
+
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
6894
|
+
}
|
|
6895
|
+
}
|
|
6704
6896
|
async findConversationByUuid(uuid) {
|
|
6897
|
+
const lookupId = this.resolveConversationLookupId(uuid);
|
|
6705
6898
|
if (!this.scannerReady && !this.scanProfiles) {
|
|
6706
|
-
const filePath2 = this.findJsonlPath(uuid);
|
|
6899
|
+
const filePath2 = this.findJsonlPath(lookupId) ?? this.findLiveSessionFilePath(uuid) ?? this.findLiveSessionFilePath(lookupId);
|
|
6707
6900
|
if (filePath2) {
|
|
6708
|
-
const account = this.cache?.getMetaById(
|
|
6901
|
+
const account = this.cache?.getMetaById(lookupId)?.account ?? void 0;
|
|
6709
6902
|
const coldScanner = this.scanner ?? this.newScanner();
|
|
6710
6903
|
const page = await coldScanner.parseSingleFilePage(filePath2, account, {
|
|
6711
6904
|
limit: Number.MAX_SAFE_INTEGER
|
|
@@ -6715,9 +6908,27 @@ var StreamerServer = class {
|
|
|
6715
6908
|
return null;
|
|
6716
6909
|
}
|
|
6717
6910
|
const scanner = await this.getScanner(true);
|
|
6718
|
-
const fromIndex = await scanner.getConversation(
|
|
6911
|
+
const fromIndex = await scanner.getConversation(lookupId);
|
|
6719
6912
|
if (fromIndex) {
|
|
6720
|
-
if (this.ptyManager.hasSession(uuid)) {
|
|
6913
|
+
if (this.ptyManager.hasSession(uuid) || this.ptyManager.hasSession(lookupId) || this.isBoundConversationLive(lookupId)) {
|
|
6914
|
+
const livePath = this.findLiveSessionFilePath(uuid) ?? this.findLiveSessionFilePath(lookupId) ?? fromIndex.filePath ?? null;
|
|
6915
|
+
const isCodexLive = this.isBoundConversationLive(lookupId) || this.sessionStore.getManaged(uuid)?.provider === CODEX_CLI_PROVIDER;
|
|
6916
|
+
if (isCodexLive && livePath) {
|
|
6917
|
+
try {
|
|
6918
|
+
const account = this.cache?.getMetaById(lookupId)?.account ?? fromIndex.account ?? void 0;
|
|
6919
|
+
const page = await scanner.parseSingleFilePage(livePath, account, {
|
|
6920
|
+
limit: Number.MAX_SAFE_INTEGER
|
|
6921
|
+
});
|
|
6922
|
+
if (page?.conversation) return page.conversation;
|
|
6923
|
+
} catch (err) {
|
|
6924
|
+
this.log.warn("codex.live_reparse_failed", {
|
|
6925
|
+
event: "codex.live_reparse_failed",
|
|
6926
|
+
conversationId: lookupId,
|
|
6927
|
+
filePath: livePath,
|
|
6928
|
+
err
|
|
6929
|
+
});
|
|
6930
|
+
}
|
|
6931
|
+
}
|
|
6721
6932
|
return fromIndex;
|
|
6722
6933
|
}
|
|
6723
6934
|
if (fromIndex.filePath && this.isConversationSnapshotStale(fromIndex)) {
|
|
@@ -6737,12 +6948,12 @@ var StreamerServer = class {
|
|
|
6737
6948
|
return fromIndex;
|
|
6738
6949
|
}
|
|
6739
6950
|
if (this.scanProfiles) return null;
|
|
6740
|
-
const filePath = this.findJsonlPath(uuid);
|
|
6951
|
+
const filePath = this.findJsonlPath(lookupId) ?? this.findLiveSessionFilePath(uuid) ?? this.findLiveSessionFilePath(lookupId);
|
|
6741
6952
|
if (!filePath) return null;
|
|
6742
6953
|
this.scanner = null;
|
|
6743
6954
|
this.scannerReady = null;
|
|
6744
6955
|
const freshScanner = await this.getScanner();
|
|
6745
|
-
return freshScanner.getConversation(
|
|
6956
|
+
return freshScanner.getConversation(lookupId);
|
|
6746
6957
|
}
|
|
6747
6958
|
// True when the JSONL on disk is meaningfully newer than the scanned
|
|
6748
6959
|
// snapshot's last-activity timestamp — i.e. the file grew after the scan.
|
|
@@ -6823,7 +7034,9 @@ var StreamerServer = class {
|
|
|
6823
7034
|
res.end();
|
|
6824
7035
|
return;
|
|
6825
7036
|
}
|
|
6826
|
-
const filtered = conversation.messages
|
|
7037
|
+
const filtered = conversation.messages.filter(
|
|
7038
|
+
(m) => !(m.role === "user" && typeof m.text === "string" && isCodexInjectedContext(m.text))
|
|
7039
|
+
);
|
|
6827
7040
|
const total = filtered.length;
|
|
6828
7041
|
const hasAnchor = url.searchParams.has("anchor_index");
|
|
6829
7042
|
const hasAfter = url.searchParams.has("after_index");
|
|
@@ -6881,8 +7094,7 @@ var StreamerServer = class {
|
|
|
6881
7094
|
})
|
|
6882
7095
|
);
|
|
6883
7096
|
}
|
|
6884
|
-
const
|
|
6885
|
-
const page = indexWindow ?? (scanLimit > 0 && pagedScanner && typeof pagedScanner.getConversationPage === "function" ? await pagedScanner.getConversationPage(id, { beforeIndex, limit: scanLimit }) : null);
|
|
7097
|
+
const page = indexWindow;
|
|
6886
7098
|
if (indexWindow) indexTotal = indexWindow.total;
|
|
6887
7099
|
const start = page?.fromIndex ?? windowStart;
|
|
6888
7100
|
slice = page?.messages ?? filtered.slice(start, beforeIndex);
|
|
@@ -7321,12 +7533,16 @@ var StreamerServer = class {
|
|
|
7321
7533
|
return;
|
|
7322
7534
|
}
|
|
7323
7535
|
this.pendingPermission.set(sessionId, gate);
|
|
7536
|
+
const safeOptions = gate.options.map((o) => {
|
|
7537
|
+
const answerKeys = sanitizeAnswerKeys(o.answerKeys);
|
|
7538
|
+
return answerKeys === void 0 ? { index: o.index, label: o.label } : { ...o, answerKeys };
|
|
7539
|
+
});
|
|
7324
7540
|
this.wsHub.broadcast({
|
|
7325
7541
|
type: "permission",
|
|
7326
7542
|
sessionId,
|
|
7327
7543
|
...gate.prompt ? { prompt: gate.prompt } : {},
|
|
7328
7544
|
...gate.detail ? { detail: gate.detail } : {},
|
|
7329
|
-
options:
|
|
7545
|
+
options: safeOptions,
|
|
7330
7546
|
...gate.cursor !== void 0 ? { cursor: gate.cursor } : {}
|
|
7331
7547
|
});
|
|
7332
7548
|
}
|
|
@@ -7690,10 +7906,7 @@ var StreamerServer = class {
|
|
|
7690
7906
|
try {
|
|
7691
7907
|
const existing = (0, import_fs12.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
|
|
7692
7908
|
if (existing.length > 0) {
|
|
7693
|
-
this.
|
|
7694
|
-
for (const line of existing) {
|
|
7695
|
-
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
7696
|
-
}
|
|
7909
|
+
this.broadcastConversationLines(sessionId, existing);
|
|
7697
7910
|
}
|
|
7698
7911
|
} catch {
|
|
7699
7912
|
}
|
|
@@ -7794,10 +8007,7 @@ var StreamerServer = class {
|
|
|
7794
8007
|
try {
|
|
7795
8008
|
const existing = (0, import_fs12.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
|
|
7796
8009
|
if (existing.length > 0) {
|
|
7797
|
-
this.
|
|
7798
|
-
for (const line of existing) {
|
|
7799
|
-
this.wsHub.broadcast({ type: "conversation_event", sessionId, line });
|
|
7800
|
-
}
|
|
8010
|
+
this.broadcastConversationLines(sessionId, existing);
|
|
7801
8011
|
}
|
|
7802
8012
|
} catch {
|
|
7803
8013
|
}
|