@cocorograph/hub-agent 0.8.95 → 0.8.97
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/claude-history.mjs +28 -4
- package/src/skills.mjs +19 -5
- package/src/startup-metrics.mjs +28 -0
package/package.json
CHANGED
package/src/claude-history.mjs
CHANGED
|
@@ -194,6 +194,28 @@ export function resetTitleMaxCarryBytesForTest() {
|
|
|
194
194
|
titleMaxCarryBytesForTest = 0
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
const CUSTOM_TITLE_MARKER = Buffer.from('"custom-title"')
|
|
198
|
+
const AI_TITLE_MARKER = Buffer.from('"ai-title"')
|
|
199
|
+
|
|
200
|
+
/** 印 ("custom-title" / "ai-title") を含む行だけをファイル順に返す (末尾の改行は含まない)。
|
|
201
|
+
* takeTitleFromLine は印を含まない行を捨てるだけなので、全行を分割して文字列化するのと結果は
|
|
202
|
+
* 同じ (印は ASCII で、UTF-8 のバイト列検索と文字列検索は一致する)。再起動直後の初回走査は
|
|
203
|
+
* 全文を読むため、全行の分割・文字列化が一覧取得の大半を占めていた。 */
|
|
204
|
+
function* titleMarkerLines(buf) {
|
|
205
|
+
let nextCustom = buf.indexOf(CUSTOM_TITLE_MARKER)
|
|
206
|
+
let nextAi = buf.indexOf(AI_TITLE_MARKER)
|
|
207
|
+
while (nextCustom >= 0 || nextAi >= 0) {
|
|
208
|
+
const hit = nextCustom < 0 ? nextAi : nextAi < 0 ? nextCustom : Math.min(nextCustom, nextAi)
|
|
209
|
+
const start = hit === 0 ? 0 : buf.lastIndexOf(NEWLINE_BYTE, hit - 1) + 1
|
|
210
|
+
const newline = buf.indexOf(NEWLINE_BYTE, hit)
|
|
211
|
+
const end = newline < 0 ? buf.length : newline
|
|
212
|
+
yield buf.subarray(start, end)
|
|
213
|
+
if (newline < 0) return
|
|
214
|
+
if (nextCustom >= 0 && nextCustom <= newline) nextCustom = buf.indexOf(CUSTOM_TITLE_MARKER, newline + 1)
|
|
215
|
+
if (nextAi >= 0 && nextAi <= newline) nextAi = buf.indexOf(AI_TITLE_MARKER, newline + 1)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
197
219
|
function takeTitleFromLine(line, entry) {
|
|
198
220
|
if (!line.includes('"custom-title"') && !line.includes('"ai-title"')) return
|
|
199
221
|
let obj
|
|
@@ -328,7 +350,7 @@ async function extractTitleCached(fileMap, filePath, size, mtimeMs) {
|
|
|
328
350
|
continue
|
|
329
351
|
}
|
|
330
352
|
const complete = region.subarray(0, lastNewline)
|
|
331
|
-
for (const lineBuf of
|
|
353
|
+
for (const lineBuf of titleMarkerLines(complete)) {
|
|
332
354
|
if (lineBuf.length > LAST_TURN_MAX_LINE) continue
|
|
333
355
|
const titleState = { customTitle, aiTitle }
|
|
334
356
|
takeTitleFromLine(lineBuf.toString("utf-8"), titleState)
|
|
@@ -339,9 +361,11 @@ async function extractTitleCached(fileMap, filePath, size, mtimeMs) {
|
|
|
339
361
|
titleMaxCarryBytesForTest = Math.max(titleMaxCarryBytesForTest, carry.length)
|
|
340
362
|
scannedBytes = position - carry.length
|
|
341
363
|
const consumed = region.subarray(0, lastNewline + 1)
|
|
342
|
-
fingerprintTail =
|
|
343
|
-
|
|
344
|
-
|
|
364
|
+
fingerprintTail = consumed.length >= TITLE_FINGERPRINT_SIZE
|
|
365
|
+
? consumed.subarray(consumed.length - TITLE_FINGERPRINT_SIZE)
|
|
366
|
+
: Buffer.concat([fingerprintTail, consumed]).subarray(
|
|
367
|
+
Math.max(0, fingerprintTail.length + consumed.length - TITLE_FINGERPRINT_SIZE),
|
|
368
|
+
)
|
|
345
369
|
}
|
|
346
370
|
|
|
347
371
|
const current = fileMap.get(filePath) || {}
|
package/src/skills.mjs
CHANGED
|
@@ -29,6 +29,20 @@ function skillsDirFor(cliKind) {
|
|
|
29
29
|
return cliKind === "codex" ? CODEX_GLOBAL_SKILLS_DIR : GLOBAL_SKILLS_DIR
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// 同じ共通ディレクトリの実行中の走査だけを共有する。結果は保持しない。
|
|
33
|
+
const globalScansInflight = new Map()
|
|
34
|
+
|
|
35
|
+
async function loadGlobalDir(loader, dir, source) {
|
|
36
|
+
const key = JSON.stringify([loader.name, dir, source])
|
|
37
|
+
let running = globalScansInflight.get(key)
|
|
38
|
+
if (!running) {
|
|
39
|
+
running = loader(dir, source).finally(() => globalScansInflight.delete(key))
|
|
40
|
+
globalScansInflight.set(key, running)
|
|
41
|
+
}
|
|
42
|
+
// 要素は文字列フィールドのみ。配列・要素を呼び出し元ごとに独立させる。
|
|
43
|
+
return (await running).map((entry) => ({ ...entry }))
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
// Claude Code 組み込みスラッシュコマンド。
|
|
33
47
|
// ⚠️ T06117: 通常経路では **SDK supportedCommands() を権威** に組込コマンドを得るため
|
|
34
48
|
// このハードコードは使わない。ここに残すのは「SDK が引けない (未実装 SDK / 一時エラー /
|
|
@@ -166,11 +180,11 @@ export async function listSkills(opts = {}) {
|
|
|
166
180
|
|
|
167
181
|
// フォールバック経路: SDK 未取得 / Codex は従来どおり FS 走査 + 組込ハードコード。
|
|
168
182
|
const out = []
|
|
169
|
-
out.push(...(cliKind === "codex" ? CODEX_BUILT_IN_COMMANDS : BUILT_IN_COMMANDS))
|
|
183
|
+
out.push(...(cliKind === "codex" ? CODEX_BUILT_IN_COMMANDS : BUILT_IN_COMMANDS).map((entry) => ({ ...entry })))
|
|
170
184
|
const globalStarted = performance.now()
|
|
171
|
-
out.push(...(await loadSkillsDir
|
|
185
|
+
out.push(...(await loadGlobalDir(loadSkillsDir, skillsDirFor(cliKind), "skill")))
|
|
172
186
|
if (cliKind === "claude") {
|
|
173
|
-
out.push(...(await loadCommandsDir
|
|
187
|
+
out.push(...(await loadGlobalDir(loadCommandsDir, GLOBAL_COMMANDS_DIR, "command")))
|
|
174
188
|
}
|
|
175
189
|
|
|
176
190
|
if (opts.timing) opts.timing.global_scan_ms = performance.now() - globalStarted
|
|
@@ -240,8 +254,8 @@ async function listSkillsFromSdk(sdkCommands, sessionName, projectsRoot, timing)
|
|
|
240
254
|
// label_ja オーバーレイ用の FS 走査 (global) + project-local の補完材料。
|
|
241
255
|
const globalStarted = performance.now()
|
|
242
256
|
const globalLabelSources = [
|
|
243
|
-
...(await loadSkillsDir
|
|
244
|
-
...(await loadCommandsDir
|
|
257
|
+
...(await loadGlobalDir(loadSkillsDir, GLOBAL_SKILLS_DIR, "skill")),
|
|
258
|
+
...(await loadGlobalDir(loadCommandsDir, GLOBAL_COMMANDS_DIR, "command")),
|
|
245
259
|
]
|
|
246
260
|
if (timing) timing.global_scan_ms = performance.now() - globalStarted
|
|
247
261
|
const projectStarted = performance.now()
|
package/src/startup-metrics.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os from "node:os"
|
|
1
2
|
import { monitorEventLoopDelay, performance } from "node:perf_hooks"
|
|
2
3
|
|
|
3
4
|
// performance.now() はプロセス起動を原点とする単調時計。テストでは _setNowForTest で差し替える。
|
|
@@ -26,10 +27,19 @@ export function logPhaseTiming(logger, msg, fields, now = defaultNow) {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* 起動直後の混み具合を 10/30/60 秒時点で 1 行ずつ出す。
|
|
32
|
+
* cpu_*_ms はプロセス起動からの累積 CPU 時間、load_avg_1m は端末全体の 1 分負荷平均。
|
|
33
|
+
* 経過時間に比べて自分の CPU 時間が小さく負荷平均が高ければ、端末上の他プロセスとの取り合い
|
|
34
|
+
* (起動直後の CLI 一斉起動など) を疑う手掛かりになる。ただし負荷平均は過去 1 分の平均で、
|
|
35
|
+
* 起動前の負荷も残るため、これだけで原因を断定しない (I/O 待ちでも自分の CPU 時間は小さい)。
|
|
36
|
+
*/
|
|
29
37
|
export function startEventLoopMetrics(logger, {
|
|
30
38
|
now = defaultNow,
|
|
31
39
|
monitor = monitorEventLoopDelay,
|
|
32
40
|
schedule = setTimeout,
|
|
41
|
+
cpuUsage = () => process.cpuUsage(),
|
|
42
|
+
loadAvg = () => os.loadavg(),
|
|
33
43
|
} = {}) {
|
|
34
44
|
const histogram = monitor({ resolution: 20 })
|
|
35
45
|
histogram.enable()
|
|
@@ -41,6 +51,7 @@ export function startEventLoopMetrics(logger, {
|
|
|
41
51
|
p50_ms: histogram.percentile(50) / 1e6,
|
|
42
52
|
p99_ms: histogram.percentile(99) / 1e6,
|
|
43
53
|
max_ms: histogram.max / 1e6,
|
|
54
|
+
...cpuFields(cpuUsage, loadAvg),
|
|
44
55
|
}, "startup event loop delay")
|
|
45
56
|
} catch {
|
|
46
57
|
// 計測ログの失敗は無視する
|
|
@@ -50,3 +61,20 @@ export function startEventLoopMetrics(logger, {
|
|
|
50
61
|
}, Math.max(0, deadline - now())).unref()
|
|
51
62
|
}
|
|
52
63
|
}
|
|
64
|
+
|
|
65
|
+
function cpuFields(cpuUsage, loadAvg) {
|
|
66
|
+
const fields = {}
|
|
67
|
+
try {
|
|
68
|
+
const { user, system } = cpuUsage()
|
|
69
|
+
fields.cpu_user_ms = user / 1000
|
|
70
|
+
fields.cpu_system_ms = system / 1000
|
|
71
|
+
} catch {
|
|
72
|
+
// CPU 時間が取れなくても遅延の記録は出す
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
fields.load_avg_1m = loadAvg()[0]
|
|
76
|
+
} catch {
|
|
77
|
+
// 同上
|
|
78
|
+
}
|
|
79
|
+
return fields
|
|
80
|
+
}
|