@cocorograph/hub-agent 0.8.10 → 0.8.12
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 +81 -40
- package/src/codex-stream-bridge.mjs +218 -142
- package/src/main.mjs +33 -11
package/package.json
CHANGED
|
@@ -525,9 +525,10 @@ export class ClaudeStreamSession {
|
|
|
525
525
|
this._busy = false
|
|
526
526
|
/** SDK-P0-3 (2026-07-10): 現ターンが既に終端処理済みか (result / idle / abort の
|
|
527
527
|
* いずれか 1 回だけ _busy=false + drain させるための冪等ガード)。resident query は
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
528
|
+
* `result` の後にもバックグラウンド処理が続き、最後に権威的な
|
|
529
|
+
* `session_state_changed:idle` が流れることがある。実 SDK では result 後に idle が
|
|
530
|
+
* 来ないターンもあるため、result / idle の先着 1 件だけが settle し、遅延 idle は
|
|
531
|
+
* _staleIdleExpected で次ターンから隔離する。ターン開始時に false へ戻す。 */
|
|
531
532
|
this._turnSettled = false
|
|
532
533
|
/** SDK-P0-3: 現ターンが 1 件でもストリームイベントを出したか。
|
|
533
534
|
* 「前ターンの遅延 idle が、drain で始まったばかりの次ターンを誤終了させる」レースの
|
|
@@ -539,9 +540,9 @@ export class ClaudeStreamSession {
|
|
|
539
540
|
this._turnProgressed = false
|
|
540
541
|
/** SDK-P0-4 (2026-07-13): ターン連番。ログと帰属デバッグ用 (_beginResidentTurn で ++)。 */
|
|
541
542
|
this._turnSeq = 0
|
|
542
|
-
/** SDK-P0-4: 「直前ターンの idle がこれから 1 件届きうる」状態。
|
|
543
|
-
*
|
|
544
|
-
*
|
|
543
|
+
/** SDK-P0-4: 「直前ターンの idle がこれから 1 件届きうる」状態。abort (interrupt) で
|
|
544
|
+
* ターンを先行して畳んだときに arm する (interrupt 後にも中断ターンの idle が流れるため)。
|
|
545
|
+
* 通常完了は idle 自身で畳むので arm しない。arm 中に届いた idle は
|
|
545
546
|
* **前ターンのもの**として 1 件だけ握り潰し、現ターンを終端しない。
|
|
546
547
|
* 解除条件: (a) idle を 1 件消費した (b) SDK の running が届いた (= 前ターンの全イベントは
|
|
547
548
|
* 順序保証により既に通過済み) (c) 現ターンの result が届いた (d) STALE_IDLE_GUARD_MS 経過。 */
|
|
@@ -1131,9 +1132,10 @@ export class ClaudeStreamSession {
|
|
|
1131
1132
|
this._busy = true
|
|
1132
1133
|
this._abortController = new AbortController()
|
|
1133
1134
|
let aborted = false
|
|
1134
|
-
//
|
|
1135
|
-
//
|
|
1135
|
+
// `session_state_changed:idle` がターン終了の正本。SDK が idle を流さず generator が
|
|
1136
|
+
// 閉じた旧/異常経路だけ合成 idle と onTurnSettled で waiting へ落とす。
|
|
1136
1137
|
let resultDelivered = false
|
|
1138
|
+
let idleDelivered = false
|
|
1137
1139
|
|
|
1138
1140
|
const options = {
|
|
1139
1141
|
cwd: this.cwd,
|
|
@@ -1185,6 +1187,7 @@ export class ClaudeStreamSession {
|
|
|
1185
1187
|
resultDelivered = true
|
|
1186
1188
|
if (typeof msg.session_id === "string") this.sessionId = msg.session_id
|
|
1187
1189
|
}
|
|
1190
|
+
if (isSessionIdleEvent(msg)) idleDelivered = true
|
|
1188
1191
|
try {
|
|
1189
1192
|
this._emit(msg)
|
|
1190
1193
|
} catch (err) {
|
|
@@ -1224,12 +1227,9 @@ export class ClaudeStreamSession {
|
|
|
1224
1227
|
if (aborted) {
|
|
1225
1228
|
this.logger?.info({ stream_id: this.stream_id }, "claude turn aborted")
|
|
1226
1229
|
}
|
|
1227
|
-
// RC-4:
|
|
1228
|
-
//
|
|
1229
|
-
|
|
1230
|
-
// result が既に waiting にしているので !resultDelivered のときだけ。_drainPending で
|
|
1231
|
-
// 次ターン (processing) が始まる前に呼ぶこと (順序: ここ → reap/drain)。
|
|
1232
|
-
if (!resultDelivered) {
|
|
1230
|
+
// RC-4: idle を届けずに generator が閉じた旧/異常経路では、チャット信号と browser の
|
|
1231
|
+
// 双方へ同じ終了境界を補う。_drainPending で次ターンを始める前に行うこと。
|
|
1232
|
+
if (!idleDelivered) {
|
|
1233
1233
|
try {
|
|
1234
1234
|
this.onTurnSettled?.()
|
|
1235
1235
|
} catch {
|
|
@@ -1244,11 +1244,24 @@ export class ClaudeStreamSession {
|
|
|
1244
1244
|
// フッターで表示する)。uuid を持たせるのは重複排除 (isDuplicateEvent) が result
|
|
1245
1245
|
// 署名 (session_id + num_turns + duration_ms + cost) で判定するため — 合成 result
|
|
1246
1246
|
// はこれらが毎回同値になり、2 回目以降の中断で「重複」と誤判定され捨てられるのを防ぐ。
|
|
1247
|
+
if (!resultDelivered) {
|
|
1248
|
+
try {
|
|
1249
|
+
this._emit({
|
|
1250
|
+
type: "result",
|
|
1251
|
+
subtype: aborted ? "aborted_by_user" : "turn_settled",
|
|
1252
|
+
uuid: randomUUID(),
|
|
1253
|
+
session_id: this.sessionId ?? undefined,
|
|
1254
|
+
timestamp: new Date().toISOString(),
|
|
1255
|
+
})
|
|
1256
|
+
} catch {
|
|
1257
|
+
/* ignore */
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1247
1260
|
try {
|
|
1248
1261
|
this._emit({
|
|
1249
|
-
type: "
|
|
1250
|
-
subtype:
|
|
1251
|
-
|
|
1262
|
+
type: "system",
|
|
1263
|
+
subtype: "session_state_changed",
|
|
1264
|
+
state: "idle",
|
|
1252
1265
|
session_id: this.sessionId ?? undefined,
|
|
1253
1266
|
timestamp: new Date().toISOString(),
|
|
1254
1267
|
})
|
|
@@ -1618,9 +1631,10 @@ export class ClaudeStreamSession {
|
|
|
1618
1631
|
}
|
|
1619
1632
|
}
|
|
1620
1633
|
|
|
1621
|
-
/**
|
|
1622
|
-
*
|
|
1623
|
-
*
|
|
1634
|
+
/** resident の「ターン終端」を 1 箇所に集約する。
|
|
1635
|
+
* SDK は result 後に session_state_changed:idle を返すことがあるが、実環境では
|
|
1636
|
+
* idle が欠落するターンもある。result / idle の先着を終端とし、result で畳んだ後の
|
|
1637
|
+
* 遅延 idle はガードで次ターンから隔離する。
|
|
1624
1638
|
* @param {string} reason "result" | "idle" — ログ用
|
|
1625
1639
|
* @param {(msg: string) => void} [denyPending] 未解決 permission を閉じるクロージャ
|
|
1626
1640
|
*/
|
|
@@ -1629,10 +1643,9 @@ export class ClaudeStreamSession {
|
|
|
1629
1643
|
this._turnSettled = true
|
|
1630
1644
|
denyPending?.("turn ended")
|
|
1631
1645
|
this._busy = false
|
|
1632
|
-
//
|
|
1633
|
-
//
|
|
1634
|
-
//
|
|
1635
|
-
// idle 自身で畳んだ場合は既にその idle を消費済みなので張らない。
|
|
1646
|
+
// result の後に同一ターンの idle が遅れて届く場合がある。pending drain は同期的に
|
|
1647
|
+
// 次ターンの busy を立てるため、その idle を新ターンへ誤帰属させないよう先にガードする。
|
|
1648
|
+
// idle 自身で畳んだ場合は既に消費済みなので張らない。
|
|
1636
1649
|
if (reason === "result") this._armStaleIdleGuard()
|
|
1637
1650
|
// 改修4 (A): シリアライズした pending があれば次の 1 件を InputQueue へ流す。
|
|
1638
1651
|
// ultracode (0.6.28): _drainResidentPending は applyFlagSettings を await するため
|
|
@@ -1649,7 +1662,7 @@ export class ClaudeStreamSession {
|
|
|
1649
1662
|
/** 改修4 (A): ターン完了時に pending の先頭 1 件を InputQueue へ流す (ターンのシリアライズ)。
|
|
1650
1663
|
* queue_state を更新して送信待ちチップを drain させる (frontend がバブル昇格する)。
|
|
1651
1664
|
* ultracode (0.6.28): 次ターンの目標 ultracode 状態へ reconcile してから push する
|
|
1652
|
-
* (await するため async 化。
|
|
1665
|
+
* (await するため async 化。idle ハンドラからは fire-and-forget で呼ばれる)。 */
|
|
1653
1666
|
async _drainResidentPending() {
|
|
1654
1667
|
if (this._closed) return
|
|
1655
1668
|
if (this._pendingMessages.length === 0) {
|
|
@@ -1693,7 +1706,7 @@ export class ClaudeStreamSession {
|
|
|
1693
1706
|
|
|
1694
1707
|
/** 改修2+4: 常駐 query を 1 回だけ起動し、streaming input キューから複数ターンを処理する。
|
|
1695
1708
|
* system/init は最初の 1 回のみ (毎ターン init = 文脈/キャッシュ温め直しを解消)。各ターンは
|
|
1696
|
-
*
|
|
1709
|
+
* 権威的終端シグナル (session_state_changed:idle) で _busy=false にするが query は継続し
|
|
1697
1710
|
* 次 input を待つ。close (inputQueue.close) で generator が終わり query が完走する。
|
|
1698
1711
|
* SDK-P0-3 (2026-07-10): 終端を result だけに頼ると、SDK が idle でターンを終えたときに
|
|
1699
1712
|
* _busy が固着し、以降の送信が pending へ永久滞留する (T06052)。
|
|
@@ -1749,6 +1762,7 @@ export class ClaudeStreamSession {
|
|
|
1749
1762
|
const q = this.sdk.query({ prompt: this._inputQueue, options })
|
|
1750
1763
|
this._residentQuery = q
|
|
1751
1764
|
for await (const msg of q) {
|
|
1765
|
+
let suppressEmit = false
|
|
1752
1766
|
if (
|
|
1753
1767
|
msg?.type === "system" &&
|
|
1754
1768
|
msg?.subtype === "init" &&
|
|
@@ -1758,11 +1772,11 @@ export class ClaudeStreamSession {
|
|
|
1758
1772
|
}
|
|
1759
1773
|
if (msg?.type === "result") {
|
|
1760
1774
|
if (typeof msg.session_id === "string") this.sessionId = msg.session_id
|
|
1761
|
-
//
|
|
1762
|
-
//
|
|
1763
|
-
//
|
|
1775
|
+
// 現ターンの result が届いた時点で前ターンの遅延 idle は順序保証により
|
|
1776
|
+
// 既に通過済み。古いガードを解除し、result 用のガードを張り直して settle する。
|
|
1777
|
+
// 実 SDK では result 後に idle が来ないターンがあるため、idle 待ちだけにすると
|
|
1778
|
+
// _busy が永久に true のまま固着し、以後の入力が pending から drain されない。
|
|
1764
1779
|
this._clearStaleIdleGuard()
|
|
1765
|
-
// ターン完了: 未解決 permission を閉じ、次 input 受付へ (query は継続)。
|
|
1766
1780
|
this._settleResidentTurn("result", denyPending)
|
|
1767
1781
|
} else if (isSessionIdleEvent(msg)) {
|
|
1768
1782
|
// SDK-P0-3 (2026-07-10): result が届かずに idle でターンが終わるケースの救済。
|
|
@@ -1777,11 +1791,14 @@ export class ClaudeStreamSession {
|
|
|
1777
1791
|
// 2) それ以外で _busy かつ (running を観測済み or 何かイベントを出した) → 現ターンの終端。
|
|
1778
1792
|
if (this._staleIdleExpected) {
|
|
1779
1793
|
this._clearStaleIdleGuard()
|
|
1794
|
+
// browser にも転送しない。前ターンの遅延 idle を新ターンの reducer が受けると、
|
|
1795
|
+
// 3点リーダーと送信ボタンだけが誤って idle になる。
|
|
1796
|
+
suppressEmit = true
|
|
1780
1797
|
this.logger?.debug?.(
|
|
1781
1798
|
{ stream_id: this.stream_id, turn: this._turnSeq },
|
|
1782
1799
|
"stale idle from previous turn ignored",
|
|
1783
1800
|
)
|
|
1784
|
-
} else if (this._busy
|
|
1801
|
+
} else if (this._busy) {
|
|
1785
1802
|
this._settleResidentTurn("idle", denyPending)
|
|
1786
1803
|
// per-message の RC-4 と対称に、チャット信号 (サイドバードット) を
|
|
1787
1804
|
// processing→waiting へ落とす。result 経由なら main.mjs が result を見て
|
|
@@ -1804,13 +1821,15 @@ export class ClaudeStreamSession {
|
|
|
1804
1821
|
// および session_state_changed の running / requires_action もここに含む。
|
|
1805
1822
|
this._turnProgressed = true
|
|
1806
1823
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1824
|
+
if (!suppressEmit) {
|
|
1825
|
+
try {
|
|
1826
|
+
this._emit(msg)
|
|
1827
|
+
} catch (err) {
|
|
1828
|
+
this.logger?.warn(
|
|
1829
|
+
{ err: err.message, stream_id: this.stream_id },
|
|
1830
|
+
"onEvent callback threw",
|
|
1831
|
+
)
|
|
1832
|
+
}
|
|
1814
1833
|
}
|
|
1815
1834
|
}
|
|
1816
1835
|
} catch (err) {
|
|
@@ -1949,6 +1968,19 @@ export class ClaudeStreamSession {
|
|
|
1949
1968
|
} catch {
|
|
1950
1969
|
/* ignore */
|
|
1951
1970
|
}
|
|
1971
|
+
// 通常完了と同じ権威境界を browser へ即時提示する。SDK が後から流す
|
|
1972
|
+
// 中断ターン由来の実 idle は _staleIdleExpected で抑止する。
|
|
1973
|
+
try {
|
|
1974
|
+
this._emit({
|
|
1975
|
+
type: "system",
|
|
1976
|
+
subtype: "session_state_changed",
|
|
1977
|
+
state: "idle",
|
|
1978
|
+
session_id: this.sessionId ?? undefined,
|
|
1979
|
+
timestamp: new Date().toISOString(),
|
|
1980
|
+
})
|
|
1981
|
+
} catch {
|
|
1982
|
+
/* ignore */
|
|
1983
|
+
}
|
|
1952
1984
|
}
|
|
1953
1985
|
// T05699 残タスク #2-1 (2026-07-04): interrupt() は現ターンの result を発火しない
|
|
1954
1986
|
// (query は継続、次 input 受付継続のみ)。そのため abort 時点で _pendingMessages に
|
|
@@ -2256,7 +2288,12 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
2256
2288
|
},
|
|
2257
2289
|
"claude stream reattached to live session",
|
|
2258
2290
|
)
|
|
2259
|
-
return {
|
|
2291
|
+
return {
|
|
2292
|
+
stream_id,
|
|
2293
|
+
resuming: true,
|
|
2294
|
+
reattached: true,
|
|
2295
|
+
turn_active: live._busy === true,
|
|
2296
|
+
}
|
|
2260
2297
|
}
|
|
2261
2298
|
}
|
|
2262
2299
|
const session = new ClaudeStreamSession({
|
|
@@ -2369,7 +2406,11 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
2369
2406
|
)
|
|
2370
2407
|
// per-message モデルでは attach 時点では query() を起動しない (即 ready)。
|
|
2371
2408
|
// 最初の input で query を起動する。
|
|
2372
|
-
return {
|
|
2409
|
+
return {
|
|
2410
|
+
stream_id,
|
|
2411
|
+
resuming: !!resumeSessionId,
|
|
2412
|
+
turn_active: session._busy === true,
|
|
2413
|
+
}
|
|
2373
2414
|
}
|
|
2374
2415
|
|
|
2375
2416
|
/**
|
|
@@ -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
|
|
@@ -3426,6 +3433,10 @@ async function dispatch(msg, ctx) {
|
|
|
3426
3433
|
type: "claude.ready",
|
|
3427
3434
|
stream_id,
|
|
3428
3435
|
resuming: info.resuming,
|
|
3436
|
+
// 再接続時の生成状態は resident session を保持する hub-agent が正本。
|
|
3437
|
+
// jsonl 履歴末尾の推測だけでは「実際は busy なのに UI は idle」となり、
|
|
3438
|
+
// 送信ボタンが楽観バブルを作る一方 agent はキューへ積む二重表示が起きる。
|
|
3439
|
+
turn_active: info.turn_active === true,
|
|
3429
3440
|
history_pushed: resumeSessionId !== null,
|
|
3430
3441
|
// id 空間統一 (2026-07-08): この agent は browser の input_id をキュー id として
|
|
3431
3442
|
// 採用し、楽観送信待ちアイテムの確認前キャンセルに対応することを通知する capability。
|
|
@@ -3937,6 +3948,9 @@ async function dispatch(msg, ctx) {
|
|
|
3937
3948
|
turn_active: info.turnActive === true,
|
|
3938
3949
|
thread_status: info.threadStatus || null,
|
|
3939
3950
|
last_turn_status: info.lastTurnStatus || null,
|
|
3951
|
+
// Codex の通常送信 FIFO。ready と同じ封筒に初期 snapshot を載せ、
|
|
3952
|
+
// reattach / reload でも送信待ち表示を即時復元する。
|
|
3953
|
+
queue_state: ctx.codexBridge.queueState(stream_id),
|
|
3940
3954
|
})
|
|
3941
3955
|
// S1 (2026-07): claude.attach と対称の identity 正本化。Codex は thread/start
|
|
3942
3956
|
// 応答時点で threadId が確定するため (Claude の fresh とは異なり)、fresh でも
|
|
@@ -4073,15 +4087,23 @@ async function dispatch(msg, ctx) {
|
|
|
4073
4087
|
return
|
|
4074
4088
|
}
|
|
4075
4089
|
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 内)。
|
|
4090
|
+
// 通常送信は idle なら turn/start、busy なら hub-agent の次ターン FIFO。
|
|
4091
|
+
// turn/steer / turn/interrupt は明示操作の別経路として扱う。
|
|
4083
4092
|
handleCodexChatInput(msg, ctx)
|
|
4084
4093
|
return
|
|
4094
|
+
case "codex.queue.cancel":
|
|
4095
|
+
try {
|
|
4096
|
+
ctx.codexBridge?.cancelQueued({
|
|
4097
|
+
stream_id: msg.stream_id,
|
|
4098
|
+
id: msg.id,
|
|
4099
|
+
})
|
|
4100
|
+
} catch (err) {
|
|
4101
|
+
ctx.logger?.warn?.(
|
|
4102
|
+
{ err: err?.message, stream_id: msg.stream_id, input_id: msg.id },
|
|
4103
|
+
"codex queued input cancel failed",
|
|
4104
|
+
)
|
|
4105
|
+
}
|
|
4106
|
+
return
|
|
4085
4107
|
case "codex.permission.reply":
|
|
4086
4108
|
if (!ctx.codexBridge) return
|
|
4087
4109
|
ctx.codexBridge.permissionReply({
|