@cocorograph/hub-agent 0.7.83 → 0.7.85
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/claude-stream-bridge.mjs +154 -66
- package/src/usage.mjs +117 -17
package/package.json
CHANGED
|
@@ -65,22 +65,24 @@ const CHAT_RESIDENT_ENABLED = process.env.HUB_AGENT_CHAT_RESIDENT !== "0"
|
|
|
65
65
|
* 1 セッション=1 query で system/init を初回 1 回のみにする。VS Code 拡張 / Claude Web
|
|
66
66
|
* と同じ挙動)。query 起動時に options.resume で過去文脈を引き継ぐ。
|
|
67
67
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
68
|
+
* ✅ 既定 ON へ復帰 (2026-07-21, T06118)。旧既定 OFF の理由だった「resume + 空 InputQueue」
|
|
69
|
+
* 窓 (下記) を **push 先行起動** (_startResidentTurnWithInput) で塞いだため、resumed セッション
|
|
70
|
+
* も常駐化して安全になった。既定 ON にする狙いは **背景エージェントの信頼性回復**:
|
|
71
|
+
* per-message (resume=1query) だとターン結果が返ると query プロセスが畳まれ、run_in_background
|
|
72
|
+
* で起動した子エージェントが親ターンより長生きして「previous Claude Code process exited」で
|
|
73
|
+
* 孤児化・停止していた (実害)。常駐化すれば query プロセスがターンをまたいで生存する。
|
|
72
74
|
*
|
|
73
|
-
*
|
|
74
|
-
* push**
|
|
75
|
-
*
|
|
76
|
-
* InputQueue に push→即 close
|
|
77
|
-
*
|
|
75
|
+
* 旧回帰の根本原因 (履歴): resident 経路 (sendMessage) が **空の InputQueue のまま query を
|
|
76
|
+
* 起動してから push** していたため、「resume + 入力がまだ空」の窓で SDK が中断/未完了
|
|
77
|
+
* セッションの自動継続ターン (Continue from where you left off) を差し込んでいた。per-message
|
|
78
|
+
* 経路は InputQueue に push→即 close してから起動するためこの窓が無い (0.6.4 で暴走を潰した
|
|
79
|
+
* 設計)。今回 resident の 3 起動サイトも「push してから起動」に揃え、初回ターンの ultracode は
|
|
80
|
+
* 起動時 options.settings で焼き込む (applyFlagSettings のための空窓も作らない)。
|
|
78
81
|
*
|
|
79
|
-
* env HUB_AGENT_CHAT_RESIDENT_RESUME="
|
|
80
|
-
*
|
|
81
|
-
* 独立制御 (新規は resume しないため自動継続の窓が無く、既定 ON のまま安全)。 */
|
|
82
|
+
* env HUB_AGENT_CHAT_RESIDENT_RESUME="0" で個別に無効化 (従来の per-message へロールバック)
|
|
83
|
+
* できる。新規セッションの常駐化は CHAT_RESIDENT_ENABLED 側で独立制御。 */
|
|
82
84
|
const CHAT_RESIDENT_RESUME_ENABLED =
|
|
83
|
-
process.env.HUB_AGENT_CHAT_RESIDENT_RESUME
|
|
85
|
+
process.env.HUB_AGENT_CHAT_RESIDENT_RESUME !== "0"
|
|
84
86
|
|
|
85
87
|
/** MCP control 専用 query のアイドル破棄時間 (2026-06-03)。MCP サーバーは Claude Code
|
|
86
88
|
* 設定 (アカウント単位) で全セッション共有のため、チャットセッションとは独立した
|
|
@@ -435,6 +437,20 @@ class ClaudeStreamSession {
|
|
|
435
437
|
* false に戻す。 */
|
|
436
438
|
this._ultracodeCurrent = false
|
|
437
439
|
|
|
440
|
+
/** 空 InputQueue 窓封鎖 (T06118): fresh start する resume 常駐 query の「初回ターンの
|
|
441
|
+
* ultracode 目標」。_startResidentTurnWithInput が push 先行で積むメッセージと一緒に
|
|
442
|
+
* セットし、_runResidentQuery が起動時 options.settings へ焼き込んで消費する
|
|
443
|
+
* (起動前 applyFlagSettings のための空 InputQueue 窓を作らないため)。 */
|
|
444
|
+
this._pendingInitialUltracode = false
|
|
445
|
+
|
|
446
|
+
/** ABA/FIFO 直列化 (T06118 codex-review major): 起動済み query 経路の sendMessage が
|
|
447
|
+
* reconcile (applyFlagSettings) を await している間 true。この間に旧 query の generator が
|
|
448
|
+
* 自然終了しても、_runResidentQuery の finally は pending を drain して新 query を先行起動
|
|
449
|
+
* してはならない (in-flight のこの送信が後着 pending に追い越されて FIFO が逆転するため)。
|
|
450
|
+
* finally は query を畳むだけに留め、await 復帰後の送信が自分のメッセージを push 先行で
|
|
451
|
+
* fresh-start する。pending はその turn settle 後に FIFO で drain される。 */
|
|
452
|
+
this._residentSendInFlight = false
|
|
453
|
+
|
|
438
454
|
/** @type {Map<string, {resolve: (decision: object) => void, tool_name: string, input: object}>}
|
|
439
455
|
* permission 応答待ち。tool_name / input も保持し、reattach 時に未応答分を
|
|
440
456
|
* onPermission で再送 (replay) して「裏で動いていたセッションを開いたら確認待ち
|
|
@@ -915,15 +931,6 @@ class ClaudeStreamSession {
|
|
|
915
931
|
|
|
916
932
|
// 改修2+4: 常駐query対象セッション。
|
|
917
933
|
if (this._residentEligible) {
|
|
918
|
-
// B6: 死亡後の (再)起動経路。既に query を起動した実績があり (_residentStarted)、かつ
|
|
919
|
-
// 現在 query が無い (_residentQuery=null = 異常終了済み) 場合、既存 _inputQueue は前 query で
|
|
920
|
-
// consume し切った (generator return 済み) インスタンス。同じものを次の sdk.query に渡すと
|
|
921
|
-
// _q/_wake の残留が新 generator と競合し得るため、push 前に必ず作り直す。
|
|
922
|
-
if (this._residentStarted && !this._residentQuery) {
|
|
923
|
-
this._inputQueue = new InputQueue()
|
|
924
|
-
} else if (!this._inputQueue) {
|
|
925
|
-
this._inputQueue = new InputQueue()
|
|
926
|
-
}
|
|
927
934
|
// 改修4 (A): ターンのシリアライズ。実行中ターン (busy) は InputQueue へ即 push せず
|
|
928
935
|
// pending へ退避し、現ターンの result 後に 1 件ずつ drain する。前ターン未完了のまま
|
|
929
936
|
// 次を push すると SDK streaming-input で割り込み扱いになり得るため (公式 interrupt 警告)。
|
|
@@ -937,31 +944,29 @@ class ClaudeStreamSession {
|
|
|
937
944
|
this._emitQueueState()
|
|
938
945
|
return { accepted: true }
|
|
939
946
|
}
|
|
947
|
+
if (this._closed) return { accepted: false, reason: "queue_dead" }
|
|
940
948
|
this._beginResidentTurn()
|
|
941
|
-
//
|
|
942
|
-
//
|
|
943
|
-
//
|
|
944
|
-
//
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
// A-2 修正 (2026-07-05): 上の await 中に常駐 query の generator が自然終了している
|
|
953
|
-
// ことがある (_residentQueryReturned が finally で true になる)。その場合 this._residentQuery
|
|
954
|
-
// はまだ非 null (finally 未実行) に見えても、実体は既に誰も consume しない返却済み
|
|
955
|
-
// generator であり、そのまま push すると消失する。fresh-start と同じ手順 (InputQueue の
|
|
956
|
-
// 再生成 + 再起動) を踏んでから push することで、既存の resident 復活ガード
|
|
957
|
-
// (B6 / 異常終了時の自動再起動) に合流させる。
|
|
958
|
-
if (this._residentQueryReturned || !this._residentQuery) {
|
|
959
|
-
this._inputQueue = new InputQueue()
|
|
960
|
-
this._residentStarted = true
|
|
961
|
-
this._startResidentQuery()
|
|
949
|
+
// 空 InputQueue 窓封鎖 (T06118): fresh start。常駐 query が未起動 (初回) / 異常終了済み
|
|
950
|
+
// (_residentQuery=null = 死亡ガード B6) / generator 返却済み (_residentQueryReturned) の
|
|
951
|
+
// いずれも「新しい query を起動する」ケース。従来は空の InputQueue で起動してから push
|
|
952
|
+
// していたため「resume + 空入力」の窓で SDK が自動継続ターン (Continue from where you
|
|
953
|
+
// left off) を差し込む回帰があった。実メッセージを積んでから起動する push 先行
|
|
954
|
+
// (_startResidentTurnWithInput、per-message と同じ) に統一してこの窓を無くす。
|
|
955
|
+
// 初回ターンの ultracode は起動時 options.settings に焼き込むため、起動前 reconcile
|
|
956
|
+
// (applyFlagSettings) のための空窓も不要。
|
|
957
|
+
if (!this._residentQuery || this._residentQueryReturned) {
|
|
958
|
+
this._startResidentTurnWithInput(toSDKUserMessage(prompt), ultracode)
|
|
959
|
+
return { accepted: true }
|
|
962
960
|
}
|
|
963
|
-
|
|
964
|
-
|
|
961
|
+
// 起動済みの常駐 query へ次ターンを流す。init 済みで空窓が無いため、ultracode は走行中
|
|
962
|
+
// query への applyFlagSettings (reconcile) で適用してから push する。reconcile await 中の
|
|
963
|
+
// 旧 query 終了 × 後着 pending の ABA/FIFO 逆転は _reconcileThenDeliver が直列化する
|
|
964
|
+
// (drain 経路 _drainResidentPending と共通)。
|
|
965
|
+
const outcome = await this._reconcileThenDeliver(
|
|
966
|
+
toSDKUserMessage(prompt),
|
|
967
|
+
ultracode,
|
|
968
|
+
)
|
|
969
|
+
if (outcome === "closed") return { accepted: false, reason: "queue_dead" }
|
|
965
970
|
return { accepted: true }
|
|
966
971
|
}
|
|
967
972
|
|
|
@@ -1244,6 +1249,24 @@ class ClaudeStreamSession {
|
|
|
1244
1249
|
}
|
|
1245
1250
|
}
|
|
1246
1251
|
|
|
1252
|
+
/** 空 InputQueue 窓封鎖 (T06118): fresh start する resume 常駐 query を「入力を先に積んで
|
|
1253
|
+
* から起動」する (push 先行)。fresh な InputQueue を作り、実ユーザーメッセージを push して
|
|
1254
|
+
* から query を起動するため、「resume + 空入力」の窓で SDK が自動継続ターン (Continue from
|
|
1255
|
+
* where you left off) を差し込む余地が無い (per-message の push→close→起動と同じ原理)。
|
|
1256
|
+
* 初回ターンの ultracode は options.settings 経由で焼き込む (_runResidentQuery が
|
|
1257
|
+
* _pendingInitialUltracode を読む) ため、起動前 applyFlagSettings のための空窓も作らない。
|
|
1258
|
+
* ⚠️ _inputQueue は必ず作り直す (B6): 前 query で consume し切った (generator return 済み)
|
|
1259
|
+
* インスタンスを次の sdk.query に渡すと _q/_wake の残留が新 generator と競合し得るため。
|
|
1260
|
+
* @param {object} sdkUserMessage toSDKUserMessage() 済みの初回ユーザーメッセージ
|
|
1261
|
+
* @param {boolean} initialUltracode 初回ターンの ultracode 目標 */
|
|
1262
|
+
_startResidentTurnWithInput(sdkUserMessage, initialUltracode) {
|
|
1263
|
+
this._inputQueue = new InputQueue()
|
|
1264
|
+
this._inputQueue.push(sdkUserMessage)
|
|
1265
|
+
this._pendingInitialUltracode = initialUltracode === true
|
|
1266
|
+
this._residentStarted = true
|
|
1267
|
+
this._startResidentQuery()
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1247
1270
|
/** 改修4 (A): 常駐 query を resume 付きで (再)起動する。_runResidentQuery 起動時に
|
|
1248
1271
|
* options.resume=this.sessionId で過去文脈を復元するため、途中死からの復活でも安全。 */
|
|
1249
1272
|
_startResidentQuery() {
|
|
@@ -1282,6 +1305,45 @@ class ClaudeStreamSession {
|
|
|
1282
1305
|
}
|
|
1283
1306
|
}
|
|
1284
1307
|
|
|
1308
|
+
/** ABA/FIFO 直列化の共通デリバリ (T06118 codex-review major×2)。起動済みの走行中 query へ
|
|
1309
|
+
* 「ultracode を reconcile してから 1 メッセージを届ける」処理を、直接送信経路 (sendMessage) と
|
|
1310
|
+
* drain 経路 (_drainResidentPending) の両方から使う共通化ポイント。reconcile は control request
|
|
1311
|
+
* の往復で await するため、その窓の間に旧 query の generator が自然終了しうる (finally が走る)。
|
|
1312
|
+
* 所有権ガード (_residentSendInFlight) で await 中の finally に pending drain を見送らせ、await
|
|
1313
|
+
* 前後の query ハンドル比較 (ABA 検出) で交代/終了を捕捉して push 先行 fresh-start へ切り替える。
|
|
1314
|
+
*
|
|
1315
|
+
* 前提: 呼び出し側が既に _beginResidentTurn() 済みで、呼び出し時点の _residentQuery が走行中
|
|
1316
|
+
* (非 null・未返却) であること。
|
|
1317
|
+
* @param {object} sdkMsg toSDKUserMessage() 済みメッセージ
|
|
1318
|
+
* @param {boolean} ultracode このターンの ultracode 目標
|
|
1319
|
+
* @returns {Promise<"pushed"|"restarted"|"closed">} 届け方の結果
|
|
1320
|
+
*/
|
|
1321
|
+
async _reconcileThenDeliver(sdkMsg, ultracode) {
|
|
1322
|
+
const activeQuery = this._residentQuery
|
|
1323
|
+
this._residentSendInFlight = true
|
|
1324
|
+
try {
|
|
1325
|
+
await this._reconcileResidentUltracode(ultracode)
|
|
1326
|
+
} finally {
|
|
1327
|
+
this._residentSendInFlight = false
|
|
1328
|
+
}
|
|
1329
|
+
if (this._closed) return "closed"
|
|
1330
|
+
if (
|
|
1331
|
+
this._residentQueryReturned ||
|
|
1332
|
+
!this._residentQuery ||
|
|
1333
|
+
this._residentQuery !== activeQuery
|
|
1334
|
+
) {
|
|
1335
|
+
// ABA 検出: await 中に query が終了/交代した。finally は _residentSendInFlight を見て
|
|
1336
|
+
// pending drain を見送っているので、自分のメッセージを push 先行で fresh-start する
|
|
1337
|
+
// (pending はこの turn settle 後に _drainResidentPending が FIFO で流す)。await 中の
|
|
1338
|
+
// finally で _busy が false へ落ちているため、_beginResidentTurn で張り直してから起動する。
|
|
1339
|
+
this._beginResidentTurn()
|
|
1340
|
+
this._startResidentTurnWithInput(sdkMsg, ultracode)
|
|
1341
|
+
return "restarted"
|
|
1342
|
+
}
|
|
1343
|
+
this._inputQueue.push(sdkMsg)
|
|
1344
|
+
return "pushed"
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1285
1347
|
/** SDK-P0-3 (2026-07-10): resident の「ターン開始」を 1 箇所に集約する。
|
|
1286
1348
|
* _busy を立てるのと同時に、そのターンの終端フラグ (_turnSettled) と進行フラグ
|
|
1287
1349
|
* (_turnProgressed) を必ずリセットする。ターン開始は 3 箇所ある
|
|
@@ -1368,10 +1430,18 @@ class ClaudeStreamSession {
|
|
|
1368
1430
|
// 次の drain がやり直せる状態に必ず回復する (例外は既存方針通り warn で握る)。
|
|
1369
1431
|
this._beginResidentTurn()
|
|
1370
1432
|
try {
|
|
1371
|
-
//
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1433
|
+
// ABA/FIFO 直列化 (T06118 codex-review major×2): drain も reconcile を await するため、
|
|
1434
|
+
// その窓で旧 query が終了して finally が「その先の pending」を新 query へ先行投入すると、
|
|
1435
|
+
// 今 drain 中の next が追い越されて FIFO が逆転する。直接送信経路と同じ _reconcileThenDeliver
|
|
1436
|
+
// を通し、_residentSendInFlight による finally の drain 見送り + ABA 検出で next を先に届ける。
|
|
1437
|
+
const outcome = await this._reconcileThenDeliver(
|
|
1438
|
+
toSDKUserMessage(next.text),
|
|
1439
|
+
next.ultracode === true,
|
|
1440
|
+
)
|
|
1441
|
+
// codex-review minor: session が close 中 (outcome==="closed") は next を配送していない。
|
|
1442
|
+
// started 通知 (queue_state に [next.text] を載せる) を出すと未配送メッセージが「開始済み」に
|
|
1443
|
+
// 見えるため抑止する。closed は終端状態なので pending へ戻さず破棄でよい (session 撤去中)。
|
|
1444
|
+
if (outcome !== "closed") this._emitQueueState([next.text])
|
|
1375
1445
|
} catch (err) {
|
|
1376
1446
|
this._busy = false
|
|
1377
1447
|
this._pendingMessages.unshift(next)
|
|
@@ -1421,6 +1491,20 @@ class ClaudeStreamSession {
|
|
|
1421
1491
|
// 改修4: 起動時に sessionId (= resumeSessionId) があれば resume チェーンで文脈を引き継ぐ。
|
|
1422
1492
|
// query 起動時点の値のみ有効 (起動後に確定/変化する session_id は同一 query 内で継続される)。
|
|
1423
1493
|
if (this.sessionId) options.resume = this.sessionId
|
|
1494
|
+
// 空 InputQueue 窓封鎖 (T06118): fresh start の初回ターンが ultracode を要求していれば、
|
|
1495
|
+
// per-message と同じく起動時 options.settings に焼き込む。こうすることで「query を空 queue
|
|
1496
|
+
// で先に起動 → applyFlagSettings を await → push」という空窓経路を踏まずに済む。初回だけ
|
|
1497
|
+
// 焼き込み、_ultracodeCurrent を true に揃えて次ターンの reconcile が正しく差分判定できる
|
|
1498
|
+
// ようにする。読み取り後は消費 (次の (再)起動へ持ち越さない)。
|
|
1499
|
+
if (this._pendingInitialUltracode === true) {
|
|
1500
|
+
options.settings = {
|
|
1501
|
+
...(options.settings || {}),
|
|
1502
|
+
ultracode: true,
|
|
1503
|
+
enableWorkflows: true,
|
|
1504
|
+
}
|
|
1505
|
+
this._ultracodeCurrent = true
|
|
1506
|
+
}
|
|
1507
|
+
this._pendingInitialUltracode = false
|
|
1424
1508
|
const denyPending = (reason) => this._denyAllPendingPermissions(reason)
|
|
1425
1509
|
try {
|
|
1426
1510
|
const q = this.sdk.query({ prompt: this._inputQueue, options })
|
|
@@ -1514,27 +1598,31 @@ class ClaudeStreamSession {
|
|
|
1514
1598
|
} finally {
|
|
1515
1599
|
this.onReap?.()
|
|
1516
1600
|
}
|
|
1517
|
-
} else if (
|
|
1601
|
+
} else if (
|
|
1602
|
+
!this._closed &&
|
|
1603
|
+
this._pendingMessages.length > 0 &&
|
|
1604
|
+
!this._residentSendInFlight
|
|
1605
|
+
) {
|
|
1518
1606
|
// 改修4 (A): 異常終了 (close 以外) で pending が残っていれば resume 付きで再起動し
|
|
1519
1607
|
// 取りこぼしを防ぐ。_runResidentQuery が options.resume=this.sessionId で文脈を復元する。
|
|
1520
|
-
//
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
|
|
1608
|
+
// 空 InputQueue 窓封鎖 (T06118): ここも fresh start なので push 先行で起動する
|
|
1609
|
+
// (_startResidentTurnWithInput が fresh な InputQueue を作り、実メッセージを積んでから
|
|
1610
|
+
// 起動する)。従来は「空 queue で起動 → reconcile await → push」で resume + 空入力の
|
|
1611
|
+
// 自動継続窓があった。異常再起動対象メッセージの ultracode は options.settings 経由で
|
|
1612
|
+
// 焼き込むため、フラグ取りこぼし (0.6.28 由来) も起きない。
|
|
1613
|
+
//
|
|
1614
|
+
// ABA/FIFO 直列化 (T06118 codex-review major): _residentSendInFlight の間 (起動済み
|
|
1615
|
+
// query 経路の sendMessage が reconcile を await 中) は、ここで pending を drain して
|
|
1616
|
+
// 新 query を先行起動してはならない。in-flight の送信が後着 pending に追い越されて FIFO が
|
|
1617
|
+
// 逆転するため。この場合は query を畳むだけに留め (上で _residentQuery=null 済み)、
|
|
1618
|
+
// await 復帰後の送信が自分のメッセージを push 先行で fresh-start する。pending はその
|
|
1619
|
+
// turn settle 後に _drainResidentPending が FIFO で流す。
|
|
1525
1620
|
const next = this._pendingMessages.shift()
|
|
1526
1621
|
this._beginResidentTurn()
|
|
1527
|
-
this.
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
// メッセージだけ ultracode フラグが落ちる (0.6.28 由来の取りこぼし) ため、ここで揃える。
|
|
1532
|
-
// _startResidentQuery → _runResidentQuery は最初の await (for await) より前に
|
|
1533
|
-
// 同期で _residentQuery と _ultracodeCurrent=false を確定するので、直後の reconcile が
|
|
1534
|
-
// 正しく差分判定できる。_reconcileResidentUltracode は内部で例外を握るため throw しない。
|
|
1535
|
-
this._startResidentQuery()
|
|
1536
|
-
await this._reconcileResidentUltracode(next.ultracode === true)
|
|
1537
|
-
this._inputQueue.push(toSDKUserMessage(next.text))
|
|
1622
|
+
this._startResidentTurnWithInput(
|
|
1623
|
+
toSDKUserMessage(next.text),
|
|
1624
|
+
next.ultracode === true,
|
|
1625
|
+
)
|
|
1538
1626
|
this._emitQueueState([next.text])
|
|
1539
1627
|
}
|
|
1540
1628
|
// マスク (2026-06-23): 常駐 query 終了後 (= CLI が jsonl を掴んでいない) のこの地点で、
|
package/src/usage.mjs
CHANGED
|
@@ -1483,6 +1483,117 @@ export function _resetCodexTokenCountMemo() {
|
|
|
1483
1483
|
_codexTokenCountMemo.clear()
|
|
1484
1484
|
}
|
|
1485
1485
|
|
|
1486
|
+
/**
|
|
1487
|
+
* Codex rate limit の窓分類定数 (T06407)。5h 窓 = 300 分、7d 窓 = 10080 分。
|
|
1488
|
+
* ⚠️ 位置 (primary/secondary) は 5h/7d を保証しない。実測 (0.144.x / pro プラン) では
|
|
1489
|
+
* primary が windowDurationMins=10080 (=7日)・secondary=null を返す。よって窓の分類は
|
|
1490
|
+
* **duration で行う** (症状⑦「7d が進まない」の根治)。閾値は 5h と 7d の中点 (~5190 分)。
|
|
1491
|
+
*/
|
|
1492
|
+
const CODEX_5H_WINDOW_MINS = 300
|
|
1493
|
+
const CODEX_7D_WINDOW_MINS = 10080
|
|
1494
|
+
/** 分類スロットの正準窓長。窓は「最も近い正準窓」のスロットへ割り当てる(中点固定分割だと
|
|
1495
|
+
* 将来 1h+5h のような同帯 2 窓で実 5h を取りこぼす。codex-review minor 対応)。 */
|
|
1496
|
+
const CODEX_RATE_LIMIT_SLOTS = [
|
|
1497
|
+
{ key: "last5h", canonicalMins: CODEX_5H_WINDOW_MINS },
|
|
1498
|
+
{ key: "last7d", canonicalMins: CODEX_7D_WINDOW_MINS },
|
|
1499
|
+
]
|
|
1500
|
+
|
|
1501
|
+
/** RateLimitWindow (App Server = camelCase / rollout jsonl = snake_case の両対応) から
|
|
1502
|
+
* used% を読む。値が無ければ null (= その窓は「未提供」)。 */
|
|
1503
|
+
function codexWindowUsedPercent(w) {
|
|
1504
|
+
if (!w) return null
|
|
1505
|
+
if (typeof w.usedPercent === "number") return w.usedPercent
|
|
1506
|
+
if (typeof w.used_percent === "number") return w.used_percent
|
|
1507
|
+
return null
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/** RateLimitWindow の窓長 (分)。camelCase/snake_case 両対応。不明なら null。 */
|
|
1511
|
+
function codexWindowMins(w) {
|
|
1512
|
+
if (!w) return null
|
|
1513
|
+
if (typeof w.windowDurationMins === "number") return w.windowDurationMins
|
|
1514
|
+
if (typeof w.window_minutes === "number") return w.window_minutes
|
|
1515
|
+
return null
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
/** RateLimitWindow の resets 時刻 (ms)。camelCase/snake_case 両対応。秒→ms 補正あり。 */
|
|
1519
|
+
function codexWindowResetMs(w) {
|
|
1520
|
+
if (!w) return null
|
|
1521
|
+
const raw =
|
|
1522
|
+
typeof w.resetsAt === "number"
|
|
1523
|
+
? w.resetsAt
|
|
1524
|
+
: typeof w.resets_at === "number"
|
|
1525
|
+
? w.resets_at
|
|
1526
|
+
: null
|
|
1527
|
+
if (typeof raw !== "number") return null
|
|
1528
|
+
return raw < 1e12 ? raw * 1000 : raw
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/** 「未提供」bucket。欠落窓を 0% と誤表示せず、frontend が「未提供」と描画できるよう
|
|
1532
|
+
* provided:false / percent:null を返す (fail-soft で沈黙させない)。 */
|
|
1533
|
+
function codexNotProvidedBucket() {
|
|
1534
|
+
return {
|
|
1535
|
+
tokens: 0,
|
|
1536
|
+
messages: 0,
|
|
1537
|
+
limit: CODEX_DEFAULT_LIMIT,
|
|
1538
|
+
percent: null,
|
|
1539
|
+
provided: false,
|
|
1540
|
+
windowMinutes: null,
|
|
1541
|
+
oldestTsMs: null,
|
|
1542
|
+
resetAtMs: null,
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/**
|
|
1547
|
+
* RateLimitSnapshot (App Server の `account/rateLimits/read`/`updated` または rollout の
|
|
1548
|
+
* `rate_limits`、両 casing) を **窓長で 5h/7d スロットへ分類**する (T06407)。
|
|
1549
|
+
*
|
|
1550
|
+
* - primary/secondary の**位置に依存しない**。各窓の windowDurationMins/window_minutes を見て、
|
|
1551
|
+
* **正準窓長に最も近いスロット**へ割り当てる (5h=300 分 / 7d=10080 分)。
|
|
1552
|
+
* - 提供されない窓 (null / used% 欠落 / duration 不明) は 0% ではなく「未提供」で返す。
|
|
1553
|
+
* - 同一スロットに複数窓が来たら、そのスロットの正準窓長に**より近い**窓を採用する
|
|
1554
|
+
* (先着でなく最近傍。将来 1h+5h のような同帯 2 窓でも実 5h を取りこぼさない)。
|
|
1555
|
+
*
|
|
1556
|
+
* @param {object|null} snapshot RateLimitSnapshot 相当 ({ primary, secondary, ... })
|
|
1557
|
+
* @returns {{ last5h: object, last7d: object }} 各 bucket は provided:true|false を持つ
|
|
1558
|
+
*/
|
|
1559
|
+
export function classifyCodexRateLimitWindows(snapshot) {
|
|
1560
|
+
const result = { last5h: codexNotProvidedBucket(), last7d: codexNotProvidedBucket() }
|
|
1561
|
+
if (!snapshot) return result
|
|
1562
|
+
// 各スロットに「これまで割り当てた窓の正準窓長からの距離」を持ち、より近い窓で上書きする。
|
|
1563
|
+
const bestDist = { last5h: Infinity, last7d: Infinity }
|
|
1564
|
+
for (const w of [snapshot.primary, snapshot.secondary]) {
|
|
1565
|
+
const usedPercent = codexWindowUsedPercent(w)
|
|
1566
|
+
if (usedPercent === null) continue
|
|
1567
|
+
const mins = codexWindowMins(w)
|
|
1568
|
+
// duration 不明は分類できない → 未提供のままにする (位置で 5h/7d に決め打ちしない)。
|
|
1569
|
+
if (mins === null) continue
|
|
1570
|
+
// 最も近い正準窓のスロットへ割り当てる。
|
|
1571
|
+
let slot = null
|
|
1572
|
+
let slotDist = Infinity
|
|
1573
|
+
for (const s of CODEX_RATE_LIMIT_SLOTS) {
|
|
1574
|
+
const d = Math.abs(mins - s.canonicalMins)
|
|
1575
|
+
if (d < slotDist) {
|
|
1576
|
+
slotDist = d
|
|
1577
|
+
slot = s.key
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
// 同一スロットに複数窓が来たら、正準窓長により近い方を採用 (先着でなく最近傍)。
|
|
1581
|
+
if (slotDist >= bestDist[slot]) continue
|
|
1582
|
+
bestDist[slot] = slotDist
|
|
1583
|
+
result[slot] = {
|
|
1584
|
+
tokens: 0,
|
|
1585
|
+
messages: 0,
|
|
1586
|
+
limit: CODEX_DEFAULT_LIMIT,
|
|
1587
|
+
percent: usedPercent,
|
|
1588
|
+
provided: true,
|
|
1589
|
+
windowMinutes: mins,
|
|
1590
|
+
oldestTsMs: null,
|
|
1591
|
+
resetAtMs: codexWindowResetMs(w),
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
return result
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1486
1597
|
/**
|
|
1487
1598
|
* 指定 threadId の Codex セッションから usage (5h/7d + context%) を返す。
|
|
1488
1599
|
* Claude の `getUsage()` と同じ shape (`UsageStats`) を返すため、frontend / wire
|
|
@@ -1525,29 +1636,18 @@ export async function codexUsageForThread(threadId) {
|
|
|
1525
1636
|
contextPercent = Math.min(100, Math.round((tokens / windowSize) * 1000) / 10)
|
|
1526
1637
|
}
|
|
1527
1638
|
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
resetMs = w.resets_at < 1e12 ? w.resets_at * 1000 : w.resets_at
|
|
1533
|
-
}
|
|
1534
|
-
return {
|
|
1535
|
-
tokens: 0,
|
|
1536
|
-
messages: 0,
|
|
1537
|
-
limit: CODEX_DEFAULT_LIMIT,
|
|
1538
|
-
percent: w.used_percent,
|
|
1539
|
-
oldestTsMs: null,
|
|
1540
|
-
resetAtMs: resetMs,
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1639
|
+
// T06407: 位置固定 (primary→5h / secondary→7d) をやめ、windowDurationMins で分類する。
|
|
1640
|
+
// rollout の rate_limits は snake_case (window_minutes / used_percent / resets_at) だが
|
|
1641
|
+
// classifyCodexRateLimitWindows が両 casing を吸収する。欠落窓は 0% でなく「未提供」。
|
|
1642
|
+
const { last5h, last7d } = classifyCodexRateLimitWindows(rateLimits)
|
|
1543
1643
|
|
|
1544
1644
|
return {
|
|
1545
1645
|
plan: rateLimits?.plan_type || "codex",
|
|
1546
1646
|
computedAt: Date.now(),
|
|
1547
1647
|
source: rateLimits ? "official" : "estimate",
|
|
1548
1648
|
context: contextPercent,
|
|
1549
|
-
last5h
|
|
1550
|
-
last7d
|
|
1649
|
+
last5h,
|
|
1650
|
+
last7d,
|
|
1551
1651
|
}
|
|
1552
1652
|
}
|
|
1553
1653
|
|