@cocorograph/hub-agent 0.7.94 → 0.7.96

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.7.94",
3
+ "version": "0.7.96",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -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"
@@ -2633,7 +2636,9 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
2633
2636
  const chatCliKind = sessionCliKind
2634
2637
  const contextSessionId =
2635
2638
  chat?.sessionId ??
2636
- (sessionCliKind === "claude" ? boundSessions?.get(s.session_name) ?? null : codexThreadId)
2639
+ (sessionCliKind === "claude"
2640
+ ? boundSessions?.get(s.session_name) ?? `unbound:${s.session_name}`
2641
+ : codexThreadId)
2637
2642
 
2638
2643
  const prev = lastByName.get(s.session_name)
2639
2644
  if (
@@ -5632,6 +5637,10 @@ async function dispatch(msg, ctx) {
5632
5637
  }
5633
5638
  case "usage.request": {
5634
5639
  try {
5640
+ const activeClaudeProfile = await getActiveProfile()
5641
+ const claudeHasCredentials = isKeychainPlatform()
5642
+ ? await profileHasKeychainCredentials(activeClaudeProfile.configDir)
5643
+ : await hasFileCredentials(activeClaudeProfile.configDir)
5635
5644
  // 5h/7d の本命ソース: SDK control request (アカウント全体、session 非依存)。
5636
5645
  // statusLine が rate_limits を出さなくなった環境 (2026-07 時点の Claude CLI) でも
5637
5646
  // ここでライブ更新できる。frontend の usage.request ポーリング (既定 60s) 頻度に
@@ -5641,15 +5650,20 @@ async function dispatch(msg, ctx) {
5641
5650
  if (ctx.claudeBridge) {
5642
5651
  try {
5643
5652
  const acct = await ctx.claudeBridge.getAccountUsage()
5644
- if (acct) recordChatUsageSnapshot(acct)
5653
+ if (acct) recordChatUsageSnapshot(acct, Date.now(), activeClaudeProfile.id)
5645
5654
  } catch (err) {
5646
5655
  ctx.logger?.debug?.({ err: err?.message }, "account usage snapshot skipped")
5647
5656
  }
5648
5657
  }
5649
- const [usage, sessions] = await Promise.all([
5650
- getUsage(),
5658
+ let [usage, sessions] = await Promise.all([
5659
+ getUsage({ profileId: activeClaudeProfile.id }),
5651
5660
  getSessionUsages(),
5652
5661
  ])
5662
+ // 未ログインで残ったローカル transcript/cache をアカウント利用量として見せない。
5663
+ if (!claudeHasCredentials) {
5664
+ usage = null
5665
+ sessions = []
5666
+ }
5653
5667
 
5654
5668
  // Codex 版 (Stream B3, 2026-07-03): rollout jsonl の token_count イベントから
5655
5669
  // 5h/7d + context% を取る。⚠️ Codex の 5h/7d は ChatGPT/OpenAI サブスクリプション
@@ -5658,7 +5672,9 @@ async function dispatch(msg, ctx) {
5658
5672
  // として返す (frontend 側でどちらのアカウントの残量か混同しないため)。
5659
5673
  let codexUsage = null
5660
5674
  let codexSessions = []
5661
- if (ctx.codexBridge && ctx.codexBridge.sessions.size > 0) {
5675
+ const activeCodexProfile = await codexProfiles.getActiveProfile()
5676
+ const codexHasCredentials = await codexProfiles.hasCodexCredentials(activeCodexProfile.configDir)
5677
+ if (ctx.codexBridge && codexHasCredentials) {
5662
5678
  try {
5663
5679
  // T06407 増分2 (axis2): per-cwd の context% は **cwd ごとに権威 producer 1 件**へ
5664
5680
  // 集約する。旧実装は全 live session を codexSessions へ流していたため、同一 cwd に
@@ -5708,10 +5724,24 @@ async function dispatch(msg, ctx) {
5708
5724
  codexUsage = { ...codexUsage, context: null }
5709
5725
  }
5710
5726
  }
5727
+ // ライブ session の有無にかかわらず、起動直後の account RPC を優先する。
5728
+ // rollout は context の補助だけにし、5h/7d はプロファイル固有の最新実値にする。
5729
+ const accountUsage = codexUsageFromAccountRateLimits(
5730
+ await ctx.codexBridge.readAccountRateLimits({ profileId: activeCodexProfile.id }),
5731
+ )
5732
+ if (accountUsage) {
5733
+ codexUsage = { ...accountUsage, context: codexUsage?.context ?? null }
5734
+ recordAccountUsageSnapshot("codex", activeCodexProfile.id, codexUsage)
5735
+ }
5711
5736
  } catch (err) {
5712
5737
  ctx.logger?.debug?.({ err: err?.message }, "codex usage snapshot skipped")
5713
5738
  }
5714
5739
  }
5740
+ // Agent 再起動後や一時的な account RPC 失敗時も、同一 profile の最終取得値だけを
5741
+ // 表示する。未ログインならここへ到達せず、表示も完全に隠す。
5742
+ if (codexHasCredentials && !codexUsage) {
5743
+ codexUsage = await readAccountUsageSnapshot("codex", activeCodexProfile.id)
5744
+ }
5715
5745
 
5716
5746
  ctx.client.send({
5717
5747
  type: "usage.response",
package/src/state.mjs CHANGED
@@ -313,6 +313,16 @@ export function detectContextPctFromText(text) {
313
313
  if (Number.isFinite(n) && n >= 0 && n <= 100) return n
314
314
  }
315
315
  }
316
+ // Claude の初回 Chrome 連携確認は、TUI セッションがまだ会話を開始していないことを
317
+ // 明示する画面。context 行は未描画だが使用量は実際に 0% なので、同一 cwd に残った
318
+ // 過去 jsonl の高使用率を採用せず 0% を返す。文言の一部だけで誤認しないよう、製品名と
319
+ // 選択肢を対にして判定する。
320
+ if (
321
+ /Claude in Chrome extension detected/i.test(text) &&
322
+ /Yes,? use my browser/i.test(text)
323
+ ) {
324
+ return 0
325
+ }
316
326
  return null
317
327
  }
318
328
 
package/src/tmux.mjs CHANGED
@@ -687,13 +687,19 @@ export async function listSessions(opts = {}) {
687
687
  boundSessionId(opts.boundSessions, s.name) ||
688
688
  (cwd && opts.claudeBridge?.liveSessionIdForCwd?.(cwd)) ||
689
689
  null
690
- const jsonlInfo = cwd ? await jsonlContextForCwd(cwd, boundId) : null
690
+ // cwd だけでは同じリポジトリで並走する別 Claude / subagent / 過去会話を区別できない。
691
+ // bound ID が無い状態で statusLine/jsonl の「cwd 最新値」を使うと、再起動後や未オープンの
692
+ // tmux セッションへ別会話の 98〜100% が混入する。実際に値の帰属先を示せる bound session
693
+ // がある場合だけ per-session cache を採用し、無ければ capture-pane の当該行由来値 (または
694
+ // null) に留める。null は「不明」であり、他会話の数値を表示するより正しい。
695
+ const hasBoundSession = typeof boundId === "string" && boundId.length > 0
696
+ const jsonlInfo = hasBoundSession && cwd ? await jsonlContextForCwd(cwd, boundId) : null
691
697
  const context_pct =
692
698
  freshUnbound
693
699
  ? 0
694
700
  : jsonlInfo?.isReset && typeof jsonlInfo.percent === "number"
695
701
  ? jsonlInfo.percent
696
- : typeof fromStatusLine === "number"
702
+ : hasBoundSession && typeof fromStatusLine === "number"
697
703
  ? fromStatusLine
698
704
  : typeof jsonlInfo?.percent === "number"
699
705
  ? jsonlInfo.percent
package/src/usage.mjs CHANGED
@@ -292,12 +292,26 @@ export function whenChatRateLimitsPersisted() {
292
292
  // (旧 chatRateLimits の設計的弱点)。専用ファイルなら誰にも上書きされない。
293
293
  // ---------------------------------------------------------------------------
294
294
 
295
- function chatUsageCachePath() {
296
- return configPath("HUB_CHAT_USAGE_CACHE", os.homedir(), ".hub", "usage", "chat-usage.json")
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 chatUsageSnapshot = null
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
- chatUsageSnapshot = {
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 || chatUsageSnapshot?.plan || null,
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 = chatUsageSnapshot
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
- if (chatUsageSnapshot) return chatUsageSnapshot
378
- const text = await readOrNull(chatUsageCachePath())
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") return j
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
- chatUsageSnapshot = null
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
- const baseline = Math.max(cacheMtime ?? 0, chatRateLimits.updatedAtMs || 0)
1190
- // `>=` (上と同じ理由): ファイル mtime とプロセス内タイムスタンプがタイになり得る。
1191
- // chatSnap は最も直接的な情報源 (SDK control request 実値) なのでタイ時は優先する。
1192
- if (chatSnap.updatedAtMs >= baseline) {
1193
- if (chatSnap.five_hour) {
1194
- result.last5h = {
1195
- ...result.last5h,
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
- if (chatSnap.seven_day) {
1201
- result.last7d = {
1202
- ...result.last7d,
1203
- percent: chatSnap.seven_day.percent,
1204
- resetAtMs: chatSnap.seven_day.resetAtMs ?? result.last7d?.resetAtMs ?? null,
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
- if (chatSnap.plan) result.plan = chatSnap.plan
1208
- if (chatSnap.five_hour || chatSnap.seven_day) result.source = "official"
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
  //