@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.8.95",
3
+ "version": "0.8.97",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -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 splitLinesBuf(complete)) {
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 = Buffer.concat([fingerprintTail, consumed]).subarray(
343
- Math.max(0, fingerprintTail.length + consumed.length - TITLE_FINGERPRINT_SIZE),
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(skillsDirFor(cliKind), "skill")))
185
+ out.push(...(await loadGlobalDir(loadSkillsDir, skillsDirFor(cliKind), "skill")))
172
186
  if (cliKind === "claude") {
173
- out.push(...(await loadCommandsDir(GLOBAL_COMMANDS_DIR, "command")))
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(GLOBAL_SKILLS_DIR, "skill")),
244
- ...(await loadCommandsDir(GLOBAL_COMMANDS_DIR, "command")),
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()
@@ -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
+ }