@cocorograph/hub-agent 0.7.35 → 0.7.37

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.35",
3
+ "version": "0.7.37",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -1134,6 +1134,24 @@ class ClaudeStreamSession {
1134
1134
  /* ignore */
1135
1135
  }
1136
1136
  this._busy = false
1137
+ // T05699 残タスク #2-1 (2026-07-04): interrupt() は現ターンの result を発火しない
1138
+ // (query は継続、次 input 受付継続のみ)。そのため abort 時点で _pendingMessages に
1139
+ // 溜まっている送信待ちメッセージは、result 経由の drain (_runResidentQuery 内の
1140
+ // result ハンドラ) を永久に迎えられず、_busy=false のまま取り残されていた。この
1141
+ // 状態で直後に次の sendMessage が届くと (_busy===false のため) pending をすっ飛ばして
1142
+ // _inputQueue へ直接 push してしまい、先に積んだメッセージより後着メッセージが
1143
+ // 先に処理される FIFO 順序崩れが起きる。ここで明示的に drain し、pending があれば
1144
+ // 同一 query 内で継続処理させる (無ければ _emitQueueState() のみで no-op)。
1145
+ // _drainResidentPending は async だが最初の await 前に _busy=true を同期的に
1146
+ // セットするため、この関数の呼び出し (await しない fire-and-forget) が完了した
1147
+ // 時点 (= abortTurn() の同期実行が終わるまでの同一 tick 内) で以降の sendMessage は
1148
+ // 正しく busy 状態を観測できる。
1149
+ this._drainResidentPending().catch((err) =>
1150
+ this.logger?.warn(
1151
+ { stream_id: this.stream_id, err: err?.message },
1152
+ "drainResidentPending (abort) threw",
1153
+ ),
1154
+ )
1137
1155
  return
1138
1156
  }
1139
1157
  if (this._abortController) {
@@ -300,6 +300,23 @@ export class CodexStreamBridge extends EventEmitter {
300
300
  this._maybeReapAfterIdle(session)
301
301
  }
302
302
 
303
+ /**
304
+ * 指定 cwd に一致するライブ (attach 済み) codex セッションの threadId を返す。
305
+ * Claude 版 `liveSessionIdForCwd` の対 (main.mjs の state loop / terminal mirror から
306
+ * cwd → threadId 解決に使う)。複数一致時は先に見つかったものを返す (5h/7d はアカウント
307
+ * 全体の値のためどのセッションから読んでも同じ契約の残量、代表 1 件で十分)。
308
+ *
309
+ * @param {string} cwd
310
+ * @returns {string|null}
311
+ */
312
+ threadIdForCwd(cwd) {
313
+ if (!cwd) return null
314
+ for (const session of this.sessions.values()) {
315
+ if (session.cwd === cwd && session.threadId) return session.threadId
316
+ }
317
+ return null
318
+ }
319
+
303
320
  _maybeReapAfterIdle(session) {
304
321
  if (session.activeTurnId) return // 完走まで待つ (turn/completed で再評価される)
305
322
  this._clearDetachTimer(session)
package/src/enroll.mjs CHANGED
@@ -66,6 +66,9 @@ export async function enroll(enrollmentToken, opts = {}) {
66
66
  claude_model: typeof data.claude_model === "string" ? data.claude_model : "",
67
67
  claude_permission_mode:
68
68
  typeof data.claude_permission_mode === "string" ? data.claude_permission_mode : "",
69
+ // Codex CLI 設定 (2026-07-04 追加、claude_model/claude_permission_mode と対称)。
70
+ codex_model: typeof data.codex_model === "string" ? data.codex_model : "",
71
+ codex_effort: typeof data.codex_effort === "string" ? data.codex_effort : "",
69
72
  })
70
73
 
71
74
  // Hub AI bundle を続けて同期する (CLAUDE.md / scripts / hooks / projects.json
package/src/main.mjs CHANGED
@@ -96,6 +96,7 @@ import { JsonlLiveWatchers } from "./jsonl-live-watchers.mjs"
96
96
  import {
97
97
  boundSessionId,
98
98
  codexSessionUsages,
99
+ codexTurnActiveForThread,
99
100
  codexUsageForThread,
100
101
  contextWindowSize,
101
102
  getSessionUsages,
@@ -173,6 +174,23 @@ function claudeCmdFromAgentConfig(config) {
173
174
  return cmd
174
175
  }
175
176
 
177
+ /**
178
+ * `~/.hub/agent.json` の Codex CLI 設定 (codex_model / codex_effort) から
179
+ * createTmuxSession の model/effort opts へのデフォルト値を取得する
180
+ * (claudeCmdFromAgentConfig の Codex 版)。
181
+ *
182
+ * tmux.create_session / worktree.create の browser 側 msg.model / msg.effort が
183
+ * 明示指定されていればそちらを優先し (呼び出し側で判定済み)、未指定のときだけ
184
+ * このデフォルトにフォールバックする。空文字は「未指定」として undefined を返す
185
+ * (buildCodexCmd 側は undefined を渡されても既定値へフォールバックするため安全)。
186
+ */
187
+ function codexDefaultsFromAgentConfig(config) {
188
+ return {
189
+ model: (config?.codex_model || "").trim() || undefined,
190
+ effort: (config?.codex_effort || "").trim() || undefined,
191
+ }
192
+ }
193
+
176
194
  /**
177
195
  * プロファイル切替用: 全 tmux セッションを kill → 同名・同 cwd で再作成する。
178
196
  *
@@ -749,6 +767,10 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
749
767
  logger,
750
768
  intervalMs: 5_000,
751
769
  claudeBridge,
770
+ // T05699 残タスク #1 (2026-07-04): 背景 Codex チャットセッションの turn_active を
771
+ // rollout ベースで解決するために渡す (main.mjs startStateLoop 内の codexBridge.threadIdForCwd
772
+ // 参照)。
773
+ codexBridge,
752
774
  readinessTracker,
753
775
  // tmux session_name → bind 中の session_id。turn_active を「そのペインが実際に走らせている
754
776
  // セッションの jsonl」で判定するために渡す (cwd dir 内の別 jsonl 取り違え=メタ欠陥#1 の根治)。
@@ -1408,7 +1430,31 @@ export function resolveStateLoopBoundSessionId(boundSessions, sessionName, cwd,
1408
1430
  return null
1409
1431
  }
1410
1432
 
1411
- export function startStateLoop({ client, plugins, logger, intervalMs, claudeBridge, readinessTracker, boundSessions }) {
1433
+ /**
1434
+ * turn_active をどの情報源で判定するかを決める純関数 (main.mjs startStateLoop から抽出、
1435
+ * T05699 残タスク #1)。実際の非同期解決 (turnActiveForCwd / codexTurnActiveForThread の
1436
+ * 呼び出し) は呼び出し側 (tick) が行う。優先順位:
1437
+ *
1438
+ * 1. chat 信号が processing → "chat-active" (true)
1439
+ * 2. chat 信号が waiting/idle → "chat-inactive" (false)
1440
+ * 3. fresh unbound bind (adopt 前の bind プレースホルダ) → "fresh-unbound" (null=不明、安全側)
1441
+ * 4. cwd に一致するライブ codexBridge セッションがある → "codex" (rollout ベース判定)
1442
+ * 5. cwd があれば → "claude" (jsonl ベース判定、TUI/bound session フォールバック)
1443
+ * 6. cwd も無ければ → "none" (null=不明)
1444
+ *
1445
+ * @param {{chatStatus?: string|null, cwd?: string|null, isFreshUnbound?: boolean, codexThreadId?: string|null}} params
1446
+ * @returns {'chat-active'|'chat-inactive'|'fresh-unbound'|'codex'|'claude'|'none'}
1447
+ */
1448
+ export function decideTurnActiveSource({ chatStatus, cwd, isFreshUnbound, codexThreadId }) {
1449
+ if (chatStatus === "processing") return "chat-active"
1450
+ if (chatStatus === "waiting" || chatStatus === "idle") return "chat-inactive"
1451
+ if (cwd && isFreshUnbound) return "fresh-unbound"
1452
+ if (codexThreadId) return "codex"
1453
+ if (cwd) return "claude"
1454
+ return "none"
1455
+ }
1456
+
1457
+ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBridge, codexBridge, readinessTracker, boundSessions }) {
1412
1458
  const lastByName = new Map() // session_name → {status, context_pct}
1413
1459
  const lastTurnAtByName = new Map() // session_name → 最後に event ファイル化した turnAt
1414
1460
  // (i) session_name → {sig, changedAt}: 出力領域署名の最終変化時刻 (出力フロー検知用)。
@@ -1536,23 +1582,50 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
1536
1582
  // null を返し mtime 最新 jsonl へフォールバックしていた (chat 版の cross-activity
1537
1583
  // 汚染, 2026-07 発覚)。resolveStateLoopBoundSessionId が claudeBridge のライブ
1538
1584
  // セッション id へフォールバックすることで TUI と同じ方式で根治する。
1585
+ // - Codex (T05699 残タスク #1, 2026-07-04): この cwd にライブな codexBridge セッション
1586
+ // (chat モード) があれば、chat 信号の 90s TTL に依存しない rollout ベースの判定
1587
+ // (codexTurnActiveForThread, task_started/task_complete イベント対) を使う。長時間
1588
+ // ターン中に chat 信号が stale 化してドットが消える欠落を根治する。Claude 版のような
1589
+ // paneAliveProbe は渡さない (chat モードのペインは素のシェルのまま = 実処理は別
1590
+ // プロセスのため、pane 生存判定を適用すると誤って dead 扱いになる)。cwd がライブな
1591
+ // codex セッションと一致しなければ従来通り Claude jsonl (turnActiveForCwd) にフォール
1592
+ // バックする (TUI/bare codex pane は現状 bound thread 解決が無いため、これまで通り
1593
+ // null=不明のまま、退行はしない)。
1594
+ // 分岐の優先順位は decideTurnActiveSource (純関数、単体テスト済み) が権威。
1539
1595
  let turnActive = null
1540
- if (chat?.status === "processing") {
1541
- turnActive = true
1542
- } else if (chat?.status === "waiting" || chat?.status === "idle") {
1543
- turnActive = false
1544
- } else if (s.cwd && isFreshUnboundBind(boundSessions?.get(s.session_name))) {
1545
- // 症状2b 根治: fresh bind プレースホルダの間は、別アクティビティ jsonl を mtime 最新で
1546
- // 取り違える汚染を避けて turn_active を出さない (null=不明)。実セッション id へ adopt
1547
- // されれば下の通常経路に乗る。青を出さない安全側にのみ倒すため固着青は生まない。
1548
- turnActive = null
1549
- } else if (s.cwd) {
1550
- turnActive = await turnActiveForCwd(
1551
- s.cwd,
1552
- resolveStateLoopBoundSessionId(boundSessions, s.session_name, s.cwd, claudeBridge),
1553
- s.session_name,
1554
- { paneAliveProbe: paneClaudeAliveOrUnknown },
1555
- )
1596
+ const codexThreadId = s.cwd ? codexBridge?.threadIdForCwd?.(s.cwd) || null : null
1597
+ const turnActiveSource = decideTurnActiveSource({
1598
+ chatStatus: chat?.status,
1599
+ cwd: s.cwd,
1600
+ isFreshUnbound: isFreshUnboundBind(boundSessions?.get(s.session_name)),
1601
+ codexThreadId,
1602
+ })
1603
+ switch (turnActiveSource) {
1604
+ case "chat-active":
1605
+ turnActive = true
1606
+ break
1607
+ case "chat-inactive":
1608
+ turnActive = false
1609
+ break
1610
+ case "fresh-unbound":
1611
+ // 症状2b 根治: fresh bind プレースホルダの間は、別アクティビティ jsonl を mtime 最新で
1612
+ // 取り違える汚染を避けて turn_active を出さない (null=不明)。実セッション id へ adopt
1613
+ // されれば下の通常経路に乗る。青を出さない安全側にのみ倒すため固着青は生まない。
1614
+ turnActive = null
1615
+ break
1616
+ case "codex":
1617
+ turnActive = await codexTurnActiveForThread(codexThreadId)
1618
+ break
1619
+ case "claude":
1620
+ turnActive = await turnActiveForCwd(
1621
+ s.cwd,
1622
+ resolveStateLoopBoundSessionId(boundSessions, s.session_name, s.cwd, claudeBridge),
1623
+ s.session_name,
1624
+ { paneAliveProbe: paneClaudeAliveOrUnknown },
1625
+ )
1626
+ break
1627
+ default:
1628
+ turnActive = null
1556
1629
  }
1557
1630
 
1558
1631
  const prev = lastByName.get(s.session_name)
@@ -1687,27 +1760,46 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
1687
1760
  * 渡す予定なので、現状は plugin hook chain だけ走らせて log に残す。
1688
1761
  */
1689
1762
  /**
1690
- * Hub から `ready` / `ack` で配信された Claude CLI 設定を agent.json と
1763
+ * Hub から `ready` / `ack` で配信された Claude/Codex CLI 設定を agent.json と
1691
1764
  * in-memory config に反映する (Phase 3 live-sync)。
1692
1765
  *
1693
1766
  * 旧設計 (Phase 2) では enrollment 時のみ取得していたため、cockpit 管理画面で
1694
1767
  * 設定を変更しても既存 agent には再 enroll しない限り反映されなかった。Phase 3 で
1695
1768
  * 全 WS 接続/heartbeat 毎に最新値が同期されるようにした。
1696
1769
  *
1770
+ * 2026-07-04: Codex turn/start 経由の model/effort 切替実装に伴い、codex_model/
1771
+ * codex_effort も同じ経路で同期する (claude_model/claude_permission_mode と対称)。
1772
+ *
1697
1773
  * 値が変化していない場合は writeConfig を呼ばない (書込負荷軽減)。
1698
1774
  * 失敗しても agent は止めず log だけ出して継続。
1699
1775
  */
1700
1776
  export async function maybeSyncClaudeSettings(msg, ctx) {
1701
- if (typeof msg?.claude_model !== "string" && typeof msg?.claude_permission_mode !== "string") {
1702
- return
1703
- }
1777
+ const hasClaudeFields =
1778
+ typeof msg?.claude_model === "string" || typeof msg?.claude_permission_mode === "string"
1779
+ const hasCodexFields =
1780
+ typeof msg?.codex_model === "string" || typeof msg?.codex_effort === "string"
1781
+ if (!hasClaudeFields && !hasCodexFields) return
1782
+
1704
1783
  const nextModel = typeof msg.claude_model === "string" ? msg.claude_model : ctx.config.claude_model || ""
1705
1784
  const nextMode = typeof msg.claude_permission_mode === "string" ? msg.claude_permission_mode : ctx.config.claude_permission_mode || ""
1785
+ const nextCodexModel = typeof msg.codex_model === "string" ? msg.codex_model : ctx.config.codex_model || ""
1786
+ const nextCodexEffort = typeof msg.codex_effort === "string" ? msg.codex_effort : ctx.config.codex_effort || ""
1706
1787
  const prevModel = ctx.config.claude_model || ""
1707
1788
  const prevMode = ctx.config.claude_permission_mode || ""
1708
- if (nextModel === prevModel && nextMode === prevMode) return
1789
+ const prevCodexModel = ctx.config.codex_model || ""
1790
+ const prevCodexEffort = ctx.config.codex_effort || ""
1791
+ if (
1792
+ nextModel === prevModel &&
1793
+ nextMode === prevMode &&
1794
+ nextCodexModel === prevCodexModel &&
1795
+ nextCodexEffort === prevCodexEffort
1796
+ ) {
1797
+ return
1798
+ }
1709
1799
  ctx.config.claude_model = nextModel
1710
1800
  ctx.config.claude_permission_mode = nextMode
1801
+ ctx.config.codex_model = nextCodexModel
1802
+ ctx.config.codex_effort = nextCodexEffort
1711
1803
  try {
1712
1804
  await writeConfig({
1713
1805
  agent_id: ctx.config.agent_id,
@@ -1715,13 +1807,20 @@ export async function maybeSyncClaudeSettings(msg, ctx) {
1715
1807
  hub_url: ctx.config.hub_url,
1716
1808
  claude_model: nextModel,
1717
1809
  claude_permission_mode: nextMode,
1810
+ codex_model: nextCodexModel,
1811
+ codex_effort: nextCodexEffort,
1718
1812
  })
1719
1813
  ctx.logger.info(
1720
- { claude_model: nextModel, claude_permission_mode: nextMode },
1721
- "claude settings synced from hub",
1814
+ {
1815
+ claude_model: nextModel,
1816
+ claude_permission_mode: nextMode,
1817
+ codex_model: nextCodexModel,
1818
+ codex_effort: nextCodexEffort,
1819
+ },
1820
+ "claude/codex settings synced from hub",
1722
1821
  )
1723
1822
  } catch (err) {
1724
- ctx.logger.warn({ err: err.message }, "failed to persist claude settings to agent.json")
1823
+ ctx.logger.warn({ err: err.message }, "failed to persist claude/codex settings to agent.json")
1725
1824
  }
1726
1825
  }
1727
1826
 
@@ -3579,11 +3678,17 @@ async function dispatch(msg, ctx) {
3579
3678
  : claudeCmdFromAgentConfig(ctx.config),
3580
3679
  codexCmd: typeof msg.codex_cmd === "string" ? msg.codex_cmd : undefined,
3581
3680
  // codex 用設定 (claude 経路では claudeCmd を直接使うため無視される)。
3582
- model: typeof msg.model === "string" ? msg.model : undefined,
3681
+ model:
3682
+ typeof msg.model === "string"
3683
+ ? msg.model
3684
+ : codexDefaultsFromAgentConfig(ctx.config).model,
3583
3685
  sandbox: typeof msg.sandbox === "string" ? msg.sandbox : undefined,
3584
3686
  approvalPolicy:
3585
3687
  typeof msg.approval_policy === "string" ? msg.approval_policy : undefined,
3586
- effort: typeof msg.effort === "string" ? msg.effort : undefined,
3688
+ effort:
3689
+ typeof msg.effort === "string"
3690
+ ? msg.effort
3691
+ : codexDefaultsFromAgentConfig(ctx.config).effort,
3587
3692
  initialPrompt:
3588
3693
  typeof msg.initial_prompt === "string" ? msg.initial_prompt : undefined,
3589
3694
  logger: ctx.logger,
@@ -3639,11 +3744,17 @@ async function dispatch(msg, ctx) {
3639
3744
  : claudeCmdFromAgentConfig(ctx.config),
3640
3745
  codexCmd: typeof msg.codex_cmd === "string" ? msg.codex_cmd : undefined,
3641
3746
  // codex 用設定 (claude 経路では無視される)。
3642
- model: typeof msg.model === "string" ? msg.model : undefined,
3747
+ model:
3748
+ typeof msg.model === "string"
3749
+ ? msg.model
3750
+ : codexDefaultsFromAgentConfig(ctx.config).model,
3643
3751
  sandbox: typeof msg.sandbox === "string" ? msg.sandbox : undefined,
3644
3752
  approvalPolicy:
3645
3753
  typeof msg.approval_policy === "string" ? msg.approval_policy : undefined,
3646
- effort: typeof msg.effort === "string" ? msg.effort : undefined,
3754
+ effort:
3755
+ typeof msg.effort === "string"
3756
+ ? msg.effort
3757
+ : codexDefaultsFromAgentConfig(ctx.config).effort,
3647
3758
  initialPrompt:
3648
3759
  typeof msg.initial_prompt === "string" ? msg.initial_prompt : undefined,
3649
3760
  logger: ctx.logger,
package/src/usage.mjs CHANGED
@@ -1450,3 +1450,123 @@ export async function codexSessionUsages(liveSessions) {
1450
1450
  )
1451
1451
  return out
1452
1452
  }
1453
+
1454
+ // ---------------------------------------------------------------------------
1455
+ // Codex 版 turn_active 判定 (Cockpit Codex 両対応 T05699 残タスク #1, 2026-07-04)。
1456
+ //
1457
+ // 背景: サイドバー背景セッションのステータスドット (turn_active) は state loop
1458
+ // (main.mjs startStateLoop) が権威。Codex はこれまで in-memory チャット信号
1459
+ // (chat-signals.mjs, TTL 90s) だけに依存しており、長時間ターン中に 90s 無通知の
1460
+ // 間隔が空くと stale 化しドットが消える欠落があった (main.mjs:1556-1573 の
1461
+ // else-if 分岐は Claude jsonl 専用 turnActiveForCwd しか知らない)。
1462
+ //
1463
+ // 実機の rollout jsonl (2026-07-04, 実データ 2444 ファイル走査) で `event_msg` の
1464
+ // `task_started`/`task_complete` が turn の開始/終了を示す明確なペアであることを
1465
+ // 確認済み (全ファイルで started==completed の対応が崩れた例は無し)。B3
1466
+ // (codexUsageForThread) と同じ「threadId → rollout ファイル特定 → 末尾 tail 走査」
1467
+ // パターンを踏襲し、チャット信号の TTL に依存しない独立した真実源にする。
1468
+ // ---------------------------------------------------------------------------
1469
+
1470
+ // task_started の記録から task_complete が届かないまま経過した場合の猶予。TUI (bare pane)
1471
+ // 経由の bound thread 解決は未対応 (2026-07-04 時点、`codex.tui.bind` 相当が無い) のため、
1472
+ // 現状 paneAliveProbe を渡すのは将来の TUI 対応時のみ想定。チャットモード (app-server 駆動)
1473
+ // はペインが素のシェルのまま (実処理は別プロセス) なので pane 生存判定の対象外。
1474
+ const CODEX_STALE_TASK_MS = 5_000
1475
+
1476
+ /** P5(perf): codex turn_active 判定結果を mtime+size でメモ化。 */
1477
+ const _codexTurnActiveMemo = new Map() // fp → { mtimeMs, size, result }
1478
+
1479
+ /**
1480
+ * rollout jsonl テキストを走査して「最終 turn_active 状態 + 直近 task_started の時刻」を
1481
+ * 返す内部ヘルパー。Claude の `_scanTurnState` と対だが、Codex は `task_started`/
1482
+ * `task_complete` という明示イベントペアがあるため stop_reason 解釈のステートマシンは不要。
1483
+ *
1484
+ * @param {string} text
1485
+ * @returns {{active: boolean, activeSinceTs: number|null}}
1486
+ */
1487
+ export function _scanCodexTurnState(text) {
1488
+ const empty = { active: false, activeSinceTs: null }
1489
+ if (!text) return empty
1490
+ let active = false
1491
+ let activeSinceTs = null
1492
+ const lines = text.split("\n")
1493
+ for (const line of lines) {
1494
+ if (!line) continue
1495
+ let d
1496
+ try {
1497
+ d = JSON.parse(line)
1498
+ } catch {
1499
+ continue
1500
+ }
1501
+ if (d.type !== "event_msg") continue
1502
+ const pt = d.payload?.type
1503
+ if (pt === "task_started") {
1504
+ active = true
1505
+ activeSinceTs = parseEntryTs(d.timestamp)
1506
+ } else if (pt === "task_complete") {
1507
+ active = false
1508
+ activeSinceTs = null
1509
+ }
1510
+ }
1511
+ return { active, activeSinceTs }
1512
+ }
1513
+
1514
+ /**
1515
+ * 指定 threadId の Codex セッションが現在ターン進行中かを rollout jsonl から判定する。
1516
+ * `turnActiveForCwd` (Claude) の対。チャット信号 (in-memory, TTL 90s) が stale なときの
1517
+ * フォールバックとして main.mjs の state loop から呼ばれる想定。
1518
+ *
1519
+ * @param {string} threadId
1520
+ * @param {{sessionName?: string, paneAliveProbe?: (name:string)=>Promise<'alive'|'dead'|'unknown'>, now?: number}} [opts]
1521
+ * sessionName/paneAliveProbe: 将来の TUI (bare pane) 対応用。チャットモードはペインが
1522
+ * 素のシェルのまま (実処理は別プロセス) のため渡さないこと (誤って dead 判定され得る)。
1523
+ * @returns {Promise<boolean|null>} true=生成中 / false=待機中 / null=rollout 不明
1524
+ */
1525
+ export async function codexTurnActiveForThread(threadId, opts = {}) {
1526
+ if (!threadId) return null
1527
+ const fp = await findCodexRolloutFile(threadId)
1528
+ if (!fp) return null
1529
+ let st
1530
+ try {
1531
+ st = await fs.stat(fp)
1532
+ } catch {
1533
+ return null
1534
+ }
1535
+
1536
+ const memo = _codexTurnActiveMemo.get(fp)
1537
+ if (memo && memo.mtimeMs === st.mtimeMs && memo.size === st.size) {
1538
+ return memo.result
1539
+ }
1540
+
1541
+ const tail = await readTail(fp, 256 * 1024)
1542
+ let state = { active: false, activeSinceTs: null }
1543
+ if (tail != null) state = _scanCodexTurnState(tail)
1544
+ // tail で active=false の場合のみ、巨大 response_item で先頭の task_started が
1545
+ // tail 窓から落ちていないか全文で確認する (Claude 版 turnActiveForCwd と同じ方針)。
1546
+ if (state.active === false) {
1547
+ const full = await readOrNull(fp)
1548
+ if (full != null) state = _scanCodexTurnState(full)
1549
+ }
1550
+
1551
+ let active = state.active
1552
+ const now = typeof opts.now === "number" ? opts.now : Date.now()
1553
+ if (
1554
+ active &&
1555
+ state.activeSinceTs !== null &&
1556
+ now - state.activeSinceTs > CODEX_STALE_TASK_MS &&
1557
+ opts.sessionName &&
1558
+ typeof opts.paneAliveProbe === "function"
1559
+ ) {
1560
+ const verdict = await opts.paneAliveProbe(opts.sessionName)
1561
+ if (verdict === "dead") active = false
1562
+ }
1563
+
1564
+ const result = active
1565
+ _codexTurnActiveMemo.set(fp, { mtimeMs: st.mtimeMs, size: st.size, result })
1566
+ return result
1567
+ }
1568
+
1569
+ /** テスト用: codex turn_active メモをクリアする。 */
1570
+ export function _resetCodexTurnActiveMemo() {
1571
+ _codexTurnActiveMemo.clear()
1572
+ }