@cocorograph/hub-agent 0.8.10 → 0.8.11
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 +68 -42
- package/src/codex-stream-bridge.mjs +218 -142
- package/src/main.mjs +29 -11
package/package.json
CHANGED
|
@@ -523,11 +523,11 @@ export class ClaudeStreamSession {
|
|
|
523
523
|
this._permissionResolvers = new Map()
|
|
524
524
|
/** 現在ターン実行中か (多重 query 防止) */
|
|
525
525
|
this._busy = false
|
|
526
|
-
/** SDK-P0-3 (2026-07-10): 現ターンが既に終端処理済みか (
|
|
526
|
+
/** SDK-P0-3 (2026-07-10): 現ターンが既に終端処理済みか (idle / abort の
|
|
527
527
|
* いずれか 1 回だけ _busy=false + drain させるための冪等ガード)。resident query は
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
528
|
+
* `result` の後にもバックグラウンド処理が続き、最後に権威的な
|
|
529
|
+
* `session_state_changed:idle` が流れる。通常完了では idle だけが settle し、
|
|
530
|
+
* 明示 interrupt だけが先行して settled を立てる。ターン開始時に false へ戻す。 */
|
|
531
531
|
this._turnSettled = false
|
|
532
532
|
/** SDK-P0-3: 現ターンが 1 件でもストリームイベントを出したか。
|
|
533
533
|
* 「前ターンの遅延 idle が、drain で始まったばかりの次ターンを誤終了させる」レースの
|
|
@@ -539,9 +539,9 @@ export class ClaudeStreamSession {
|
|
|
539
539
|
this._turnProgressed = false
|
|
540
540
|
/** SDK-P0-4 (2026-07-13): ターン連番。ログと帰属デバッグ用 (_beginResidentTurn で ++)。 */
|
|
541
541
|
this._turnSeq = 0
|
|
542
|
-
/** SDK-P0-4: 「直前ターンの idle がこれから 1 件届きうる」状態。
|
|
543
|
-
*
|
|
544
|
-
*
|
|
542
|
+
/** SDK-P0-4: 「直前ターンの idle がこれから 1 件届きうる」状態。abort (interrupt) で
|
|
543
|
+
* ターンを先行して畳んだときに arm する (interrupt 後にも中断ターンの idle が流れるため)。
|
|
544
|
+
* 通常完了は idle 自身で畳むので arm しない。arm 中に届いた idle は
|
|
545
545
|
* **前ターンのもの**として 1 件だけ握り潰し、現ターンを終端しない。
|
|
546
546
|
* 解除条件: (a) idle を 1 件消費した (b) SDK の running が届いた (= 前ターンの全イベントは
|
|
547
547
|
* 順序保証により既に通過済み) (c) 現ターンの result が届いた (d) STALE_IDLE_GUARD_MS 経過。 */
|
|
@@ -1131,9 +1131,10 @@ export class ClaudeStreamSession {
|
|
|
1131
1131
|
this._busy = true
|
|
1132
1132
|
this._abortController = new AbortController()
|
|
1133
1133
|
let aborted = false
|
|
1134
|
-
//
|
|
1135
|
-
//
|
|
1134
|
+
// `session_state_changed:idle` がターン終了の正本。SDK が idle を流さず generator が
|
|
1135
|
+
// 閉じた旧/異常経路だけ合成 idle と onTurnSettled で waiting へ落とす。
|
|
1136
1136
|
let resultDelivered = false
|
|
1137
|
+
let idleDelivered = false
|
|
1137
1138
|
|
|
1138
1139
|
const options = {
|
|
1139
1140
|
cwd: this.cwd,
|
|
@@ -1185,6 +1186,7 @@ export class ClaudeStreamSession {
|
|
|
1185
1186
|
resultDelivered = true
|
|
1186
1187
|
if (typeof msg.session_id === "string") this.sessionId = msg.session_id
|
|
1187
1188
|
}
|
|
1189
|
+
if (isSessionIdleEvent(msg)) idleDelivered = true
|
|
1188
1190
|
try {
|
|
1189
1191
|
this._emit(msg)
|
|
1190
1192
|
} catch (err) {
|
|
@@ -1224,12 +1226,9 @@ export class ClaudeStreamSession {
|
|
|
1224
1226
|
if (aborted) {
|
|
1225
1227
|
this.logger?.info({ stream_id: this.stream_id }, "claude turn aborted")
|
|
1226
1228
|
}
|
|
1227
|
-
// RC-4:
|
|
1228
|
-
//
|
|
1229
|
-
|
|
1230
|
-
// result が既に waiting にしているので !resultDelivered のときだけ。_drainPending で
|
|
1231
|
-
// 次ターン (processing) が始まる前に呼ぶこと (順序: ここ → reap/drain)。
|
|
1232
|
-
if (!resultDelivered) {
|
|
1229
|
+
// RC-4: idle を届けずに generator が閉じた旧/異常経路では、チャット信号と browser の
|
|
1230
|
+
// 双方へ同じ終了境界を補う。_drainPending で次ターンを始める前に行うこと。
|
|
1231
|
+
if (!idleDelivered) {
|
|
1233
1232
|
try {
|
|
1234
1233
|
this.onTurnSettled?.()
|
|
1235
1234
|
} catch {
|
|
@@ -1244,11 +1243,24 @@ export class ClaudeStreamSession {
|
|
|
1244
1243
|
// フッターで表示する)。uuid を持たせるのは重複排除 (isDuplicateEvent) が result
|
|
1245
1244
|
// 署名 (session_id + num_turns + duration_ms + cost) で判定するため — 合成 result
|
|
1246
1245
|
// はこれらが毎回同値になり、2 回目以降の中断で「重複」と誤判定され捨てられるのを防ぐ。
|
|
1246
|
+
if (!resultDelivered) {
|
|
1247
|
+
try {
|
|
1248
|
+
this._emit({
|
|
1249
|
+
type: "result",
|
|
1250
|
+
subtype: aborted ? "aborted_by_user" : "turn_settled",
|
|
1251
|
+
uuid: randomUUID(),
|
|
1252
|
+
session_id: this.sessionId ?? undefined,
|
|
1253
|
+
timestamp: new Date().toISOString(),
|
|
1254
|
+
})
|
|
1255
|
+
} catch {
|
|
1256
|
+
/* ignore */
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1247
1259
|
try {
|
|
1248
1260
|
this._emit({
|
|
1249
|
-
type: "
|
|
1250
|
-
subtype:
|
|
1251
|
-
|
|
1261
|
+
type: "system",
|
|
1262
|
+
subtype: "session_state_changed",
|
|
1263
|
+
state: "idle",
|
|
1252
1264
|
session_id: this.sessionId ?? undefined,
|
|
1253
1265
|
timestamp: new Date().toISOString(),
|
|
1254
1266
|
})
|
|
@@ -1618,10 +1630,10 @@ export class ClaudeStreamSession {
|
|
|
1618
1630
|
}
|
|
1619
1631
|
}
|
|
1620
1632
|
|
|
1621
|
-
/**
|
|
1622
|
-
*
|
|
1623
|
-
*
|
|
1624
|
-
* @param {string} reason "
|
|
1633
|
+
/** resident の権威的な「ターン終端」を 1 箇所に集約する。
|
|
1634
|
+
* 通常完了は SDK の session_state_changed:idle だけが呼ぶ。result は最終結果の
|
|
1635
|
+
* flush であり、バックグラウンド処理を含むターン全体の終了境界ではない。
|
|
1636
|
+
* @param {string} reason "idle" — ログ用
|
|
1625
1637
|
* @param {(msg: string) => void} [denyPending] 未解決 permission を閉じるクロージャ
|
|
1626
1638
|
*/
|
|
1627
1639
|
_settleResidentTurn(reason, denyPending) {
|
|
@@ -1629,11 +1641,6 @@ export class ClaudeStreamSession {
|
|
|
1629
1641
|
this._turnSettled = true
|
|
1630
1642
|
denyPending?.("turn ended")
|
|
1631
1643
|
this._busy = false
|
|
1632
|
-
// SDK-P0-4: result で畳んだターンは、この後に自分の idle を必ず 1 件流す
|
|
1633
|
-
// (sdk.d.ts: "'idle' fires after heldBackResult flushes … authoritative turn-over signal")。
|
|
1634
|
-
// drain が同期で次ターンを開始するため、その idle が新ターンに誤帰属しないようガードを張る。
|
|
1635
|
-
// idle 自身で畳んだ場合は既にその idle を消費済みなので張らない。
|
|
1636
|
-
if (reason === "result") this._armStaleIdleGuard()
|
|
1637
1644
|
// 改修4 (A): シリアライズした pending があれば次の 1 件を InputQueue へ流す。
|
|
1638
1645
|
// ultracode (0.6.28): _drainResidentPending は applyFlagSettings を await するため
|
|
1639
1646
|
// async。for await ループ内からは fire-and-forget で呼ぶ (最初の await より前に
|
|
@@ -1649,7 +1656,7 @@ export class ClaudeStreamSession {
|
|
|
1649
1656
|
/** 改修4 (A): ターン完了時に pending の先頭 1 件を InputQueue へ流す (ターンのシリアライズ)。
|
|
1650
1657
|
* queue_state を更新して送信待ちチップを drain させる (frontend がバブル昇格する)。
|
|
1651
1658
|
* ultracode (0.6.28): 次ターンの目標 ultracode 状態へ reconcile してから push する
|
|
1652
|
-
* (await するため async 化。
|
|
1659
|
+
* (await するため async 化。idle ハンドラからは fire-and-forget で呼ばれる)。 */
|
|
1653
1660
|
async _drainResidentPending() {
|
|
1654
1661
|
if (this._closed) return
|
|
1655
1662
|
if (this._pendingMessages.length === 0) {
|
|
@@ -1693,7 +1700,7 @@ export class ClaudeStreamSession {
|
|
|
1693
1700
|
|
|
1694
1701
|
/** 改修2+4: 常駐 query を 1 回だけ起動し、streaming input キューから複数ターンを処理する。
|
|
1695
1702
|
* system/init は最初の 1 回のみ (毎ターン init = 文脈/キャッシュ温め直しを解消)。各ターンは
|
|
1696
|
-
*
|
|
1703
|
+
* 権威的終端シグナル (session_state_changed:idle) で _busy=false にするが query は継続し
|
|
1697
1704
|
* 次 input を待つ。close (inputQueue.close) で generator が終わり query が完走する。
|
|
1698
1705
|
* SDK-P0-3 (2026-07-10): 終端を result だけに頼ると、SDK が idle でターンを終えたときに
|
|
1699
1706
|
* _busy が固着し、以降の送信が pending へ永久滞留する (T06052)。
|
|
@@ -1749,6 +1756,7 @@ export class ClaudeStreamSession {
|
|
|
1749
1756
|
const q = this.sdk.query({ prompt: this._inputQueue, options })
|
|
1750
1757
|
this._residentQuery = q
|
|
1751
1758
|
for await (const msg of q) {
|
|
1759
|
+
let suppressEmit = false
|
|
1752
1760
|
if (
|
|
1753
1761
|
msg?.type === "system" &&
|
|
1754
1762
|
msg?.subtype === "init" &&
|
|
@@ -1758,12 +1766,12 @@ export class ClaudeStreamSession {
|
|
|
1758
1766
|
}
|
|
1759
1767
|
if (msg?.type === "result") {
|
|
1760
1768
|
if (typeof msg.session_id === "string") this.sessionId = msg.session_id
|
|
1761
|
-
//
|
|
1762
|
-
//
|
|
1763
|
-
//
|
|
1769
|
+
// result は結果 flush でありターン終了ではない。SDK の型定義が権威的な
|
|
1770
|
+
// turn-over signal とする session_state_changed:idle まで busy を維持し、
|
|
1771
|
+
// pending を streaming input へ流さない。
|
|
1772
|
+
// 現ターンの result が届いた時点で前ターンの遅延 idle は順序保証により
|
|
1773
|
+
// 既に通過済みなので、interrupt 由来のガードだけ解除できる。
|
|
1764
1774
|
this._clearStaleIdleGuard()
|
|
1765
|
-
// ターン完了: 未解決 permission を閉じ、次 input 受付へ (query は継続)。
|
|
1766
|
-
this._settleResidentTurn("result", denyPending)
|
|
1767
1775
|
} else if (isSessionIdleEvent(msg)) {
|
|
1768
1776
|
// SDK-P0-3 (2026-07-10): result が届かずに idle でターンが終わるケースの救済。
|
|
1769
1777
|
// これを見ていなかったため _busy が true で固着し、以降の送信が
|
|
@@ -1777,11 +1785,14 @@ export class ClaudeStreamSession {
|
|
|
1777
1785
|
// 2) それ以外で _busy かつ (running を観測済み or 何かイベントを出した) → 現ターンの終端。
|
|
1778
1786
|
if (this._staleIdleExpected) {
|
|
1779
1787
|
this._clearStaleIdleGuard()
|
|
1788
|
+
// browser にも転送しない。前ターンの遅延 idle を新ターンの reducer が受けると、
|
|
1789
|
+
// 3点リーダーと送信ボタンだけが誤って idle になる。
|
|
1790
|
+
suppressEmit = true
|
|
1780
1791
|
this.logger?.debug?.(
|
|
1781
1792
|
{ stream_id: this.stream_id, turn: this._turnSeq },
|
|
1782
1793
|
"stale idle from previous turn ignored",
|
|
1783
1794
|
)
|
|
1784
|
-
} else if (this._busy
|
|
1795
|
+
} else if (this._busy) {
|
|
1785
1796
|
this._settleResidentTurn("idle", denyPending)
|
|
1786
1797
|
// per-message の RC-4 と対称に、チャット信号 (サイドバードット) を
|
|
1787
1798
|
// processing→waiting へ落とす。result 経由なら main.mjs が result を見て
|
|
@@ -1804,13 +1815,15 @@ export class ClaudeStreamSession {
|
|
|
1804
1815
|
// および session_state_changed の running / requires_action もここに含む。
|
|
1805
1816
|
this._turnProgressed = true
|
|
1806
1817
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1818
|
+
if (!suppressEmit) {
|
|
1819
|
+
try {
|
|
1820
|
+
this._emit(msg)
|
|
1821
|
+
} catch (err) {
|
|
1822
|
+
this.logger?.warn(
|
|
1823
|
+
{ err: err.message, stream_id: this.stream_id },
|
|
1824
|
+
"onEvent callback threw",
|
|
1825
|
+
)
|
|
1826
|
+
}
|
|
1814
1827
|
}
|
|
1815
1828
|
}
|
|
1816
1829
|
} catch (err) {
|
|
@@ -1949,6 +1962,19 @@ export class ClaudeStreamSession {
|
|
|
1949
1962
|
} catch {
|
|
1950
1963
|
/* ignore */
|
|
1951
1964
|
}
|
|
1965
|
+
// 通常完了と同じ権威境界を browser へ即時提示する。SDK が後から流す
|
|
1966
|
+
// 中断ターン由来の実 idle は _staleIdleExpected で抑止する。
|
|
1967
|
+
try {
|
|
1968
|
+
this._emit({
|
|
1969
|
+
type: "system",
|
|
1970
|
+
subtype: "session_state_changed",
|
|
1971
|
+
state: "idle",
|
|
1972
|
+
session_id: this.sessionId ?? undefined,
|
|
1973
|
+
timestamp: new Date().toISOString(),
|
|
1974
|
+
})
|
|
1975
|
+
} catch {
|
|
1976
|
+
/* ignore */
|
|
1977
|
+
}
|
|
1952
1978
|
}
|
|
1953
1979
|
// T05699 残タスク #2-1 (2026-07-04): interrupt() は現ターンの result を発火しない
|
|
1954
1980
|
// (query は継続、次 input 受付継続のみ)。そのため abort 時点で _pendingMessages に
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Claude 側との対応 (詳細は main.mjs の codex.* dispatch = Slice 3 で配線):
|
|
10
10
|
* claude.attach → attach() (initialize → thread/start か thread/resume)
|
|
11
|
-
* claude.input → input() (turn/start
|
|
11
|
+
* claude.input → input() (idle は turn/start、生成中は次ターン FIFO)
|
|
12
12
|
* claude.interrupt → interrupt() (turn/interrupt)
|
|
13
13
|
* claude.detach → detach() (TTL 付き graceful close)
|
|
14
14
|
* (codex 固有) → keepalive() (閲覧中の生存シグナル。pty.resize 相当)
|
|
@@ -136,6 +136,10 @@ const DEAD_TERMINAL_TTL_MS =
|
|
|
136
136
|
const TURN_START_TIMEOUT_MS =
|
|
137
137
|
Number(process.env.HUB_AGENT_CODEX_TURN_START_TIMEOUT_MS) || 30 * 1000
|
|
138
138
|
|
|
139
|
+
// browser が ack を取りこぼしても queue snapshot から受理済みと再確認できるよう、
|
|
140
|
+
// 直近 input_id を bounded ring で保持する (Claude queue_state.accepted と同じ契約)。
|
|
141
|
+
const ACCEPTED_INPUT_ID_RING_MAX = 256
|
|
142
|
+
|
|
139
143
|
// sibling-inject の turn RPC が成功した後、app-server が同じ userMessage を
|
|
140
144
|
// item/completed で echo するまで待つ上限。RPC accepted と userMessage delivered を
|
|
141
145
|
// 分離し、旧 stream / 初期化競合で rollout に残らない入力を成功扱いしない。
|
|
@@ -255,9 +259,8 @@ export function deriveResumeTurnState(thread) {
|
|
|
255
259
|
const lastTurnStatus = typeof lastTurn?.status === "string" ? lastTurn.status : null
|
|
256
260
|
// 権威シグナル: スレッドが active のときだけ「ターン実行中」。fresh resume は idle。
|
|
257
261
|
const turnActive = threadStatus === "active"
|
|
258
|
-
// active かつ最後のターンが inProgress のときのみ activeTurnId
|
|
259
|
-
//
|
|
260
|
-
// = 次の入力は turn/start になり、固着しない。
|
|
262
|
+
// active かつ最後のターンが inProgress のときのみ activeTurnId を復元する。
|
|
263
|
+
// 通常入力はこの ID がある間は次ターン FIFO で待ち、一致する completed 後に start する。
|
|
261
264
|
const activeTurnId =
|
|
262
265
|
turnActive && lastTurnStatus === "inProgress" && typeof lastTurn?.id === "string"
|
|
263
266
|
? lastTurn.id
|
|
@@ -269,10 +272,9 @@ export function deriveResumeTurnState(thread) {
|
|
|
269
272
|
* 配達保証 (S2, 2026-07-05): `input()` が reject したエラーを `codex.input.nack` の
|
|
270
273
|
* `reason` へ分類する。
|
|
271
274
|
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
* 付与するようになった。付与されていればそれを最優先で採用し (main.mjs が組み立てる
|
|
275
|
+
* `CodexAppServerClient.request()` のタイムアウト等、bridge が判断できる失敗は reject する
|
|
276
|
+
* Error に権威ある `.reason` を直接付与する。付与されていればそれを最優先で採用し
|
|
277
|
+
* (main.mjs が組み立てる
|
|
276
278
|
* `codex.error.reason` と同じ値になり、frontend 側の分岐を一致させられる)、付与されて
|
|
277
279
|
* いない場合 (例: `_require()` の「セッション不在」) のみメッセージ文言からの
|
|
278
280
|
* ベストエフォート分類にフォールバックする。
|
|
@@ -481,19 +483,20 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
481
483
|
// この session を引き当て _tryReattach の session.ready 合流に入り、二重 app-server を作らない。
|
|
482
484
|
resumeThreadId: resumeThreadId || null,
|
|
483
485
|
activeTurnId: null,
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
// pendingInputs の二重路 (queue 経路 + 直通経路) が overtaking と並行リトライ二重ターンの温床だった。
|
|
486
|
+
// 通常入力は session 単位の次ターン FIFO。activeTurnId がある間は App Server へ
|
|
487
|
+
// turn/steer せずここで待ち、対象 turn/completed 後に 1 件ずつ turn/start する。
|
|
487
488
|
/** @type {Array<{input: object[], model?: string, effort?: string, resolve: Function,
|
|
488
|
-
* reject: Function,
|
|
489
|
-
* sourceStreamId?: string}>}
|
|
490
|
-
* 全 input() はここに積まれ、単一 pump (_pumpInputs) が **1 件ずつ直列**に処理する。直通経路を
|
|
491
|
-
* 廃したので、drain 中の新規 input が queue を追い越したり、並行 turn/start・steer を発行して
|
|
492
|
-
* 二重ターンになる事故が起きない。各 promise は **実 delivery (turn/start か turn/steer の RPC)**で
|
|
493
|
-
* settle する (main.mjs が codex.input.ack/nack を実配達に合わせて出せる)。 */
|
|
489
|
+
* reject: Function, deliveryId?: string, inputId?: string, text: string,
|
|
490
|
+
* sourceStreamId?: string, acceptedAsQueued?: boolean}>} */
|
|
494
491
|
inputQueue: [],
|
|
495
492
|
/** round3 major#1: pump が走行中か (単一 pump を保証する singleflight フラグ)。 */
|
|
496
493
|
pumping: false,
|
|
494
|
+
/** turn/start RPC を送出中の item。queue snapshot では pending から除外する。 */
|
|
495
|
+
dispatchingInput: null,
|
|
496
|
+
/** queue_state の session 内単調増加版番号。generation は session.seq を使う。 */
|
|
497
|
+
queueStateSeq: 0,
|
|
498
|
+
/** ack 喪失からの自己回復用、受理済み browser input_id の bounded ring。 */
|
|
499
|
+
acceptedInputIds: [],
|
|
497
500
|
/** @type {{resolve: Function, timer: any} | null} round3 major#2: turn/start 後に turn/started
|
|
498
501
|
* (activeTurnId 確定) を待つ barrier の待機者。timeout 付きでハングを防ぐ。 */
|
|
499
502
|
_turnActiveWaiter: null,
|
|
@@ -943,6 +946,82 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
943
946
|
return Array.from(new Set(this.sessions.values())).filter((s) => !s.dead)
|
|
944
947
|
}
|
|
945
948
|
|
|
949
|
+
_recordAcceptedInput(session, inputId) {
|
|
950
|
+
if (!session || typeof inputId !== "string" || !inputId) return
|
|
951
|
+
const ids = session.acceptedInputIds || (session.acceptedInputIds = [])
|
|
952
|
+
const at = ids.indexOf(inputId)
|
|
953
|
+
if (at !== -1) ids.splice(at, 1)
|
|
954
|
+
ids.push(inputId)
|
|
955
|
+
if (ids.length > ACCEPTED_INPUT_ID_RING_MAX) {
|
|
956
|
+
ids.splice(0, ids.length - ACCEPTED_INPUT_ID_RING_MAX)
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
_pendingInputItems(session) {
|
|
961
|
+
if (!session || !Array.isArray(session.inputQueue)) return []
|
|
962
|
+
return session.inputQueue.filter((item) => item !== session.dispatchingInput)
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/** Claude の queue_state と同じ payload を Codex notification 上へ載せる。 */
|
|
966
|
+
queueState(stream_id) {
|
|
967
|
+
const session = this._require(stream_id)
|
|
968
|
+
const pending = this._pendingInputItems(session)
|
|
969
|
+
return {
|
|
970
|
+
type: "queue_state",
|
|
971
|
+
pending: pending.length,
|
|
972
|
+
messages: pending.map((item) => item.text),
|
|
973
|
+
items: pending.map((item) => ({ id: item.inputId || item.deliveryId || "", text: item.text })),
|
|
974
|
+
seq: session.queueStateSeq || 0,
|
|
975
|
+
generation: session.seq,
|
|
976
|
+
accepted: [...(session.acceptedInputIds || [])],
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/** 次ターン FIFO に残っている browser 入力を id 指定で取り消す。
|
|
981
|
+
* dispatchingInput は既に turn/start RPC 中なので対象外(実行開始済みとして扱う)。 */
|
|
982
|
+
cancelQueued({ stream_id, id }) {
|
|
983
|
+
const session = this._require(stream_id)
|
|
984
|
+
if (!id) return false
|
|
985
|
+
const index = session.inputQueue.findIndex(
|
|
986
|
+
(item) =>
|
|
987
|
+
(item?.inputId === id || item?.deliveryId === id) &&
|
|
988
|
+
item !== session.dispatchingInput,
|
|
989
|
+
)
|
|
990
|
+
if (index < 0) {
|
|
991
|
+
this._emitQueueState(session)
|
|
992
|
+
return false
|
|
993
|
+
}
|
|
994
|
+
const [item] = session.inputQueue.splice(index, 1)
|
|
995
|
+
if (!item.acceptedAsQueued) {
|
|
996
|
+
const err = Object.assign(new Error("codex queued input cancelled"), {
|
|
997
|
+
reason: "cancelled",
|
|
998
|
+
})
|
|
999
|
+
item.reject(err)
|
|
1000
|
+
}
|
|
1001
|
+
this._emitQueueState(session)
|
|
1002
|
+
return true
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
_emitQueueState(session, startedItem = null) {
|
|
1006
|
+
if (!session || session.dead) return
|
|
1007
|
+
session.queueStateSeq = (session.queueStateSeq || 0) + 1
|
|
1008
|
+
const snapshot = this.queueState(session.stream_id)
|
|
1009
|
+
if (startedItem?.text) {
|
|
1010
|
+
snapshot.started = [startedItem.text]
|
|
1011
|
+
snapshot.startedIds = [startedItem.inputId || startedItem.deliveryId || null]
|
|
1012
|
+
} else {
|
|
1013
|
+
snapshot.started = []
|
|
1014
|
+
snapshot.startedIds = []
|
|
1015
|
+
}
|
|
1016
|
+
this.emit("event", {
|
|
1017
|
+
stream_id: session.stream_id,
|
|
1018
|
+
producer_seq: session.seq,
|
|
1019
|
+
thread_id: session.threadId,
|
|
1020
|
+
cwd: session.cwd,
|
|
1021
|
+
notification: { method: "cockpit/queue_state", params: snapshot },
|
|
1022
|
+
})
|
|
1023
|
+
}
|
|
1024
|
+
|
|
946
1025
|
/** 連携カード種別 + 配達確認: 注入本文 → entry を session の FIFO に控える。sendToCwd/
|
|
947
1026
|
* sendToStream が input() を呼ぶ直前に記録し、直後の userMessage echo を
|
|
948
1027
|
* _tagInjectSourceOnItem が拾う。entry 自体が delivery promise の handle であり、
|
|
@@ -1120,7 +1199,27 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1120
1199
|
)
|
|
1121
1200
|
break
|
|
1122
1201
|
case "turn/completed":
|
|
1123
|
-
|
|
1202
|
+
{
|
|
1203
|
+
const completedTurnId = msg.params?.turn?.id || null
|
|
1204
|
+
// App Server の対象 turn と現在の権威 activeTurnId が一致したときだけ idle 化する。
|
|
1205
|
+
// 遅延した別 turn の completed で現ターンを落として FIFO を早期排出しない。
|
|
1206
|
+
if (
|
|
1207
|
+
session.activeTurnId &&
|
|
1208
|
+
completedTurnId !== session.activeTurnId
|
|
1209
|
+
) {
|
|
1210
|
+
this.logger?.warn?.(
|
|
1211
|
+
{
|
|
1212
|
+
stream_id: session.stream_id,
|
|
1213
|
+
thread_id: session.threadId,
|
|
1214
|
+
active_turn_id: session.activeTurnId,
|
|
1215
|
+
completed_turn_id: completedTurnId,
|
|
1216
|
+
},
|
|
1217
|
+
"ignored stale codex turn/completed",
|
|
1218
|
+
)
|
|
1219
|
+
break
|
|
1220
|
+
}
|
|
1221
|
+
session.activeTurnId = null
|
|
1222
|
+
}
|
|
1124
1223
|
// round3 major#2: turn/started を取りこぼしたまま turn/completed が来た異常時も barrier が
|
|
1125
1224
|
// 固着しないよう解除する (pump が次 item を fresh に turn/start できる)。
|
|
1126
1225
|
this._settleTurnActiveWaiter(session)
|
|
@@ -1138,6 +1237,8 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1138
1237
|
)
|
|
1139
1238
|
// Stage 5: turn 完了後の reap 予約は不要。以降 idle になれば周期 reconcile が
|
|
1140
1239
|
// 長 idle TTL / LRU で扱う(detach 済みでも 7 日 warm 保持)。
|
|
1240
|
+
// 通常送信 FIFO は matching turn/completed だけを排出境界にする。
|
|
1241
|
+
void this._pumpInputs(session)
|
|
1141
1242
|
break
|
|
1142
1243
|
case "item/completed":
|
|
1143
1244
|
// T06541 案1: 完了した ThreadItem を高忠実度履歴に追記する(cwd reattach で返す用)。
|
|
@@ -1258,176 +1359,142 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1258
1359
|
* @param {string} [args.effort] - 公式スキーマ `TurnStartParams.effort`
|
|
1259
1360
|
* ("Override the reasoning effort for this turn and subsequent turns")。turn/start でのみ有効。
|
|
1260
1361
|
* @param {string} [args.deliveryId] sibling-inject の内部相関 ID
|
|
1261
|
-
* @
|
|
1262
|
-
*
|
|
1263
|
-
* ⚠️ `turn/steer` (`TurnSteerParams`) には model/effort フィールドが存在しない
|
|
1264
|
-
* (公式スキーマで未対応と確認済み、2026-07-03)。そのためターン実行中 (busy) に
|
|
1265
|
-
* model/effort を渡しても turn/steer 経由では反映されず、次の非 busy な input()
|
|
1266
|
-
* (= turn/start) から有効になる。呼び出し側 (main.mjs) はこれを承知の上で常に
|
|
1267
|
-
* model/effort を渡してよい (steer 分岐では単に無視される)。
|
|
1362
|
+
* @param {string} [args.inputId] browser の配達確認・キュー表示用 ID
|
|
1363
|
+
* @returns {Promise<{method: "turn/start"|"queued", deliveryId?: string}>}
|
|
1268
1364
|
*/
|
|
1269
|
-
async input({ stream_id, message, model, effort, deliveryId }) {
|
|
1365
|
+
async input({ stream_id, message, model, effort, deliveryId, inputId }) {
|
|
1270
1366
|
const session = this._require(stream_id)
|
|
1271
1367
|
// Stage 5: _touch で lastActivityMs を更新 = LRU 先頭へ。ターン開始で activeTurnId が
|
|
1272
1368
|
// 立ち、reconcile は healthy busy を protected 扱いにするので実行中に kill されない。
|
|
1273
1369
|
// 案B: stream_id を渡し端末別活性も更新(死端末 GC が入力継続端末を掃除しない)。
|
|
1274
1370
|
this._touch(session, stream_id)
|
|
1275
1371
|
const input = userInputFromMessage(message)
|
|
1276
|
-
|
|
1277
|
-
|
|
1372
|
+
const text = input.map((part) => part?.text || "").join("\n")
|
|
1373
|
+
if (inputId && session.acceptedInputIds?.includes(inputId)) {
|
|
1374
|
+
return { method: session.activeTurnId ? "queued" : "turn/start", deliveryId }
|
|
1375
|
+
}
|
|
1376
|
+
const queuedAtArrival =
|
|
1377
|
+
!!session.activeTurnId ||
|
|
1378
|
+
session.pumping === true ||
|
|
1379
|
+
!!session._turnActiveWaiter
|
|
1380
|
+
this._recordAcceptedInput(session, inputId)
|
|
1278
1381
|
return new Promise((resolve, reject) => {
|
|
1279
|
-
|
|
1382
|
+
const item = {
|
|
1280
1383
|
input,
|
|
1384
|
+
text,
|
|
1281
1385
|
model,
|
|
1282
1386
|
effort,
|
|
1283
1387
|
deliveryId,
|
|
1388
|
+
inputId,
|
|
1284
1389
|
sourceStreamId: stream_id,
|
|
1285
1390
|
resolve,
|
|
1286
1391
|
reject,
|
|
1287
|
-
|
|
1288
|
-
}
|
|
1392
|
+
acceptedAsQueued: queuedAtArrival && !!inputId,
|
|
1393
|
+
}
|
|
1394
|
+
session.inputQueue.push(item)
|
|
1395
|
+
if (item.acceptedAsQueued) {
|
|
1396
|
+
resolve({ method: "queued", deliveryId })
|
|
1397
|
+
this.logger?.info?.(
|
|
1398
|
+
{
|
|
1399
|
+
stream_id,
|
|
1400
|
+
thread_id: session.threadId,
|
|
1401
|
+
input_id: inputId,
|
|
1402
|
+
queued: this._pendingInputItems(session).length,
|
|
1403
|
+
active_turn_id: session.activeTurnId,
|
|
1404
|
+
},
|
|
1405
|
+
"codex busy, message queued for next turn",
|
|
1406
|
+
)
|
|
1407
|
+
this._emitQueueState(session)
|
|
1408
|
+
}
|
|
1289
1409
|
void this._pumpInputs(session)
|
|
1290
1410
|
})
|
|
1291
1411
|
}
|
|
1292
1412
|
|
|
1293
1413
|
/**
|
|
1294
|
-
*
|
|
1295
|
-
*
|
|
1296
|
-
*
|
|
1297
|
-
* turn/started (activeTurnId 確定) を **timeout 付き**で待つ (round3 major#2: ハング防止)。
|
|
1298
|
-
* - busy (activeTurnId 有り): turn/steer で追い打ち。
|
|
1299
|
-
* - turn/steer 失敗 (expectedTurnId 不一致など): activeTurnId を捨て、同 item を **1 回だけ**
|
|
1300
|
-
* turn/start にフォールバックして自己修復する (旧 self-heal を FIFO 内で継続。並行はしない)。
|
|
1301
|
-
* 各 item の promise は実 delivery の RPC 結果で settle する。
|
|
1414
|
+
* 通常送信の次ターン FIFO pump。activeTurnId がある間は何も App Server へ送らず、
|
|
1415
|
+
* matching turn/completed が activeTurnId を解除した後だけ先頭を turn/start する。
|
|
1416
|
+
* これにより通常クリックと明示的な turn/steer / turn/interrupt の意味を分離する。
|
|
1302
1417
|
*/
|
|
1303
1418
|
async _pumpInputs(session) {
|
|
1304
|
-
if (session.pumping) return
|
|
1419
|
+
if (session.pumping || session.activeTurnId) return
|
|
1305
1420
|
session.pumping = true
|
|
1306
1421
|
try {
|
|
1307
1422
|
while (session.inputQueue.length > 0) {
|
|
1423
|
+
if (session.activeTurnId) return
|
|
1308
1424
|
if (session.dead) {
|
|
1309
1425
|
this._drainRejectQueue(session, { reason: "queue_dead" })
|
|
1310
1426
|
return
|
|
1311
1427
|
}
|
|
1312
1428
|
const item = session.inputQueue[0]
|
|
1313
|
-
|
|
1314
|
-
// turn/completed が来て session.activeTurnId が null に戻ると、catch 時点の値では steer 試行が
|
|
1315
|
-
// start と誤判定され、self-heal リトライがスキップされて item が誤 reject される。
|
|
1316
|
-
const attemptedSteer = !!session.activeTurnId
|
|
1429
|
+
session.dispatchingInput = item
|
|
1317
1430
|
try {
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
} else {
|
|
1339
|
-
// round4 major#2: turn/start dispatch の **前に** 世代を控える。start 応答後に
|
|
1340
|
-
// turn/started(+completed) が pump 再開前に処理されても、世代前進で「ターンが始まった」を
|
|
1341
|
-
// 検知でき、誤 timeout しない。
|
|
1342
|
-
const genBefore = session.turnGeneration
|
|
1343
|
-
const params = { threadId: session.threadId, input: item.input }
|
|
1344
|
-
if (item.model) params.model = item.model
|
|
1345
|
-
if (item.effort) params.effort = item.effort
|
|
1346
|
-
await session.client.request("turn/start", params)
|
|
1347
|
-
session.inputQueue.shift()
|
|
1348
|
-
this.logger?.info?.(
|
|
1349
|
-
{
|
|
1350
|
-
delivery_id: item.deliveryId || null,
|
|
1351
|
-
stream_id: item.sourceStreamId || session.stream_id,
|
|
1352
|
-
current_stream_id: session.stream_id,
|
|
1353
|
-
thread_id: session.threadId,
|
|
1354
|
-
attempted_steer: false,
|
|
1355
|
-
rpc_method: "turn/start",
|
|
1356
|
-
rpc_result: "accepted",
|
|
1357
|
-
},
|
|
1358
|
-
"codex input RPC settled",
|
|
1359
|
-
)
|
|
1431
|
+
const genBefore = session.turnGeneration
|
|
1432
|
+
const params = { threadId: session.threadId, input: item.input }
|
|
1433
|
+
if (item.model) params.model = item.model
|
|
1434
|
+
if (item.effort) params.effort = item.effort
|
|
1435
|
+
await session.client.request("turn/start", params)
|
|
1436
|
+
session.inputQueue.shift()
|
|
1437
|
+
session.dispatchingInput = null
|
|
1438
|
+
this.logger?.info?.(
|
|
1439
|
+
{
|
|
1440
|
+
delivery_id: item.deliveryId || null,
|
|
1441
|
+
input_id: item.inputId || null,
|
|
1442
|
+
stream_id: item.sourceStreamId || session.stream_id,
|
|
1443
|
+
current_stream_id: session.stream_id,
|
|
1444
|
+
thread_id: session.threadId,
|
|
1445
|
+
rpc_method: "turn/start",
|
|
1446
|
+
rpc_result: "accepted",
|
|
1447
|
+
},
|
|
1448
|
+
"codex input RPC settled",
|
|
1449
|
+
)
|
|
1450
|
+
if (!item.acceptedAsQueued) {
|
|
1360
1451
|
item.resolve({ method: "turn/start", deliveryId: item.deliveryId })
|
|
1361
|
-
// 次の item を steer にするため turn/started (= 世代前進) を待つ。timeout でハングを防ぐ。
|
|
1362
|
-
// timeout したら「ターンが正しく始まらなかった」とみなし残り queue を reject して打ち切る。
|
|
1363
|
-
const becameActive = await this._waitTurnActive(session, genBefore)
|
|
1364
|
-
if (!becameActive) {
|
|
1365
|
-
this._drainRejectQueue(session, { reason: "turn_start_timeout" })
|
|
1366
|
-
return
|
|
1367
|
-
}
|
|
1368
1452
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
stream_id: item.sourceStreamId || session.stream_id,
|
|
1375
|
-
current_stream_id: session.stream_id,
|
|
1376
|
-
thread_id: session.threadId,
|
|
1377
|
-
attempted_steer: true,
|
|
1378
|
-
rpc_method: "turn/steer",
|
|
1379
|
-
rpc_result: "retry_as_start",
|
|
1380
|
-
err: err instanceof Error ? err.message : String(err),
|
|
1381
|
-
},
|
|
1382
|
-
"codex input RPC settled",
|
|
1383
|
-
)
|
|
1384
|
-
// turn/steer 失敗 → 自己修復: activeTurnId を捨て、同 item を turn/start で 1 回だけリトライ
|
|
1385
|
-
// (shift しない = 同 item を次ループの start 経路で再処理)。並行はしない (単一 pump)。
|
|
1386
|
-
// 元の steer エラーを stash して、リトライも失敗したとき合成メッセージを作れるようにする。
|
|
1387
|
-
session.activeTurnId = null
|
|
1388
|
-
item.retriedAsStart = true
|
|
1389
|
-
item.priorSteerError = err instanceof Error ? err : new Error(String(err))
|
|
1390
|
-
continue
|
|
1453
|
+
this._emitQueueState(session, item.acceptedAsQueued ? item : null)
|
|
1454
|
+
const becameActive = await this._waitTurnActive(session, genBefore)
|
|
1455
|
+
if (!becameActive) {
|
|
1456
|
+
this._drainRejectQueue(session, { reason: "turn_start_timeout" })
|
|
1457
|
+
return
|
|
1391
1458
|
}
|
|
1392
|
-
//
|
|
1393
|
-
//
|
|
1459
|
+
// 通常は active のままなので matching turn/completed まで停止する。start 応答前に
|
|
1460
|
+
// started+completed 済みの fast turn だけ、そのまま次の FIFO item へ進める。
|
|
1461
|
+
if (session.activeTurnId) return
|
|
1462
|
+
} catch (err) {
|
|
1394
1463
|
session.inputQueue.shift()
|
|
1395
|
-
session.
|
|
1396
|
-
|
|
1397
|
-
if (
|
|
1398
|
-
// steer→start リトライがともに失敗: 合成メッセージ + reason (main.mjs の classifyCodexInputError
|
|
1399
|
-
// が reason を優先採用。従来の単一 input() 実装と同じ診断を保つ)。
|
|
1400
|
-
e = Object.assign(
|
|
1401
|
-
new Error(
|
|
1402
|
-
`turn/steer failed (${item.priorSteerError.message}) and turn/start retry also failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1403
|
-
),
|
|
1404
|
-
{ reason: "turn_mismatch_retry_failed", cause: err },
|
|
1405
|
-
)
|
|
1406
|
-
} else {
|
|
1407
|
-
e = err instanceof Error ? err : new Error(String(err))
|
|
1408
|
-
if (!e.reason) e.reason = "start_error"
|
|
1409
|
-
}
|
|
1464
|
+
session.dispatchingInput = null
|
|
1465
|
+
const e = err instanceof Error ? err : new Error(String(err))
|
|
1466
|
+
if (!e.reason) e.reason = "start_error"
|
|
1410
1467
|
this.logger?.error?.(
|
|
1411
1468
|
{
|
|
1412
1469
|
delivery_id: item.deliveryId || null,
|
|
1470
|
+
input_id: item.inputId || null,
|
|
1413
1471
|
stream_id: item.sourceStreamId || session.stream_id,
|
|
1414
1472
|
current_stream_id: session.stream_id,
|
|
1415
1473
|
thread_id: session.threadId,
|
|
1416
|
-
|
|
1417
|
-
rpc_method: attemptedSteer ? "turn/steer" : "turn/start",
|
|
1474
|
+
rpc_method: "turn/start",
|
|
1418
1475
|
rpc_result: "failed",
|
|
1419
1476
|
err: e.message,
|
|
1420
1477
|
reason: e.reason || null,
|
|
1421
1478
|
},
|
|
1422
1479
|
"codex input RPC settled",
|
|
1423
1480
|
)
|
|
1424
|
-
item.
|
|
1481
|
+
if (item.acceptedAsQueued) {
|
|
1482
|
+
this.emit("error", {
|
|
1483
|
+
stream_id: item.sourceStreamId || session.stream_id,
|
|
1484
|
+
thread_id: session.threadId,
|
|
1485
|
+
error: e.message,
|
|
1486
|
+
})
|
|
1487
|
+
} else {
|
|
1488
|
+
item.reject(e)
|
|
1489
|
+
}
|
|
1490
|
+
this._emitQueueState(session)
|
|
1425
1491
|
}
|
|
1426
1492
|
}
|
|
1427
1493
|
} finally {
|
|
1494
|
+
session.dispatchingInput = null
|
|
1428
1495
|
session.pumping = false
|
|
1429
1496
|
// pumping=false 直後の enqueue 競合窓を拾い直す (取りこぼし防止)。再帰は 1 段のみ。
|
|
1430
|
-
if (session.inputQueue.length > 0 && !session.dead) {
|
|
1497
|
+
if (session.inputQueue.length > 0 && !session.dead && !session.activeTurnId) {
|
|
1431
1498
|
void this._pumpInputs(session)
|
|
1432
1499
|
}
|
|
1433
1500
|
}
|
|
@@ -1486,15 +1553,24 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1486
1553
|
if (!session || !Array.isArray(session.inputQueue) || session.inputQueue.length === 0) return
|
|
1487
1554
|
const pending = session.inputQueue
|
|
1488
1555
|
session.inputQueue = []
|
|
1556
|
+
session.dispatchingInput = null
|
|
1489
1557
|
const reason = cause?.reason || "queue_dead"
|
|
1490
|
-
for (const
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
cause,
|
|
1495
|
-
}),
|
|
1558
|
+
for (const item of pending) {
|
|
1559
|
+
const error = Object.assign(
|
|
1560
|
+
new Error("codex input dropped: turn could not be delivered"),
|
|
1561
|
+
{ reason, cause },
|
|
1496
1562
|
)
|
|
1563
|
+
if (item.acceptedAsQueued) {
|
|
1564
|
+
this.emit("error", {
|
|
1565
|
+
stream_id: item.sourceStreamId || session.stream_id,
|
|
1566
|
+
thread_id: session.threadId,
|
|
1567
|
+
error: error.message,
|
|
1568
|
+
})
|
|
1569
|
+
} else {
|
|
1570
|
+
item.reject(error)
|
|
1571
|
+
}
|
|
1497
1572
|
}
|
|
1573
|
+
if (!session.dead) this._emitQueueState(session)
|
|
1498
1574
|
}
|
|
1499
1575
|
|
|
1500
1576
|
/** @returns {Promise<void>} */
|
package/src/main.mjs
CHANGED
|
@@ -853,8 +853,9 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
853
853
|
// ドーナツ) と session.event (ソート) に橋渡しする。チャットは tmux ペインに
|
|
854
854
|
// TUI を出さず、capture-pane スクレイプも bundle hook も発火しないための補完。
|
|
855
855
|
// - assistant (生成中) → processing + usage から context%
|
|
856
|
-
// -
|
|
857
|
-
//
|
|
856
|
+
// - session_state_changed:idle → waiting
|
|
857
|
+
// result は結果 flush であり、SDK が権威的ターン終了と定義する idle より早く届く。
|
|
858
|
+
// assistant / idle はターンの進行/完了 = permission 応答後に必ず流れるため、
|
|
858
859
|
// ここで inputPending を落として「確認待ち」ドット/通知を自己解消させる。
|
|
859
860
|
if (event?.type === "assistant") {
|
|
860
861
|
// 分母は result で学習したセッション固有の窓サイズ (無ければグローバル概算)。
|
|
@@ -876,6 +877,11 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
876
877
|
} else if (event?.type === "result") {
|
|
877
878
|
// SDK の権威的な窓サイズ (result.modelUsage[model].contextWindow) をここで学習する。
|
|
878
879
|
resolveChatContextPct(cwd, "claude", event)
|
|
880
|
+
} else if (
|
|
881
|
+
event?.type === "system" &&
|
|
882
|
+
event?.subtype === "session_state_changed" &&
|
|
883
|
+
event?.state === "idle"
|
|
884
|
+
) {
|
|
879
885
|
try {
|
|
880
886
|
recordChatActivity(cwd, "claude", {
|
|
881
887
|
status: "waiting",
|
|
@@ -3134,8 +3140,8 @@ export function buildAttachHistoryPromise({
|
|
|
3134
3140
|
/**
|
|
3135
3141
|
* SDK チャット配達保証 (S2, 2026-07-05): `codex.input` を処理し、browser が付与した
|
|
3136
3142
|
* `input_id` があれば `codex.input.ack` / `codex.input.nack` を返す (claude.input と
|
|
3137
|
-
* 対称)
|
|
3138
|
-
*
|
|
3143
|
+
* 対称)。idle 入力は turn/start の受理、生成中入力は hub-agent FIFO への受理をもって
|
|
3144
|
+
* ack する。FIFO の実配送は matching turn/completed 後に bridge が行う。
|
|
3139
3145
|
* 既存の `codex.error` 通知も維持する (input_id を知らない旧 frontend や、他の消費者向け)。
|
|
3140
3146
|
* テストから直接呼べるよう dispatch から切り出して export する。
|
|
3141
3147
|
*/
|
|
@@ -3172,6 +3178,7 @@ export function handleCodexChatInput(msg, ctx) {
|
|
|
3172
3178
|
message: msg.message,
|
|
3173
3179
|
model: msg.model || undefined,
|
|
3174
3180
|
effort: msg.effort || undefined,
|
|
3181
|
+
inputId: input_id || undefined,
|
|
3175
3182
|
})
|
|
3176
3183
|
.then(() => {
|
|
3177
3184
|
if (!input_id) return
|
|
@@ -3937,6 +3944,9 @@ async function dispatch(msg, ctx) {
|
|
|
3937
3944
|
turn_active: info.turnActive === true,
|
|
3938
3945
|
thread_status: info.threadStatus || null,
|
|
3939
3946
|
last_turn_status: info.lastTurnStatus || null,
|
|
3947
|
+
// Codex の通常送信 FIFO。ready と同じ封筒に初期 snapshot を載せ、
|
|
3948
|
+
// reattach / reload でも送信待ち表示を即時復元する。
|
|
3949
|
+
queue_state: ctx.codexBridge.queueState(stream_id),
|
|
3940
3950
|
})
|
|
3941
3951
|
// S1 (2026-07): claude.attach と対称の identity 正本化。Codex は thread/start
|
|
3942
3952
|
// 応答時点で threadId が確定するため (Claude の fresh とは異なり)、fresh でも
|
|
@@ -4073,15 +4083,23 @@ async function dispatch(msg, ctx) {
|
|
|
4073
4083
|
return
|
|
4074
4084
|
}
|
|
4075
4085
|
case "codex.input":
|
|
4076
|
-
//
|
|
4077
|
-
//
|
|
4078
|
-
// codex-stream-bridge.mjs 側で無視され次の非 busy な input() から効く。
|
|
4079
|
-
// S2/S3 統合 (2026-07-05): S3 の自己修復 (turn/steer 失敗 → turn/start 自動
|
|
4080
|
-
// リトライ) が付与する err.reason ('timeout' / 'turn_mismatch_retry_failed') を
|
|
4081
|
-
// classifyCodexInputError が優先採用し、input_id 付きなら codex.input.nack の
|
|
4082
|
-
// reason としてそのまま使う (handleCodexChatInput 内)。
|
|
4086
|
+
// 通常送信は idle なら turn/start、busy なら hub-agent の次ターン FIFO。
|
|
4087
|
+
// turn/steer / turn/interrupt は明示操作の別経路として扱う。
|
|
4083
4088
|
handleCodexChatInput(msg, ctx)
|
|
4084
4089
|
return
|
|
4090
|
+
case "codex.queue.cancel":
|
|
4091
|
+
try {
|
|
4092
|
+
ctx.codexBridge?.cancelQueued({
|
|
4093
|
+
stream_id: msg.stream_id,
|
|
4094
|
+
id: msg.id,
|
|
4095
|
+
})
|
|
4096
|
+
} catch (err) {
|
|
4097
|
+
ctx.logger?.warn?.(
|
|
4098
|
+
{ err: err?.message, stream_id: msg.stream_id, input_id: msg.id },
|
|
4099
|
+
"codex queued input cancel failed",
|
|
4100
|
+
)
|
|
4101
|
+
}
|
|
4102
|
+
return
|
|
4085
4103
|
case "codex.permission.reply":
|
|
4086
4104
|
if (!ctx.codexBridge) return
|
|
4087
4105
|
ctx.codexBridge.permissionReply({
|