@cocorograph/hub-agent 0.6.7 → 0.6.9

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.6.7",
3
+ "version": "0.6.9",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/src/main.mjs CHANGED
@@ -37,7 +37,7 @@ import {
37
37
  listWorktreeStubs,
38
38
  removeWorktree as removeWorktreeDir,
39
39
  } from "./tmux.mjs"
40
- import { getSessionUsages, getUsage } from "./usage.mjs"
40
+ import { getSessionUsages, getUsage, recordChatRateLimit } from "./usage.mjs"
41
41
 
42
42
  const logger = pino({ name: "hub-agent" })
43
43
 
@@ -179,6 +179,15 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
179
179
  // claude.error を返す経路に分岐 (dispatch 側で判定)。
180
180
  if (claudeBridge) {
181
181
  claudeBridge.on("event", ({ stream_id, session_id, event }) => {
182
+ // SDK の rate_limit_event を捕捉して usage の 5h/7d 実値に反映する
183
+ // (チャットモードでは statusLine が更新されないため、これが live 値の唯一の源)。
184
+ if (event?.type === "rate_limit_event" && event.rate_limit_info) {
185
+ try {
186
+ recordChatRateLimit(event.rate_limit_info)
187
+ } catch {
188
+ /* ignore */
189
+ }
190
+ }
182
191
  client.send({ type: "claude.event", stream_id, session_id, event })
183
192
  })
184
193
  claudeBridge.on("permission", ({ stream_id, request_id, tool_name, input }) => {
package/src/usage.mjs CHANGED
@@ -107,6 +107,42 @@ async function readOrNull(p) {
107
107
  }
108
108
  }
109
109
 
110
+ // ---------------------------------------------------------------------------
111
+ // チャット(SDK) の rate_limit_event から取得した最新 rate-limit (プロセス内共有)。
112
+ // statusLine cache はターミナルでしか更新されないため、チャットモードでは
113
+ // SDK stream の rate_limit_event を捕捉してここに溜め、getUsage で 5h/7d に反映する。
114
+ // ---------------------------------------------------------------------------
115
+ const chatRateLimits = {
116
+ /** @type {{percent:number, resetAtMs:number|null}|null} */ five_hour: null,
117
+ /** @type {{percent:number, resetAtMs:number|null}|null} */ seven_day: null,
118
+ updatedAtMs: 0,
119
+ }
120
+
121
+ /** utilization (0-1 割合 or 0-100 %) を % に正規化。 */
122
+ function utilizationToPercent(u) {
123
+ if (typeof u !== "number" || !Number.isFinite(u)) return null
124
+ return u <= 1 ? Math.round(u * 1000) / 10 : Math.round(u * 10) / 10
125
+ }
126
+
127
+ /**
128
+ * SDK の SDKRateLimitInfo を取り込む (main.mjs の rate_limit_event ハンドラから呼ぶ)。
129
+ * rateLimitType: 'five_hour' / 'seven_day*' を 5h / 7d スロットに振り分ける。
130
+ */
131
+ export function recordChatRateLimit(info) {
132
+ if (!info || typeof info !== "object") return
133
+ const type = info.rateLimitType
134
+ const slot = type === "five_hour" ? "five_hour" : typeof type === "string" && type.startsWith("seven_day") ? "seven_day" : null
135
+ if (!slot) return // overage 等は対象外
136
+ const percent = utilizationToPercent(info.utilization)
137
+ if (percent === null) return
138
+ let resetAtMs = null
139
+ if (typeof info.resetsAt === "number") {
140
+ resetAtMs = info.resetsAt < 1e12 ? info.resetsAt * 1000 : info.resetsAt
141
+ }
142
+ chatRateLimits[slot] = { percent, resetAtMs }
143
+ chatRateLimits.updatedAtMs = Date.now()
144
+ }
145
+
110
146
  async function readOfficial(now) {
111
147
  const text = await readOrNull(statuslineCache())
112
148
  if (!text) return null
@@ -245,6 +281,26 @@ async function statuslineCacheMtime() {
245
281
  }
246
282
  }
247
283
 
284
+ /**
285
+ * context 窓サイズ (トークン) を解決する。statusLine cache に context_window_size が
286
+ * あればそれを使う (1M ベータ等を自動反映)。無ければ HUB_CONTEXT_WINDOW / 200000。
287
+ * ※ cache が stale でも窓サイズ自体は不変なので利用できる。
288
+ */
289
+ async function contextWindowSize() {
290
+ const text = await readOrNull(statuslineCache())
291
+ if (text) {
292
+ try {
293
+ const j = JSON.parse(text)
294
+ const cw = j.context_window ?? j.contextWindow
295
+ const size = cw?.context_window_size ?? cw?.contextWindowSize
296
+ if (typeof size === "number" && size > 0) return size
297
+ } catch {
298
+ /* ignore */
299
+ }
300
+ }
301
+ return CONTEXT_WINDOW
302
+ }
303
+
248
304
  /**
249
305
  * 直近にアクティブな session jsonl の最後の assistant.usage から context% を
250
306
  * 推定する。チャットモード(SDK headless)は statusLine を書き出さないため、
@@ -279,6 +335,7 @@ async function latestJsonlContext(now) {
279
335
  }),
280
336
  )
281
337
  if (!best) return null
338
+ const windowSize = await contextWindowSize()
282
339
  const text = await readOrNull(best.fp)
283
340
  if (!text) return null
284
341
  const lines = text.split("\n")
@@ -301,7 +358,7 @@ async function latestJsonlContext(now) {
301
358
  (u.cache_creation_input_tokens || 0) +
302
359
  (u.output_tokens || 0)
303
360
  if (tokens <= 0) continue
304
- const percent = Math.min(100, Math.round((tokens / CONTEXT_WINDOW) * 1000) / 10)
361
+ const percent = Math.min(100, Math.round((tokens / windowSize) * 1000) / 10)
305
362
  return { percent, mtimeMs: best.mtimeMs }
306
363
  }
307
364
  return null
@@ -318,10 +375,10 @@ export async function getUsage() {
318
375
  const now = Date.now()
319
376
  const official = await readOfficial(now)
320
377
  const result = official || (await readEstimate(now))
378
+ const cacheMtime = await statuslineCacheMtime()
321
379
 
322
380
  const est = await latestJsonlContext(now)
323
381
  if (est) {
324
- const cacheMtime = await statuslineCacheMtime()
325
382
  // statusLine が jsonl と同程度以上に新しければ official.context が信頼できる
326
383
  // (ターミナルモード)。jsonl の方が新しい (チャットモード) なら推定で上書き。
327
384
  const officialFresh =
@@ -333,6 +390,33 @@ export async function getUsage() {
333
390
  result.context = est.percent
334
391
  }
335
392
  }
393
+
394
+ // チャット(SDK) の rate_limit_event が statusLine cache より新しければ、5h/7d を
395
+ // その実値 (utilization) で上書きする。ターミナルが statusLine を更新していない
396
+ // チャットモードでも 5h/7d がライブで動くようになる。
397
+ if (
398
+ chatRateLimits.updatedAtMs > 0 &&
399
+ (cacheMtime === null || chatRateLimits.updatedAtMs > cacheMtime)
400
+ ) {
401
+ if (chatRateLimits.five_hour) {
402
+ result.last5h = {
403
+ ...result.last5h,
404
+ percent: chatRateLimits.five_hour.percent,
405
+ resetAtMs: chatRateLimits.five_hour.resetAtMs ?? result.last5h?.resetAtMs ?? null,
406
+ }
407
+ }
408
+ if (chatRateLimits.seven_day) {
409
+ result.last7d = {
410
+ ...result.last7d,
411
+ percent: chatRateLimits.seven_day.percent,
412
+ resetAtMs: chatRateLimits.seven_day.resetAtMs ?? result.last7d?.resetAtMs ?? null,
413
+ }
414
+ }
415
+ if (chatRateLimits.five_hour || chatRateLimits.seven_day) {
416
+ // rate_limit_event は API 実値なので official 扱い。
417
+ result.source = "official"
418
+ }
419
+ }
336
420
  return result
337
421
  }
338
422