@cocorograph/hub-agent 0.7.93 → 0.7.95
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/codex-stream-bridge.mjs +22 -0
- package/src/main.mjs +99 -4
- package/src/usage.mjs +152 -37
package/package.json
CHANGED
|
@@ -333,6 +333,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
333
333
|
}
|
|
334
334
|
/** @type {Map<string, object>} stream_id -> session record */
|
|
335
335
|
this.sessions = new Map()
|
|
336
|
+
this._accountRateLimitsByProfile = new Map()
|
|
336
337
|
}
|
|
337
338
|
|
|
338
339
|
/**
|
|
@@ -787,6 +788,27 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
787
788
|
}
|
|
788
789
|
}
|
|
789
790
|
|
|
791
|
+
/**
|
|
792
|
+
* アカウント単位の利用制限を取得する。thread を起動しないため、全セッション停止中でも
|
|
793
|
+
* Cockpit フッターを最新化できる。呼び出し側は認証済みプロファイルの場合だけ呼ぶ。
|
|
794
|
+
*/
|
|
795
|
+
async readAccountRateLimits({ cwd, profileId = "default" } = {}) {
|
|
796
|
+
const client = this._clientFactory({ cwd: cwd || process.cwd(), codexCmd: this.codexCmd, logger: this.logger })
|
|
797
|
+
client.start()
|
|
798
|
+
try {
|
|
799
|
+
await client.request("initialize", { clientInfo: CLIENT_INFO })
|
|
800
|
+
const result = await client.request("account/rateLimits/read", {})
|
|
801
|
+
this._accountRateLimitsByProfile.set(profileId, result)
|
|
802
|
+
return result
|
|
803
|
+
} catch (err) {
|
|
804
|
+
const cached = this._accountRateLimitsByProfile.get(profileId)
|
|
805
|
+
if (cached) return cached
|
|
806
|
+
throw err
|
|
807
|
+
} finally {
|
|
808
|
+
client.stop()
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
790
812
|
_wireClient(session) {
|
|
791
813
|
const { client } = session
|
|
792
814
|
client.on("notification", (msg) => this._onNotification(session, msg))
|
package/src/main.mjs
CHANGED
|
@@ -118,6 +118,7 @@ import { SiblingInjectBridge } from "./sibling-inject.mjs"
|
|
|
118
118
|
import {
|
|
119
119
|
boundSessionId,
|
|
120
120
|
codexContextPercentFromTokenUsage,
|
|
121
|
+
codexUsageFromAccountRateLimits,
|
|
121
122
|
codexLiveContextForProducer,
|
|
122
123
|
codexSessionUsages,
|
|
123
124
|
codexTurnActiveForThread,
|
|
@@ -130,6 +131,8 @@ import {
|
|
|
130
131
|
jsonlContextForCwd,
|
|
131
132
|
recordChatRateLimit,
|
|
132
133
|
recordChatUsageSnapshot,
|
|
134
|
+
readAccountUsageSnapshot,
|
|
135
|
+
recordAccountUsageSnapshot,
|
|
133
136
|
recordCodexLiveTokenUsage,
|
|
134
137
|
turnActiveForCwd,
|
|
135
138
|
} from "./usage.mjs"
|
|
@@ -3076,6 +3079,50 @@ export function handleClaudeChatInput(msg, ctx) {
|
|
|
3076
3079
|
})
|
|
3077
3080
|
}
|
|
3078
3081
|
|
|
3082
|
+
/**
|
|
3083
|
+
* claude.attach の ready 直後にpushする履歴を組み立てる。
|
|
3084
|
+
* Browserからの claude.history.request 往復を省くため、resume時だけ先回りして読む。
|
|
3085
|
+
* 失敗しても空のhistory.responseへ縮退し、attach自体を失敗させない。
|
|
3086
|
+
*/
|
|
3087
|
+
export async function loadClaudeAttachHistory({
|
|
3088
|
+
cwd,
|
|
3089
|
+
sessionId,
|
|
3090
|
+
logger,
|
|
3091
|
+
getProjectsRootFn = getActiveProjectsRoot,
|
|
3092
|
+
fetchHistoryFn = fetchSessionHistory,
|
|
3093
|
+
}) {
|
|
3094
|
+
if (!cwd || !sessionId) return null
|
|
3095
|
+
try {
|
|
3096
|
+
const projectsRoot = await getProjectsRootFn()
|
|
3097
|
+
const result = await fetchHistoryFn({
|
|
3098
|
+
cwd,
|
|
3099
|
+
session_id: sessionId,
|
|
3100
|
+
maxLines: 500,
|
|
3101
|
+
projectsRoot,
|
|
3102
|
+
logger,
|
|
3103
|
+
})
|
|
3104
|
+
return {
|
|
3105
|
+
type: "claude.history.response",
|
|
3106
|
+
session_id: sessionId,
|
|
3107
|
+
cwd,
|
|
3108
|
+
events: result.events || [],
|
|
3109
|
+
total_lines: result.total_lines || 0,
|
|
3110
|
+
truncated: result.truncated === true,
|
|
3111
|
+
error: result.error,
|
|
3112
|
+
}
|
|
3113
|
+
} catch (err) {
|
|
3114
|
+
return {
|
|
3115
|
+
type: "claude.history.response",
|
|
3116
|
+
session_id: sessionId,
|
|
3117
|
+
cwd,
|
|
3118
|
+
events: [],
|
|
3119
|
+
total_lines: 0,
|
|
3120
|
+
truncated: false,
|
|
3121
|
+
error: err?.message || String(err),
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3079
3126
|
/**
|
|
3080
3127
|
* SDK チャット配達保証 (S2, 2026-07-05): `codex.input` を処理し、browser が付与した
|
|
3081
3128
|
* `input_id` があれば `codex.input.ack` / `codex.input.nack` を返す (claude.input と
|
|
@@ -3494,6 +3541,16 @@ async function dispatch(msg, ctx) {
|
|
|
3494
3541
|
// model / permissionMode は agent.json の値も使えるよう fallback。
|
|
3495
3542
|
const cwdArg = msg.cwd || ctx.config?.claude_cwd || undefined
|
|
3496
3543
|
const resumeSessionId = msg.resume_session_id || null
|
|
3544
|
+
// attachは同期処理なので、履歴I/Oを先に開始してreadyまでの待ちを重ねる。
|
|
3545
|
+
// 新Browserはhistory_pushedを見て別requestを省略し、旧Browserは従来どおり
|
|
3546
|
+
// requestしてもhydrateのuuid dedupで安全に収束する。
|
|
3547
|
+
const historyPromise = resumeSessionId
|
|
3548
|
+
? loadClaudeAttachHistory({
|
|
3549
|
+
cwd: cwdArg || process.env.HOME || process.cwd(),
|
|
3550
|
+
sessionId: resumeSessionId,
|
|
3551
|
+
logger: ctx.logger,
|
|
3552
|
+
})
|
|
3553
|
+
: Promise.resolve(null)
|
|
3497
3554
|
const info = ctx.claudeBridge.attach({
|
|
3498
3555
|
stream_id,
|
|
3499
3556
|
cwd: cwdArg,
|
|
@@ -3517,12 +3574,25 @@ async function dispatch(msg, ctx) {
|
|
|
3517
3574
|
type: "claude.ready",
|
|
3518
3575
|
stream_id,
|
|
3519
3576
|
resuming: info.resuming,
|
|
3577
|
+
history_pushed: resumeSessionId !== null,
|
|
3520
3578
|
// id 空間統一 (2026-07-08): この agent は browser の input_id をキュー id として
|
|
3521
3579
|
// 採用し、楽観送信待ちアイテムの確認前キャンセルに対応することを通知する capability。
|
|
3522
3580
|
// 旧 agent にはこのフラグが無く、browser は楽観アイテムの×を出さない (ゴースト
|
|
3523
3581
|
// 実行防止のフォールバック)。
|
|
3524
3582
|
supports_queue_input_id: true,
|
|
3525
3583
|
})
|
|
3584
|
+
// dispatch全体はWSメッセージ順を守る直列キュー上にある。ここで履歴I/Oをawaitすると
|
|
3585
|
+
// 同一接続のpermission/input等まで停止するため、readyの同期送信後は非同期でpushする。
|
|
3586
|
+
void historyPromise
|
|
3587
|
+
.then((pushedHistory) => {
|
|
3588
|
+
if (pushedHistory) ctx.client.send({ ...pushedHistory, stream_id })
|
|
3589
|
+
})
|
|
3590
|
+
.catch((err) => {
|
|
3591
|
+
ctx.logger?.warn?.(
|
|
3592
|
+
{ err: err?.message || String(err), stream_id },
|
|
3593
|
+
"failed to push Claude attach history",
|
|
3594
|
+
)
|
|
3595
|
+
})
|
|
3526
3596
|
// S1 (2026-07): セッション状態の identity 正本化。TUI の claude.tui.bind
|
|
3527
3597
|
// (main.mjs 内 clearChatSignal 呼び出し箇所参照) と対称に、SDK チャットの
|
|
3528
3598
|
// attach 時点でも前セッションのチャット信号を引き継がない。
|
|
@@ -5565,6 +5635,10 @@ async function dispatch(msg, ctx) {
|
|
|
5565
5635
|
}
|
|
5566
5636
|
case "usage.request": {
|
|
5567
5637
|
try {
|
|
5638
|
+
const activeClaudeProfile = await getActiveProfile()
|
|
5639
|
+
const claudeHasCredentials = isKeychainPlatform()
|
|
5640
|
+
? await profileHasKeychainCredentials(activeClaudeProfile.configDir)
|
|
5641
|
+
: await hasFileCredentials(activeClaudeProfile.configDir)
|
|
5568
5642
|
// 5h/7d の本命ソース: SDK control request (アカウント全体、session 非依存)。
|
|
5569
5643
|
// statusLine が rate_limits を出さなくなった環境 (2026-07 時点の Claude CLI) でも
|
|
5570
5644
|
// ここでライブ更新できる。frontend の usage.request ポーリング (既定 60s) 頻度に
|
|
@@ -5574,15 +5648,20 @@ async function dispatch(msg, ctx) {
|
|
|
5574
5648
|
if (ctx.claudeBridge) {
|
|
5575
5649
|
try {
|
|
5576
5650
|
const acct = await ctx.claudeBridge.getAccountUsage()
|
|
5577
|
-
if (acct) recordChatUsageSnapshot(acct)
|
|
5651
|
+
if (acct) recordChatUsageSnapshot(acct, Date.now(), activeClaudeProfile.id)
|
|
5578
5652
|
} catch (err) {
|
|
5579
5653
|
ctx.logger?.debug?.({ err: err?.message }, "account usage snapshot skipped")
|
|
5580
5654
|
}
|
|
5581
5655
|
}
|
|
5582
|
-
|
|
5583
|
-
getUsage(),
|
|
5656
|
+
let [usage, sessions] = await Promise.all([
|
|
5657
|
+
getUsage({ profileId: activeClaudeProfile.id }),
|
|
5584
5658
|
getSessionUsages(),
|
|
5585
5659
|
])
|
|
5660
|
+
// 未ログインで残ったローカル transcript/cache をアカウント利用量として見せない。
|
|
5661
|
+
if (!claudeHasCredentials) {
|
|
5662
|
+
usage = null
|
|
5663
|
+
sessions = []
|
|
5664
|
+
}
|
|
5586
5665
|
|
|
5587
5666
|
// Codex 版 (Stream B3, 2026-07-03): rollout jsonl の token_count イベントから
|
|
5588
5667
|
// 5h/7d + context% を取る。⚠️ Codex の 5h/7d は ChatGPT/OpenAI サブスクリプション
|
|
@@ -5591,7 +5670,9 @@ async function dispatch(msg, ctx) {
|
|
|
5591
5670
|
// として返す (frontend 側でどちらのアカウントの残量か混同しないため)。
|
|
5592
5671
|
let codexUsage = null
|
|
5593
5672
|
let codexSessions = []
|
|
5594
|
-
|
|
5673
|
+
const activeCodexProfile = await codexProfiles.getActiveProfile()
|
|
5674
|
+
const codexHasCredentials = await codexProfiles.hasCodexCredentials(activeCodexProfile.configDir)
|
|
5675
|
+
if (ctx.codexBridge && codexHasCredentials) {
|
|
5595
5676
|
try {
|
|
5596
5677
|
// T06407 増分2 (axis2): per-cwd の context% は **cwd ごとに権威 producer 1 件**へ
|
|
5597
5678
|
// 集約する。旧実装は全 live session を codexSessions へ流していたため、同一 cwd に
|
|
@@ -5641,10 +5722,24 @@ async function dispatch(msg, ctx) {
|
|
|
5641
5722
|
codexUsage = { ...codexUsage, context: null }
|
|
5642
5723
|
}
|
|
5643
5724
|
}
|
|
5725
|
+
// ライブ session の有無にかかわらず、起動直後の account RPC を優先する。
|
|
5726
|
+
// rollout は context の補助だけにし、5h/7d はプロファイル固有の最新実値にする。
|
|
5727
|
+
const accountUsage = codexUsageFromAccountRateLimits(
|
|
5728
|
+
await ctx.codexBridge.readAccountRateLimits({ profileId: activeCodexProfile.id }),
|
|
5729
|
+
)
|
|
5730
|
+
if (accountUsage) {
|
|
5731
|
+
codexUsage = { ...accountUsage, context: codexUsage?.context ?? null }
|
|
5732
|
+
recordAccountUsageSnapshot("codex", activeCodexProfile.id, codexUsage)
|
|
5733
|
+
}
|
|
5644
5734
|
} catch (err) {
|
|
5645
5735
|
ctx.logger?.debug?.({ err: err?.message }, "codex usage snapshot skipped")
|
|
5646
5736
|
}
|
|
5647
5737
|
}
|
|
5738
|
+
// Agent 再起動後や一時的な account RPC 失敗時も、同一 profile の最終取得値だけを
|
|
5739
|
+
// 表示する。未ログインならここへ到達せず、表示も完全に隠す。
|
|
5740
|
+
if (codexHasCredentials && !codexUsage) {
|
|
5741
|
+
codexUsage = await readAccountUsageSnapshot("codex", activeCodexProfile.id)
|
|
5742
|
+
}
|
|
5648
5743
|
|
|
5649
5744
|
ctx.client.send({
|
|
5650
5745
|
type: "usage.response",
|
package/src/usage.mjs
CHANGED
|
@@ -292,12 +292,26 @@ export function whenChatRateLimitsPersisted() {
|
|
|
292
292
|
// (旧 chatRateLimits の設計的弱点)。専用ファイルなら誰にも上書きされない。
|
|
293
293
|
// ---------------------------------------------------------------------------
|
|
294
294
|
|
|
295
|
-
function
|
|
296
|
-
|
|
295
|
+
function profileCacheKey(profileId = "default") {
|
|
296
|
+
// Profile id は UI から作られるが、キャッシュパスに使う前に必ず無害な文字列へ
|
|
297
|
+
// 正規化する。プロフィール間の値が混ざるより、読めない cache を無視する方が安全。
|
|
298
|
+
const value = String(profileId || "default")
|
|
299
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 120) || "default"
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function chatUsageCachePath(profileId = "default") {
|
|
303
|
+
const configured = process.env.HUB_CHAT_USAGE_CACHE
|
|
304
|
+
if (configured) {
|
|
305
|
+
// 既存の環境変数は default profile の互換パスとして維持する。追加 profile は
|
|
306
|
+
// 同じディレクトリ内に別ファイルで置き、最後に利用した別 profile を読まない。
|
|
307
|
+
if (profileId === "default") return configured
|
|
308
|
+
return path.join(path.dirname(configured), `chat-usage-${profileCacheKey(profileId)}.json`)
|
|
309
|
+
}
|
|
310
|
+
return path.join(os.homedir(), ".hub", "usage", "chat-usage", `${profileCacheKey(profileId)}.json`)
|
|
297
311
|
}
|
|
298
312
|
|
|
299
313
|
/** @type {{five_hour:{percent:number,resetAtMs:number|null}|null, seven_day:{percent:number,resetAtMs:number|null}|null, plan:string|null, updatedAtMs:number}|null} */
|
|
300
|
-
let
|
|
314
|
+
let chatUsageSnapshots = new Map()
|
|
301
315
|
let _chatUsagePersistInFlight = Promise.resolve()
|
|
302
316
|
|
|
303
317
|
/** SDKControlGetUsageResponse.rate_limits.{five_hour,seven_day} の 1 窓を正規化する。 */
|
|
@@ -322,8 +336,9 @@ function normalizeRateLimitWindow(w) {
|
|
|
322
336
|
*
|
|
323
337
|
* @param {{rate_limits_available?: boolean, rate_limits?: {five_hour?: object|null, seven_day?: object|null}|null, subscription_type?: string|null}|null} usageResponse
|
|
324
338
|
* @param {number} [now]
|
|
339
|
+
* @param {string} [profileId]
|
|
325
340
|
*/
|
|
326
|
-
export function recordChatUsageSnapshot(usageResponse, now = Date.now()) {
|
|
341
|
+
export function recordChatUsageSnapshot(usageResponse, now = Date.now(), profileId = "default") {
|
|
327
342
|
if (!usageResponse || typeof usageResponse !== "object") return
|
|
328
343
|
if (usageResponse.rate_limits_available === false) return
|
|
329
344
|
const rl = usageResponse.rate_limits
|
|
@@ -331,24 +346,25 @@ export function recordChatUsageSnapshot(usageResponse, now = Date.now()) {
|
|
|
331
346
|
const five = normalizeRateLimitWindow(rl.five_hour)
|
|
332
347
|
const seven = normalizeRateLimitWindow(rl.seven_day)
|
|
333
348
|
if (!five && !seven) return
|
|
334
|
-
|
|
349
|
+
const previous = chatUsageSnapshots.get(profileId)
|
|
350
|
+
chatUsageSnapshots.set(profileId, {
|
|
335
351
|
five_hour: five,
|
|
336
352
|
seven_day: seven,
|
|
337
|
-
plan: usageResponse.subscription_type ||
|
|
353
|
+
plan: usageResponse.subscription_type || previous?.plan || null,
|
|
338
354
|
updatedAtMs: now,
|
|
339
|
-
}
|
|
355
|
+
})
|
|
340
356
|
// P5(bug) と同じ直列化パターン (recordChatRateLimit 参照): 並行 persist の tmp 破壊/
|
|
341
357
|
// 中間状態読みを防ぐため、常に前回の persist 完了を待ってから次を実行する。
|
|
342
358
|
_chatUsagePersistInFlight = _chatUsagePersistInFlight.then(
|
|
343
|
-
() => persistChatUsageSnapshot(),
|
|
344
|
-
() => persistChatUsageSnapshot(),
|
|
359
|
+
() => persistChatUsageSnapshot(profileId),
|
|
360
|
+
() => persistChatUsageSnapshot(profileId),
|
|
345
361
|
)
|
|
346
362
|
}
|
|
347
363
|
|
|
348
|
-
async function persistChatUsageSnapshot() {
|
|
349
|
-
const snapshot =
|
|
364
|
+
async function persistChatUsageSnapshot(profileId) {
|
|
365
|
+
const snapshot = chatUsageSnapshots.get(profileId)
|
|
350
366
|
if (!snapshot) return
|
|
351
|
-
const p = chatUsageCachePath()
|
|
367
|
+
const p = chatUsageCachePath(profileId)
|
|
352
368
|
try {
|
|
353
369
|
await fs.mkdir(path.dirname(p), { recursive: true })
|
|
354
370
|
} catch {
|
|
@@ -373,13 +389,17 @@ export function whenChatUsageSnapshotPersisted() {
|
|
|
373
389
|
}
|
|
374
390
|
|
|
375
391
|
/** プロセス内メモリを優先し、無ければディスク永続化 (hub-agent 再起動直後等) を読む。 */
|
|
376
|
-
async function readChatUsageSnapshot() {
|
|
377
|
-
|
|
378
|
-
|
|
392
|
+
async function readChatUsageSnapshot(profileId = "default") {
|
|
393
|
+
const inMemory = chatUsageSnapshots.get(profileId)
|
|
394
|
+
if (inMemory) return inMemory
|
|
395
|
+
const text = await readOrNull(chatUsageCachePath(profileId))
|
|
379
396
|
if (!text) return null
|
|
380
397
|
try {
|
|
381
398
|
const j = JSON.parse(text)
|
|
382
|
-
if (j && typeof j === "object" && typeof j.updatedAtMs === "number")
|
|
399
|
+
if (j && typeof j === "object" && typeof j.updatedAtMs === "number") {
|
|
400
|
+
chatUsageSnapshots.set(profileId, j)
|
|
401
|
+
return j
|
|
402
|
+
}
|
|
383
403
|
} catch {
|
|
384
404
|
/* ignore */
|
|
385
405
|
}
|
|
@@ -388,7 +408,7 @@ async function readChatUsageSnapshot() {
|
|
|
388
408
|
|
|
389
409
|
/** テスト用: chatUsageSnapshot をクリアする。 */
|
|
390
410
|
export function _resetChatUsageSnapshot() {
|
|
391
|
-
|
|
411
|
+
chatUsageSnapshots = new Map()
|
|
392
412
|
}
|
|
393
413
|
|
|
394
414
|
async function readOfficial(now) {
|
|
@@ -1129,7 +1149,7 @@ export function _resetTurnActiveMemo() {
|
|
|
1129
1149
|
* context%: statusLine 値が最新 jsonl より新しければ official を優先。
|
|
1130
1150
|
* 古い / 無い (= チャットモードで statusLine 未更新) 場合は jsonl 推定で上書き。
|
|
1131
1151
|
*/
|
|
1132
|
-
export async function getUsage() {
|
|
1152
|
+
export async function getUsage({ profileId = "default" } = {}) {
|
|
1133
1153
|
const now = Date.now()
|
|
1134
1154
|
const official = await readOfficial(now)
|
|
1135
1155
|
const result = official || (await readEstimate(now))
|
|
@@ -1184,28 +1204,30 @@ export async function getUsage() {
|
|
|
1184
1204
|
// chatRateLimits (rate_limit_event) より本命の情報源 (utilization 欠落問題が無い、
|
|
1185
1205
|
// 2026-07 時点で実測確認済み)。statusLine / rate_limit_event のどちらよりも新しければ
|
|
1186
1206
|
// 上書きする。5h/7d が実際に埋まったときだけ source を official に昇格させる。
|
|
1187
|
-
const chatSnap = await readChatUsageSnapshot()
|
|
1207
|
+
const chatSnap = await readChatUsageSnapshot(profileId)
|
|
1188
1208
|
if (chatSnap && chatSnap.updatedAtMs > 0) {
|
|
1189
|
-
|
|
1190
|
-
//
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
percent: chatSnap.five_hour.percent,
|
|
1197
|
-
resetAtMs: chatSnap.five_hour.resetAtMs ?? result.last5h?.resetAtMs ?? null,
|
|
1198
|
-
}
|
|
1209
|
+
// statusLine/latest.json と rate_limit_event は profile を持たない旧経路。別 profile
|
|
1210
|
+
// の更新時刻がより新しくても、profile 専用 SDK snapshot を負かしてはならない。
|
|
1211
|
+
if (chatSnap.five_hour) {
|
|
1212
|
+
result.last5h = {
|
|
1213
|
+
...result.last5h,
|
|
1214
|
+
percent: chatSnap.five_hour.percent,
|
|
1215
|
+
resetAtMs: chatSnap.five_hour.resetAtMs ?? result.last5h?.resetAtMs ?? null,
|
|
1199
1216
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1217
|
+
}
|
|
1218
|
+
if (chatSnap.seven_day) {
|
|
1219
|
+
result.last7d = {
|
|
1220
|
+
...result.last7d,
|
|
1221
|
+
percent: chatSnap.seven_day.percent,
|
|
1222
|
+
resetAtMs: chatSnap.seven_day.resetAtMs ?? result.last7d?.resetAtMs ?? null,
|
|
1206
1223
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1224
|
+
}
|
|
1225
|
+
if (chatSnap.plan) result.plan = chatSnap.plan
|
|
1226
|
+
if (chatSnap.five_hour || chatSnap.seven_day) {
|
|
1227
|
+
result.source = "official"
|
|
1228
|
+
// スナップショットを再表示しただけでは「今取得した」ことにしない。フッターの
|
|
1229
|
+
// 最終更新時刻は実アカウント値を得た時刻を示す。
|
|
1230
|
+
result.computedAt = chatSnap.updatedAtMs
|
|
1209
1231
|
}
|
|
1210
1232
|
}
|
|
1211
1233
|
return result
|
|
@@ -1594,6 +1616,99 @@ export function classifyCodexRateLimitWindows(snapshot) {
|
|
|
1594
1616
|
return result
|
|
1595
1617
|
}
|
|
1596
1618
|
|
|
1619
|
+
/** App Server のアカウント利用制限レスポンスを Cockpit wire 形式へ正規化する。 */
|
|
1620
|
+
export function codexUsageFromAccountRateLimits(response) {
|
|
1621
|
+
const rateLimits = response?.rateLimits
|
|
1622
|
+
if (!rateLimits || typeof rateLimits !== "object") return null
|
|
1623
|
+
const { last5h, last7d } = classifyCodexRateLimitWindows(rateLimits)
|
|
1624
|
+
return {
|
|
1625
|
+
plan: rateLimits.planType || "codex",
|
|
1626
|
+
computedAt: Date.now(),
|
|
1627
|
+
source: "official",
|
|
1628
|
+
context: null,
|
|
1629
|
+
last5h,
|
|
1630
|
+
last7d,
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// Codex app-server の account/rateLimits/read はセッション不要だが、一時的な CLI 障害や
|
|
1635
|
+
// hub-agent 再起動直後には読めないことがある。最後に確認できた値を provider + profile
|
|
1636
|
+
// ごとに保存し、別アカウントの残量を表示しないようにする。
|
|
1637
|
+
function accountUsageCachePath(provider, profileId = "default") {
|
|
1638
|
+
const root = process.env.HUB_ACCOUNT_USAGE_CACHE_DIR || path.join(os.homedir(), ".hub", "usage", "accounts")
|
|
1639
|
+
return path.join(root, profileCacheKey(provider), `${profileCacheKey(profileId)}.json`)
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
const accountUsageSnapshots = new Map()
|
|
1643
|
+
let _accountUsagePersistInFlight = Promise.resolve()
|
|
1644
|
+
|
|
1645
|
+
function accountUsageSnapshotKey(provider, profileId) {
|
|
1646
|
+
return `${profileCacheKey(provider)}:${profileCacheKey(profileId)}`
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function isUsageSnapshot(value) {
|
|
1650
|
+
return (
|
|
1651
|
+
value &&
|
|
1652
|
+
typeof value === "object" &&
|
|
1653
|
+
typeof value.computedAt === "number" &&
|
|
1654
|
+
typeof value.source === "string" &&
|
|
1655
|
+
value.last5h &&
|
|
1656
|
+
value.last7d
|
|
1657
|
+
)
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/** 最後に取得できたアカウント利用量を profile 単位で永続化する。 */
|
|
1661
|
+
export function recordAccountUsageSnapshot(provider, profileId, usage) {
|
|
1662
|
+
if (!isUsageSnapshot(usage)) return
|
|
1663
|
+
const key = accountUsageSnapshotKey(provider, profileId)
|
|
1664
|
+
accountUsageSnapshots.set(key, usage)
|
|
1665
|
+
_accountUsagePersistInFlight = _accountUsagePersistInFlight.then(
|
|
1666
|
+
() => persistAccountUsageSnapshot(provider, profileId, usage),
|
|
1667
|
+
() => persistAccountUsageSnapshot(provider, profileId, usage),
|
|
1668
|
+
)
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
async function persistAccountUsageSnapshot(provider, profileId, usage) {
|
|
1672
|
+
const target = accountUsageCachePath(provider, profileId)
|
|
1673
|
+
try {
|
|
1674
|
+
await fs.mkdir(path.dirname(target), { recursive: true })
|
|
1675
|
+
const tmp = `${target}.tmp.${randomUUID()}`
|
|
1676
|
+
try {
|
|
1677
|
+
await fs.writeFile(tmp, JSON.stringify(usage))
|
|
1678
|
+
await fs.rename(tmp, target)
|
|
1679
|
+
} catch {
|
|
1680
|
+
await fs.unlink(tmp).catch(() => {})
|
|
1681
|
+
}
|
|
1682
|
+
} catch {
|
|
1683
|
+
// 表示用キャッシュの書込み失敗は利用セッションを妨げない。
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/** メモリ優先、hub-agent 再起動後は provider/profile 専用のディスク値を返す。 */
|
|
1688
|
+
export async function readAccountUsageSnapshot(provider, profileId = "default") {
|
|
1689
|
+
const key = accountUsageSnapshotKey(provider, profileId)
|
|
1690
|
+
const inMemory = accountUsageSnapshots.get(key)
|
|
1691
|
+
if (inMemory) return inMemory
|
|
1692
|
+
const text = await readOrNull(accountUsageCachePath(provider, profileId))
|
|
1693
|
+
if (!text) return null
|
|
1694
|
+
try {
|
|
1695
|
+
const parsed = JSON.parse(text)
|
|
1696
|
+
if (!isUsageSnapshot(parsed)) return null
|
|
1697
|
+
accountUsageSnapshots.set(key, parsed)
|
|
1698
|
+
return parsed
|
|
1699
|
+
} catch {
|
|
1700
|
+
return null
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
export function whenAccountUsageSnapshotPersisted() {
|
|
1705
|
+
return _accountUsagePersistInFlight
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
export function _resetAccountUsageSnapshots() {
|
|
1709
|
+
accountUsageSnapshots.clear()
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1597
1712
|
// ---------------------------------------------------------------------------
|
|
1598
1713
|
// Codex context%(ドーナツ)の live source(T06407 増分2, 症状⑥ ドーナツ前後動の根治)
|
|
1599
1714
|
//
|