@ccpocket/bridge 1.86.1 → 1.87.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.
@@ -145,6 +145,7 @@ const HEAD_BYTES = 16384; // 16KB — covers first user entry + metadata
145
145
  const TAIL_BYTES = 8192; // 8KB — covers last entries for modified/lastPrompt
146
146
  const CODEX_HEAD_BYTES = 131072; // 128KB — Codex turn_context can be large
147
147
  const CODEX_TAIL_BYTES = 16384;
148
+ const CODEX_MAX_TAIL_BYTES = 4 * 1024 * 1024;
148
149
  /**
149
150
  * Run async tasks with a concurrency limit.
150
151
  * Returns results in the same order as the input tasks.
@@ -1165,12 +1166,28 @@ function parseCodexSessionJsonl(raw, fallbackSessionId) {
1165
1166
  }
1166
1167
  if (entry.type === "response_item") {
1167
1168
  const payload = entry.payload;
1168
- if (!payload || payload.type !== "message" || payload.role !== "assistant") {
1169
+ if (!payload || payload.type !== "message") {
1169
1170
  continue;
1170
1171
  }
1171
1172
  const content = payload.content;
1172
1173
  if (!Array.isArray(content))
1173
1174
  continue;
1175
+ if (payload.role === "user") {
1176
+ const text = content
1177
+ .filter((item) => item.type === "input_text" && typeof item.text === "string")
1178
+ .map((item) => item.text)
1179
+ .join("\n")
1180
+ .trim();
1181
+ if (text && !isCodexInjectedUserContext(text)) {
1182
+ hasMessages = true;
1183
+ if (!firstPrompt)
1184
+ firstPrompt = text;
1185
+ lastPrompt = text;
1186
+ }
1187
+ continue;
1188
+ }
1189
+ if (payload.role !== "assistant")
1190
+ continue;
1174
1191
  const text = content
1175
1192
  .filter((item) => item.type === "output_text" && typeof item.text === "string")
1176
1193
  .map((item) => item.text)
@@ -1228,6 +1245,39 @@ function parseCodexSessionJsonl(raw, fallbackSessionId) {
1228
1245
  codexRepositoryUrls.set(entry, repositoryUrl);
1229
1246
  return { threadId, entry };
1230
1247
  }
1248
+ function hasCodexUserMessage(raw) {
1249
+ for (const line of raw.split("\n")) {
1250
+ if (!line.trim())
1251
+ continue;
1252
+ let entry;
1253
+ try {
1254
+ entry = JSON.parse(line);
1255
+ }
1256
+ catch {
1257
+ continue;
1258
+ }
1259
+ const payload = entry.payload;
1260
+ if (entry.type === "event_msg"
1261
+ && payload?.type === "user_message"
1262
+ && typeof payload.message === "string"
1263
+ && !isCodexInjectedUserContext(payload.message)) {
1264
+ return true;
1265
+ }
1266
+ if (entry.type === "response_item"
1267
+ && payload?.type === "message"
1268
+ && payload.role === "user"
1269
+ && Array.isArray(payload.content)) {
1270
+ const text = payload.content
1271
+ .filter((item) => item.type === "input_text" && typeof item.text === "string")
1272
+ .map((item) => item.text)
1273
+ .join("\n")
1274
+ .trim();
1275
+ if (text && !isCodexInjectedUserContext(text))
1276
+ return true;
1277
+ }
1278
+ }
1279
+ return false;
1280
+ }
1231
1281
  /**
1232
1282
  * Fast parse a Codex JSONL file for recent-session list metadata.
1233
1283
  * The first chunk contains session_meta / first prompt; the tail chunk contains
@@ -1253,13 +1303,21 @@ async function parseCodexSessionJsonlFast(filePath, fallbackSessionId) {
1253
1303
  }
1254
1304
  const headBuf = Buffer.alloc(CODEX_HEAD_BYTES);
1255
1305
  await fh.read(headBuf, 0, CODEX_HEAD_BYTES, 0);
1256
- const tailBuf = Buffer.alloc(CODEX_TAIL_BYTES);
1257
- await fh.read(tailBuf, 0, CODEX_TAIL_BYTES, fileSize - CODEX_TAIL_BYTES);
1258
- const tailRaw = tailBuf.toString("utf-8");
1259
- const firstNewline = tailRaw.indexOf("\n");
1260
- const cleanTail = firstNewline >= 0 ? tailRaw.slice(firstNewline + 1) : "";
1261
- const partialRaw = `${headBuf.toString("utf-8")}\n${cleanTail}`;
1262
- return parseCodexSessionJsonl(partialRaw, fallbackSessionId);
1306
+ let tailBytes = CODEX_TAIL_BYTES;
1307
+ while (true) {
1308
+ const tailBuf = Buffer.alloc(tailBytes);
1309
+ await fh.read(tailBuf, 0, tailBytes, fileSize - tailBytes);
1310
+ const tailRaw = tailBuf.toString("utf-8");
1311
+ const firstNewline = tailRaw.indexOf("\n");
1312
+ const cleanTail = firstNewline >= 0 ? tailRaw.slice(firstNewline + 1) : "";
1313
+ const partialRaw = `${headBuf.toString("utf-8")}\n${cleanTail}`;
1314
+ const parsed = parseCodexSessionJsonl(partialRaw, fallbackSessionId);
1315
+ if (hasCodexUserMessage(cleanTail)
1316
+ || tailBytes >= Math.min(fileSize, CODEX_MAX_TAIL_BYTES)) {
1317
+ return parsed;
1318
+ }
1319
+ tailBytes = Math.min(tailBytes * 2, fileSize, CODEX_MAX_TAIL_BYTES);
1320
+ }
1263
1321
  }
1264
1322
  finally {
1265
1323
  await fh.close();
@@ -1413,8 +1471,13 @@ export async function loadCodexSessionNames() {
1413
1471
  continue;
1414
1472
  try {
1415
1473
  const entry = JSON.parse(line);
1416
- if (entry.id && entry.thread_name) {
1417
- names.set(entry.id, entry.thread_name);
1474
+ if (entry.id && typeof entry.thread_name === "string") {
1475
+ if (entry.thread_name) {
1476
+ names.set(entry.id, entry.thread_name);
1477
+ }
1478
+ else {
1479
+ names.delete(entry.id);
1480
+ }
1418
1481
  }
1419
1482
  }
1420
1483
  catch {
@@ -1583,8 +1646,10 @@ function matchingCodexThreadIdFromFilePath(filePath, wantedThreadIds) {
1583
1646
  if (wantedThreadIds.has(fallbackSessionId))
1584
1647
  return fallbackSessionId;
1585
1648
  for (const threadId of wantedThreadIds) {
1586
- if (fallbackSessionId.endsWith(`-${threadId}`))
1649
+ if (fallbackSessionId.endsWith(`-${threadId}`)
1650
+ || fallbackSessionId.includes(`-${threadId}_`)) {
1587
1651
  return threadId;
1652
+ }
1588
1653
  }
1589
1654
  return null;
1590
1655
  }
@@ -1594,25 +1659,30 @@ export async function getCodexSessionIndexMetadata(threadIds) {
1594
1659
  if (wantedThreadIds.size === 0)
1595
1660
  return result;
1596
1661
  const files = await listCodexSessionFiles();
1597
- const targets = [];
1598
- const matchedThreadIds = new Set();
1599
- for (const filePath of files) {
1600
- const threadId = matchingCodexThreadIdFromFilePath(filePath, wantedThreadIds);
1601
- if (!threadId || matchedThreadIds.has(threadId))
1602
- continue;
1603
- targets.push(filePath);
1604
- matchedThreadIds.add(threadId);
1605
- if (matchedThreadIds.size === wantedThreadIds.size)
1606
- break;
1607
- }
1662
+ const targets = files.filter((filePath) => matchingCodexThreadIdFromFilePath(filePath, wantedThreadIds) !== null);
1608
1663
  const parsedResults = await parallelMap(targets, PARALLEL_FILE_READ_LIMIT, async (filePath) => {
1609
1664
  const fallbackSessionId = basename(filePath, ".jsonl");
1610
- return parseCodexSessionJsonlFast(filePath, fallbackSessionId);
1611
- });
1612
- for (const parsed of parsedResults) {
1665
+ const parsed = await parseCodexSessionJsonlFast(filePath, fallbackSessionId);
1613
1666
  if (!parsed || !wantedThreadIds.has(parsed.threadId))
1667
+ return null;
1668
+ try {
1669
+ return { parsed, modified: (await stat(filePath)).mtimeMs };
1670
+ }
1671
+ catch {
1672
+ return null;
1673
+ }
1674
+ });
1675
+ const newestByThread = new Map();
1676
+ for (const candidate of parsedResults) {
1677
+ if (!candidate)
1614
1678
  continue;
1615
- result.set(parsed.threadId, {
1679
+ const existing = newestByThread.get(candidate.parsed.threadId);
1680
+ if (!existing || candidate.modified > existing.modified) {
1681
+ newestByThread.set(candidate.parsed.threadId, candidate);
1682
+ }
1683
+ }
1684
+ for (const [threadId, { parsed }] of newestByThread) {
1685
+ result.set(threadId, {
1616
1686
  ...(parsed.entry.codexSettings
1617
1687
  ? { codexSettings: parsed.entry.codexSettings }
1618
1688
  : {}),
@@ -2203,12 +2273,50 @@ async function findSessionJsonlPath(sessionId) {
2203
2273
  }
2204
2274
  async function findCodexSessionJsonlPath(threadId) {
2205
2275
  const files = await listCodexSessionFiles();
2206
- for (const filePath of files) {
2276
+ const candidates = files.filter((filePath) => {
2207
2277
  const fallbackSessionId = basename(filePath, ".jsonl");
2208
- if (fallbackSessionId === threadId ||
2209
- fallbackSessionId.endsWith(`-${threadId}`)) {
2210
- return filePath;
2278
+ return fallbackSessionId === threadId ||
2279
+ fallbackSessionId.endsWith(`-${threadId}`) ||
2280
+ fallbackSessionId.includes(`-${threadId}_`);
2281
+ });
2282
+ const matches = await parallelMap(candidates, PARALLEL_FILE_READ_LIMIT, async (filePath) => {
2283
+ let file;
2284
+ try {
2285
+ file = await open(filePath, "r");
2286
+ const metadata = await file.stat();
2287
+ const buffer = Buffer.alloc(Math.min(metadata.size, CODEX_HEAD_BYTES));
2288
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
2289
+ // History/image lookup must not depend on list metadata (cwd or text).
2290
+ // Older rollouts have no session_meta, so retain the filename fallback.
2291
+ for (const line of buffer.subarray(0, bytesRead).toString("utf-8").split("\n")) {
2292
+ try {
2293
+ const entry = JSON.parse(line);
2294
+ if (entry?.type === "session_meta" && typeof entry.payload?.id === "string") {
2295
+ if (entry.payload.id !== threadId)
2296
+ return null;
2297
+ break;
2298
+ }
2299
+ }
2300
+ catch {
2301
+ // Ignore partial or malformed header records.
2302
+ }
2303
+ }
2304
+ return { filePath, modified: metadata.mtimeMs };
2305
+ }
2306
+ catch {
2307
+ return null;
2211
2308
  }
2309
+ finally {
2310
+ await file?.close();
2311
+ }
2312
+ });
2313
+ const newest = matches
2314
+ .filter((match) => match !== null)
2315
+ .sort((a, b) => b.modified - a.modified)[0];
2316
+ if (newest)
2317
+ return newest.filePath;
2318
+ for (const filePath of files) {
2319
+ const fallbackSessionId = basename(filePath, ".jsonl");
2212
2320
  let raw;
2213
2321
  try {
2214
2322
  raw = await readFile(filePath, "utf-8");