@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 +1 -1
- package/src/main.mjs +10 -1
- package/src/usage.mjs +86 -2
package/package.json
CHANGED
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 /
|
|
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
|
|