@cocorograph/hub-agent 0.8.2 → 0.8.3
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/package.json +1 -1
- package/src/usage.mjs +77 -45
package/package.json
CHANGED
package/src/usage.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { promises as fs } from "node:fs"
|
|
|
20
20
|
import { randomUUID } from "node:crypto"
|
|
21
21
|
import os from "node:os"
|
|
22
22
|
import path from "node:path"
|
|
23
|
+
import { StringDecoder } from "node:string_decoder"
|
|
23
24
|
|
|
24
25
|
import { encodeCwdToDirName } from "./claude-history.mjs"
|
|
25
26
|
|
|
@@ -147,41 +148,6 @@ async function readTail(fp, maxBytes = 64 * 1024) {
|
|
|
147
148
|
}
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
/**
|
|
151
|
-
* ファイル先頭だけを読む (head)。readTail の対称。rollout の session_meta ヘッダー行
|
|
152
|
-
* (先頭行) + 先頭近傍の最初の user_message (preview) を取り出すのに使う。maxBytes まで
|
|
153
|
-
* 読み、末尾の不完全な行を捨てる (最後の改行以降を落とす)。size <= maxBytes なら全体。失敗時 null。
|
|
154
|
-
*/
|
|
155
|
-
async function readHead(fp, maxBytes = 32 * 1024) {
|
|
156
|
-
let fh
|
|
157
|
-
try {
|
|
158
|
-
fh = await fs.open(fp, "r")
|
|
159
|
-
const st = await fh.stat()
|
|
160
|
-
const size = st.size
|
|
161
|
-
if (size === 0) return ""
|
|
162
|
-
const len = size > maxBytes ? maxBytes : size
|
|
163
|
-
const buf = Buffer.allocUnsafe(len)
|
|
164
|
-
// ⚠️ bytesRead で切る (allocUnsafe は未初期化。並行 truncate 等で len 未満しか読めなかった
|
|
165
|
-
// とき、残りのゴミ領域を文字列化しない。cross-review minor)。
|
|
166
|
-
const { bytesRead } = await fh.read(buf, 0, len, 0)
|
|
167
|
-
let text = buf.toString("utf-8", 0, bytesRead)
|
|
168
|
-
// EOF まで読み切っていない (maxBytes で頭だけ / 途中 truncate) 場合、末尾の不完全な行を捨てる。
|
|
169
|
-
if (bytesRead < size) {
|
|
170
|
-
const nl = text.lastIndexOf("\n")
|
|
171
|
-
text = nl >= 0 ? text.slice(0, nl) : text
|
|
172
|
-
}
|
|
173
|
-
return text
|
|
174
|
-
} catch {
|
|
175
|
-
return null
|
|
176
|
-
} finally {
|
|
177
|
-
try {
|
|
178
|
-
await fh?.close()
|
|
179
|
-
} catch {
|
|
180
|
-
/* ignore */
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
151
|
/** P5(perf): latestJsonlContext の結果を {mtimeMs,size} キーでメモ化し再パースを避ける。 */
|
|
186
152
|
const _jsonlCtxMemo = new Map() // fp → { mtimeMs, size, result }
|
|
187
153
|
|
|
@@ -1513,21 +1479,37 @@ function threadIdFromRolloutFilename(fp) {
|
|
|
1513
1479
|
return m ? m[1] : ""
|
|
1514
1480
|
}
|
|
1515
1481
|
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1482
|
+
const CODEX_ROLLOUT_HEAD_CHUNK_BYTES = 32 * 1024
|
|
1483
|
+
const CODEX_ROLLOUT_PREVIEW_MAX_BYTES = 1024 * 1024
|
|
1484
|
+
|
|
1485
|
+
/**
|
|
1486
|
+
* rollout の先頭から一覧用メタ (thread_id / cwd / preview) を取り出す。
|
|
1487
|
+
*
|
|
1488
|
+
* 現行 Codex は最初の user_message より前に system / developer / AGENTS コンテキストを
|
|
1489
|
+
* response_item として書くため、user_message が 200KB 以降に現れることがある。固定 32KB の
|
|
1490
|
+
* head 読みでは preview が常に空になったため、32KB 単位で最大 1MB まで段階的に読み進める。
|
|
1491
|
+
* ファイル全体は読まず、cwd が対象外と分かった時点、または preview が見つかった時点で止める。
|
|
1492
|
+
*
|
|
1493
|
+
* @param {string} fp
|
|
1494
|
+
* @param {string} expectedCwd cwd-scoped 一覧の対象。別 cwd は session_meta 読み取り後に早期終了。
|
|
1495
|
+
* @returns {Promise<{thread_id:string,cwd:string,preview:string}|null>}
|
|
1496
|
+
*/
|
|
1497
|
+
async function parseCodexRolloutHead(fp, expectedCwd = "") {
|
|
1521
1498
|
let thread_id = threadIdFromRolloutFilename(fp)
|
|
1522
1499
|
let cwd = ""
|
|
1523
1500
|
let preview = ""
|
|
1524
|
-
|
|
1525
|
-
|
|
1501
|
+
let fh
|
|
1502
|
+
const decoder = new StringDecoder("utf8")
|
|
1503
|
+
let pending = ""
|
|
1504
|
+
let offset = 0
|
|
1505
|
+
|
|
1506
|
+
const parseLine = (line) => {
|
|
1507
|
+
if (!line) return
|
|
1526
1508
|
let o
|
|
1527
1509
|
try {
|
|
1528
1510
|
o = JSON.parse(line)
|
|
1529
1511
|
} catch {
|
|
1530
|
-
|
|
1512
|
+
return
|
|
1531
1513
|
}
|
|
1532
1514
|
if (o.type === "session_meta" && o.payload && typeof o.payload === "object") {
|
|
1533
1515
|
if (typeof o.payload.cwd === "string") cwd = o.payload.cwd
|
|
@@ -1543,8 +1525,58 @@ async function parseCodexRolloutHead(fp) {
|
|
|
1543
1525
|
) {
|
|
1544
1526
|
preview = o.payload.message.slice(0, 80)
|
|
1545
1527
|
}
|
|
1546
|
-
if (cwd && preview) break // 両方揃ったら以降は読まない (先頭近傍で足りる)。
|
|
1547
1528
|
}
|
|
1529
|
+
|
|
1530
|
+
try {
|
|
1531
|
+
fh = await fs.open(fp, "r")
|
|
1532
|
+
while (offset < CODEX_ROLLOUT_PREVIEW_MAX_BYTES) {
|
|
1533
|
+
const readSize = Math.min(
|
|
1534
|
+
CODEX_ROLLOUT_HEAD_CHUNK_BYTES,
|
|
1535
|
+
CODEX_ROLLOUT_PREVIEW_MAX_BYTES - offset,
|
|
1536
|
+
)
|
|
1537
|
+
const buf = Buffer.allocUnsafe(readSize)
|
|
1538
|
+
const { bytesRead } = await fh.read(buf, 0, readSize, offset)
|
|
1539
|
+
if (bytesRead === 0) {
|
|
1540
|
+
pending += decoder.end()
|
|
1541
|
+
if (pending) parseLine(pending)
|
|
1542
|
+
break
|
|
1543
|
+
}
|
|
1544
|
+
offset += bytesRead
|
|
1545
|
+
pending += decoder.write(buf.subarray(0, bytesRead))
|
|
1546
|
+
|
|
1547
|
+
let newline
|
|
1548
|
+
while ((newline = pending.indexOf("\n")) >= 0) {
|
|
1549
|
+
const line = pending.slice(0, newline)
|
|
1550
|
+
pending = pending.slice(newline + 1)
|
|
1551
|
+
parseLine(line)
|
|
1552
|
+
// session_meta は先頭行にある。別 workspace の候補に対して巨大な初期コンテキストを
|
|
1553
|
+
// 読み進めないことで、maxScan が大きい場合も一覧走査コストを抑える。
|
|
1554
|
+
if (cwd && expectedCwd && cwd !== expectedCwd) {
|
|
1555
|
+
return thread_id ? { thread_id, cwd, preview } : null
|
|
1556
|
+
}
|
|
1557
|
+
if (cwd && preview) {
|
|
1558
|
+
return thread_id ? { thread_id, cwd, preview } : null
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// 通常ファイルの EOF。decoder.end() で分割された UTF-8 の末尾を確定し、改行なしの
|
|
1563
|
+
// 最終行も一度だけ解析する。
|
|
1564
|
+
if (bytesRead < readSize) {
|
|
1565
|
+
pending += decoder.end()
|
|
1566
|
+
if (pending) parseLine(pending)
|
|
1567
|
+
break
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
} catch {
|
|
1571
|
+
return null
|
|
1572
|
+
} finally {
|
|
1573
|
+
try {
|
|
1574
|
+
await fh?.close()
|
|
1575
|
+
} catch {
|
|
1576
|
+
/* ignore */
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1548
1580
|
if (!thread_id) return null
|
|
1549
1581
|
return { thread_id, cwd, preview }
|
|
1550
1582
|
}
|
|
@@ -1610,7 +1642,7 @@ export async function listCodexThreadsFromDisk({
|
|
|
1610
1642
|
if (limit && out.length >= limit) break
|
|
1611
1643
|
if (opened >= maxScan) break
|
|
1612
1644
|
opened++
|
|
1613
|
-
const meta = await parseCodexRolloutHead(fp)
|
|
1645
|
+
const meta = await parseCodexRolloutHead(fp, cwd)
|
|
1614
1646
|
if (!meta) continue
|
|
1615
1647
|
// header の cwd が一致するものだけ (cwd 未読/不一致は除外 = 無関係スレッドの混入防止)。
|
|
1616
1648
|
if (meta.cwd !== cwd) continue
|