@cocorograph/hub-agent 0.7.94 → 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 +32 -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"
|
|
@@ -5632,6 +5635,10 @@ async function dispatch(msg, ctx) {
|
|
|
5632
5635
|
}
|
|
5633
5636
|
case "usage.request": {
|
|
5634
5637
|
try {
|
|
5638
|
+
const activeClaudeProfile = await getActiveProfile()
|
|
5639
|
+
const claudeHasCredentials = isKeychainPlatform()
|
|
5640
|
+
? await profileHasKeychainCredentials(activeClaudeProfile.configDir)
|
|
5641
|
+
: await hasFileCredentials(activeClaudeProfile.configDir)
|
|
5635
5642
|
// 5h/7d の本命ソース: SDK control request (アカウント全体、session 非依存)。
|
|
5636
5643
|
// statusLine が rate_limits を出さなくなった環境 (2026-07 時点の Claude CLI) でも
|
|
5637
5644
|
// ここでライブ更新できる。frontend の usage.request ポーリング (既定 60s) 頻度に
|
|
@@ -5641,15 +5648,20 @@ async function dispatch(msg, ctx) {
|
|
|
5641
5648
|
if (ctx.claudeBridge) {
|
|
5642
5649
|
try {
|
|
5643
5650
|
const acct = await ctx.claudeBridge.getAccountUsage()
|
|
5644
|
-
if (acct) recordChatUsageSnapshot(acct)
|
|
5651
|
+
if (acct) recordChatUsageSnapshot(acct, Date.now(), activeClaudeProfile.id)
|
|
5645
5652
|
} catch (err) {
|
|
5646
5653
|
ctx.logger?.debug?.({ err: err?.message }, "account usage snapshot skipped")
|
|
5647
5654
|
}
|
|
5648
5655
|
}
|
|
5649
|
-
|
|
5650
|
-
getUsage(),
|
|
5656
|
+
let [usage, sessions] = await Promise.all([
|
|
5657
|
+
getUsage({ profileId: activeClaudeProfile.id }),
|
|
5651
5658
|
getSessionUsages(),
|
|
5652
5659
|
])
|
|
5660
|
+
// 未ログインで残ったローカル transcript/cache をアカウント利用量として見せない。
|
|
5661
|
+
if (!claudeHasCredentials) {
|
|
5662
|
+
usage = null
|
|
5663
|
+
sessions = []
|
|
5664
|
+
}
|
|
5653
5665
|
|
|
5654
5666
|
// Codex 版 (Stream B3, 2026-07-03): rollout jsonl の token_count イベントから
|
|
5655
5667
|
// 5h/7d + context% を取る。⚠️ Codex の 5h/7d は ChatGPT/OpenAI サブスクリプション
|
|
@@ -5658,7 +5670,9 @@ async function dispatch(msg, ctx) {
|
|
|
5658
5670
|
// として返す (frontend 側でどちらのアカウントの残量か混同しないため)。
|
|
5659
5671
|
let codexUsage = null
|
|
5660
5672
|
let codexSessions = []
|
|
5661
|
-
|
|
5673
|
+
const activeCodexProfile = await codexProfiles.getActiveProfile()
|
|
5674
|
+
const codexHasCredentials = await codexProfiles.hasCodexCredentials(activeCodexProfile.configDir)
|
|
5675
|
+
if (ctx.codexBridge && codexHasCredentials) {
|
|
5662
5676
|
try {
|
|
5663
5677
|
// T06407 増分2 (axis2): per-cwd の context% は **cwd ごとに権威 producer 1 件**へ
|
|
5664
5678
|
// 集約する。旧実装は全 live session を codexSessions へ流していたため、同一 cwd に
|
|
@@ -5708,10 +5722,24 @@ async function dispatch(msg, ctx) {
|
|
|
5708
5722
|
codexUsage = { ...codexUsage, context: null }
|
|
5709
5723
|
}
|
|
5710
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
|
+
}
|
|
5711
5734
|
} catch (err) {
|
|
5712
5735
|
ctx.logger?.debug?.({ err: err?.message }, "codex usage snapshot skipped")
|
|
5713
5736
|
}
|
|
5714
5737
|
}
|
|
5738
|
+
// Agent 再起動後や一時的な account RPC 失敗時も、同一 profile の最終取得値だけを
|
|
5739
|
+
// 表示する。未ログインならここへ到達せず、表示も完全に隠す。
|
|
5740
|
+
if (codexHasCredentials && !codexUsage) {
|
|
5741
|
+
codexUsage = await readAccountUsageSnapshot("codex", activeCodexProfile.id)
|
|
5742
|
+
}
|
|
5715
5743
|
|
|
5716
5744
|
ctx.client.send({
|
|
5717
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
|
//
|