@cocorograph/hub-agent 0.6.6 → 0.6.8
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 +118 -2
package/package.json
CHANGED
package/src/usage.mjs
CHANGED
|
@@ -55,6 +55,12 @@ function statuslineSessionsDir() {
|
|
|
55
55
|
|
|
56
56
|
const SESSION_STALE_MS = 15 * 60 * 1000
|
|
57
57
|
|
|
58
|
+
// コンテキスト窓サイズ (トークン)。Opus/Sonnet 4.x は 200k が標準。
|
|
59
|
+
// 1M ベータ等を使う場合は HUB_CONTEXT_WINDOW で上書き。
|
|
60
|
+
const CONTEXT_WINDOW = Number(process.env.HUB_CONTEXT_WINDOW) || 200000
|
|
61
|
+
// チャットモード(SDK)の context% 推定で参照する jsonl の鮮度窓 (直近 N 分)。
|
|
62
|
+
const CONTEXT_JSONL_RECENT_MS = 30 * 60 * 1000
|
|
63
|
+
|
|
58
64
|
const PLAN_LIMITS = {
|
|
59
65
|
pro: { msg5h: 45, msg7d: 315 },
|
|
60
66
|
max_5x: { msg5h: 280, msg7d: 1960 },
|
|
@@ -230,15 +236,125 @@ async function readEstimate(now) {
|
|
|
230
236
|
}
|
|
231
237
|
}
|
|
232
238
|
|
|
239
|
+
/** statusLine cache ファイルの mtime (ms)。存在しなければ 0。 */
|
|
240
|
+
async function statuslineCacheMtime() {
|
|
241
|
+
try {
|
|
242
|
+
return (await fs.stat(statuslineCache())).mtimeMs
|
|
243
|
+
} catch {
|
|
244
|
+
return null
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* context 窓サイズ (トークン) を解決する。statusLine cache に context_window_size が
|
|
250
|
+
* あればそれを使う (1M ベータ等を自動反映)。無ければ HUB_CONTEXT_WINDOW / 200000。
|
|
251
|
+
* ※ cache が stale でも窓サイズ自体は不変なので利用できる。
|
|
252
|
+
*/
|
|
253
|
+
async function contextWindowSize() {
|
|
254
|
+
const text = await readOrNull(statuslineCache())
|
|
255
|
+
if (text) {
|
|
256
|
+
try {
|
|
257
|
+
const j = JSON.parse(text)
|
|
258
|
+
const cw = j.context_window ?? j.contextWindow
|
|
259
|
+
const size = cw?.context_window_size ?? cw?.contextWindowSize
|
|
260
|
+
if (typeof size === "number" && size > 0) return size
|
|
261
|
+
} catch {
|
|
262
|
+
/* ignore */
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return CONTEXT_WINDOW
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 直近にアクティブな session jsonl の最後の assistant.usage から context% を
|
|
270
|
+
* 推定する。チャットモード(SDK headless)は statusLine を書き出さないため、
|
|
271
|
+
* ドーナツ(usage.context)が固まるのを防ぐフォールバック。
|
|
272
|
+
*
|
|
273
|
+
* context トークン = input + cache_read + cache_creation (+ 直近応答の output)。
|
|
274
|
+
* プロンプトキャッシュ有効時は cache_creation / cache_read に大半が乗るため、
|
|
275
|
+
* input_tokens だけだと大幅に過小評価になる点に注意 (3 種を必ず合算する)。
|
|
276
|
+
*
|
|
277
|
+
* @returns {Promise<{ percent: number, mtimeMs: number } | null>}
|
|
278
|
+
*/
|
|
279
|
+
async function latestJsonlContext(now) {
|
|
280
|
+
const projects = await fs.readdir(projectsDir()).catch(() => null)
|
|
281
|
+
if (!projects) return null
|
|
282
|
+
const recent = now - CONTEXT_JSONL_RECENT_MS
|
|
283
|
+
let best = null // { mtimeMs, fp }
|
|
284
|
+
await Promise.all(
|
|
285
|
+
projects.map(async (p) => {
|
|
286
|
+
const dir = path.join(projectsDir(), p)
|
|
287
|
+
const files = await fs.readdir(dir).catch(() => [])
|
|
288
|
+
for (const f of files) {
|
|
289
|
+
if (!f.endsWith(".jsonl")) continue
|
|
290
|
+
const fp = path.join(dir, f)
|
|
291
|
+
try {
|
|
292
|
+
const st = await fs.stat(fp)
|
|
293
|
+
if (st.mtimeMs < recent) continue
|
|
294
|
+
if (!best || st.mtimeMs > best.mtimeMs) best = { mtimeMs: st.mtimeMs, fp }
|
|
295
|
+
} catch {
|
|
296
|
+
/* ignore */
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}),
|
|
300
|
+
)
|
|
301
|
+
if (!best) return null
|
|
302
|
+
const windowSize = await contextWindowSize()
|
|
303
|
+
const text = await readOrNull(best.fp)
|
|
304
|
+
if (!text) return null
|
|
305
|
+
const lines = text.split("\n")
|
|
306
|
+
// 末尾から最初に見つかった assistant.usage を採用 (= 現在の文脈サイズ)
|
|
307
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
308
|
+
const line = lines[i]
|
|
309
|
+
if (!line || !line.includes('"usage"')) continue
|
|
310
|
+
let d
|
|
311
|
+
try {
|
|
312
|
+
d = JSON.parse(line)
|
|
313
|
+
} catch {
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
if (d.type !== "assistant") continue
|
|
317
|
+
const u = d.message?.usage
|
|
318
|
+
if (!u) continue
|
|
319
|
+
const tokens =
|
|
320
|
+
(u.input_tokens || 0) +
|
|
321
|
+
(u.cache_read_input_tokens || 0) +
|
|
322
|
+
(u.cache_creation_input_tokens || 0) +
|
|
323
|
+
(u.output_tokens || 0)
|
|
324
|
+
if (tokens <= 0) continue
|
|
325
|
+
const percent = Math.min(100, Math.round((tokens / windowSize) * 1000) / 10)
|
|
326
|
+
return { percent, mtimeMs: best.mtimeMs }
|
|
327
|
+
}
|
|
328
|
+
return null
|
|
329
|
+
}
|
|
330
|
+
|
|
233
331
|
/**
|
|
234
332
|
* 5h / 7d 集計の UsageStats を返す。statusLine が無い環境では transcript
|
|
235
333
|
* から推定する。
|
|
334
|
+
*
|
|
335
|
+
* context%: statusLine 値が最新 jsonl より新しければ official を優先。
|
|
336
|
+
* 古い / 無い (= チャットモードで statusLine 未更新) 場合は jsonl 推定で上書き。
|
|
236
337
|
*/
|
|
237
338
|
export async function getUsage() {
|
|
238
339
|
const now = Date.now()
|
|
239
340
|
const official = await readOfficial(now)
|
|
240
|
-
|
|
241
|
-
|
|
341
|
+
const result = official || (await readEstimate(now))
|
|
342
|
+
|
|
343
|
+
const est = await latestJsonlContext(now)
|
|
344
|
+
if (est) {
|
|
345
|
+
const cacheMtime = await statuslineCacheMtime()
|
|
346
|
+
// statusLine が jsonl と同程度以上に新しければ official.context が信頼できる
|
|
347
|
+
// (ターミナルモード)。jsonl の方が新しい (チャットモード) なら推定で上書き。
|
|
348
|
+
const officialFresh =
|
|
349
|
+
official &&
|
|
350
|
+
typeof official.context === "number" &&
|
|
351
|
+
cacheMtime !== null &&
|
|
352
|
+
cacheMtime >= est.mtimeMs - 5000
|
|
353
|
+
if (!officialFresh) {
|
|
354
|
+
result.context = est.percent
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return result
|
|
242
358
|
}
|
|
243
359
|
|
|
244
360
|
/**
|