@cocorograph/hub-agent 0.8.9 → 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-history.mjs +24 -3
- package/src/claude-stream-bridge.mjs +68 -42
- package/src/codex-stream-bridge.mjs +223 -142
- package/src/main.mjs +93 -314
- package/src/sidebar-message-snapshot.mjs +362 -0
|
@@ -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 相当)
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
import { EventEmitter } from "node:events"
|
|
44
44
|
import { randomUUID } from "node:crypto"
|
|
45
45
|
|
|
46
|
+
import { recordInjectSourcePersistent } from "./inject-source-store.mjs"
|
|
47
|
+
|
|
46
48
|
import { AsyncTtlCache } from "./async-ttl-cache.mjs"
|
|
47
49
|
import { CodexAppServerClient } from "./codex-appserver-client.mjs"
|
|
48
50
|
import {
|
|
@@ -134,6 +136,10 @@ const DEAD_TERMINAL_TTL_MS =
|
|
|
134
136
|
const TURN_START_TIMEOUT_MS =
|
|
135
137
|
Number(process.env.HUB_AGENT_CODEX_TURN_START_TIMEOUT_MS) || 30 * 1000
|
|
136
138
|
|
|
139
|
+
// browser が ack を取りこぼしても queue snapshot から受理済みと再確認できるよう、
|
|
140
|
+
// 直近 input_id を bounded ring で保持する (Claude queue_state.accepted と同じ契約)。
|
|
141
|
+
const ACCEPTED_INPUT_ID_RING_MAX = 256
|
|
142
|
+
|
|
137
143
|
// sibling-inject の turn RPC が成功した後、app-server が同じ userMessage を
|
|
138
144
|
// item/completed で echo するまで待つ上限。RPC accepted と userMessage delivered を
|
|
139
145
|
// 分離し、旧 stream / 初期化競合で rollout に残らない入力を成功扱いしない。
|
|
@@ -253,9 +259,8 @@ export function deriveResumeTurnState(thread) {
|
|
|
253
259
|
const lastTurnStatus = typeof lastTurn?.status === "string" ? lastTurn.status : null
|
|
254
260
|
// 権威シグナル: スレッドが active のときだけ「ターン実行中」。fresh resume は idle。
|
|
255
261
|
const turnActive = threadStatus === "active"
|
|
256
|
-
// active かつ最後のターンが inProgress のときのみ activeTurnId
|
|
257
|
-
//
|
|
258
|
-
// = 次の入力は turn/start になり、固着しない。
|
|
262
|
+
// active かつ最後のターンが inProgress のときのみ activeTurnId を復元する。
|
|
263
|
+
// 通常入力はこの ID がある間は次ターン FIFO で待ち、一致する completed 後に start する。
|
|
259
264
|
const activeTurnId =
|
|
260
265
|
turnActive && lastTurnStatus === "inProgress" && typeof lastTurn?.id === "string"
|
|
261
266
|
? lastTurn.id
|
|
@@ -267,10 +272,9 @@ export function deriveResumeTurnState(thread) {
|
|
|
267
272
|
* 配達保証 (S2, 2026-07-05): `input()` が reject したエラーを `codex.input.nack` の
|
|
268
273
|
* `reason` へ分類する。
|
|
269
274
|
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
* 付与するようになった。付与されていればそれを最優先で採用し (main.mjs が組み立てる
|
|
275
|
+
* `CodexAppServerClient.request()` のタイムアウト等、bridge が判断できる失敗は reject する
|
|
276
|
+
* Error に権威ある `.reason` を直接付与する。付与されていればそれを最優先で採用し
|
|
277
|
+
* (main.mjs が組み立てる
|
|
274
278
|
* `codex.error.reason` と同じ値になり、frontend 側の分岐を一致させられる)、付与されて
|
|
275
279
|
* いない場合 (例: `_require()` の「セッション不在」) のみメッセージ文言からの
|
|
276
280
|
* ベストエフォート分類にフォールバックする。
|
|
@@ -479,19 +483,20 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
479
483
|
// この session を引き当て _tryReattach の session.ready 合流に入り、二重 app-server を作らない。
|
|
480
484
|
resumeThreadId: resumeThreadId || null,
|
|
481
485
|
activeTurnId: null,
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
// pendingInputs の二重路 (queue 経路 + 直通経路) が overtaking と並行リトライ二重ターンの温床だった。
|
|
486
|
+
// 通常入力は session 単位の次ターン FIFO。activeTurnId がある間は App Server へ
|
|
487
|
+
// turn/steer せずここで待ち、対象 turn/completed 後に 1 件ずつ turn/start する。
|
|
485
488
|
/** @type {Array<{input: object[], model?: string, effort?: string, resolve: Function,
|
|
486
|
-
* reject: Function,
|
|
487
|
-
* sourceStreamId?: string}>}
|
|
488
|
-
* 全 input() はここに積まれ、単一 pump (_pumpInputs) が **1 件ずつ直列**に処理する。直通経路を
|
|
489
|
-
* 廃したので、drain 中の新規 input が queue を追い越したり、並行 turn/start・steer を発行して
|
|
490
|
-
* 二重ターンになる事故が起きない。各 promise は **実 delivery (turn/start か turn/steer の RPC)**で
|
|
491
|
-
* settle する (main.mjs が codex.input.ack/nack を実配達に合わせて出せる)。 */
|
|
489
|
+
* reject: Function, deliveryId?: string, inputId?: string, text: string,
|
|
490
|
+
* sourceStreamId?: string, acceptedAsQueued?: boolean}>} */
|
|
492
491
|
inputQueue: [],
|
|
493
492
|
/** round3 major#1: pump が走行中か (単一 pump を保証する singleflight フラグ)。 */
|
|
494
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: [],
|
|
495
500
|
/** @type {{resolve: Function, timer: any} | null} round3 major#2: turn/start 後に turn/started
|
|
496
501
|
* (activeTurnId 確定) を待つ barrier の待機者。timeout 付きでハングを防ぐ。 */
|
|
497
502
|
_turnActiveWaiter: null,
|
|
@@ -941,6 +946,82 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
941
946
|
return Array.from(new Set(this.sessions.values())).filter((s) => !s.dead)
|
|
942
947
|
}
|
|
943
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
|
+
|
|
944
1025
|
/** 連携カード種別 + 配達確認: 注入本文 → entry を session の FIFO に控える。sendToCwd/
|
|
945
1026
|
* sendToStream が input() を呼ぶ直前に記録し、直後の userMessage echo を
|
|
946
1027
|
* _tagInjectSourceOnItem が拾う。entry 自体が delivery promise の handle であり、
|
|
@@ -1118,7 +1199,27 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1118
1199
|
)
|
|
1119
1200
|
break
|
|
1120
1201
|
case "turn/completed":
|
|
1121
|
-
|
|
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
|
+
}
|
|
1122
1223
|
// round3 major#2: turn/started を取りこぼしたまま turn/completed が来た異常時も barrier が
|
|
1123
1224
|
// 固着しないよう解除する (pump が次 item を fresh に turn/start できる)。
|
|
1124
1225
|
this._settleTurnActiveWaiter(session)
|
|
@@ -1136,6 +1237,8 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1136
1237
|
)
|
|
1137
1238
|
// Stage 5: turn 完了後の reap 予約は不要。以降 idle になれば周期 reconcile が
|
|
1138
1239
|
// 長 idle TTL / LRU で扱う(detach 済みでも 7 日 warm 保持)。
|
|
1240
|
+
// 通常送信 FIFO は matching turn/completed だけを排出境界にする。
|
|
1241
|
+
void this._pumpInputs(session)
|
|
1139
1242
|
break
|
|
1140
1243
|
case "item/completed":
|
|
1141
1244
|
// T06541 案1: 完了した ThreadItem を高忠実度履歴に追記する(cwd reattach で返す用)。
|
|
@@ -1256,176 +1359,142 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1256
1359
|
* @param {string} [args.effort] - 公式スキーマ `TurnStartParams.effort`
|
|
1257
1360
|
* ("Override the reasoning effort for this turn and subsequent turns")。turn/start でのみ有効。
|
|
1258
1361
|
* @param {string} [args.deliveryId] sibling-inject の内部相関 ID
|
|
1259
|
-
* @
|
|
1260
|
-
*
|
|
1261
|
-
* ⚠️ `turn/steer` (`TurnSteerParams`) には model/effort フィールドが存在しない
|
|
1262
|
-
* (公式スキーマで未対応と確認済み、2026-07-03)。そのためターン実行中 (busy) に
|
|
1263
|
-
* model/effort を渡しても turn/steer 経由では反映されず、次の非 busy な input()
|
|
1264
|
-
* (= turn/start) から有効になる。呼び出し側 (main.mjs) はこれを承知の上で常に
|
|
1265
|
-
* model/effort を渡してよい (steer 分岐では単に無視される)。
|
|
1362
|
+
* @param {string} [args.inputId] browser の配達確認・キュー表示用 ID
|
|
1363
|
+
* @returns {Promise<{method: "turn/start"|"queued", deliveryId?: string}>}
|
|
1266
1364
|
*/
|
|
1267
|
-
async input({ stream_id, message, model, effort, deliveryId }) {
|
|
1365
|
+
async input({ stream_id, message, model, effort, deliveryId, inputId }) {
|
|
1268
1366
|
const session = this._require(stream_id)
|
|
1269
1367
|
// Stage 5: _touch で lastActivityMs を更新 = LRU 先頭へ。ターン開始で activeTurnId が
|
|
1270
1368
|
// 立ち、reconcile は healthy busy を protected 扱いにするので実行中に kill されない。
|
|
1271
1369
|
// 案B: stream_id を渡し端末別活性も更新(死端末 GC が入力継続端末を掃除しない)。
|
|
1272
1370
|
this._touch(session, stream_id)
|
|
1273
1371
|
const input = userInputFromMessage(message)
|
|
1274
|
-
|
|
1275
|
-
|
|
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)
|
|
1276
1381
|
return new Promise((resolve, reject) => {
|
|
1277
|
-
|
|
1382
|
+
const item = {
|
|
1278
1383
|
input,
|
|
1384
|
+
text,
|
|
1279
1385
|
model,
|
|
1280
1386
|
effort,
|
|
1281
1387
|
deliveryId,
|
|
1388
|
+
inputId,
|
|
1282
1389
|
sourceStreamId: stream_id,
|
|
1283
1390
|
resolve,
|
|
1284
1391
|
reject,
|
|
1285
|
-
|
|
1286
|
-
}
|
|
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
|
+
}
|
|
1287
1409
|
void this._pumpInputs(session)
|
|
1288
1410
|
})
|
|
1289
1411
|
}
|
|
1290
1412
|
|
|
1291
1413
|
/**
|
|
1292
|
-
*
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1295
|
-
* turn/started (activeTurnId 確定) を **timeout 付き**で待つ (round3 major#2: ハング防止)。
|
|
1296
|
-
* - busy (activeTurnId 有り): turn/steer で追い打ち。
|
|
1297
|
-
* - turn/steer 失敗 (expectedTurnId 不一致など): activeTurnId を捨て、同 item を **1 回だけ**
|
|
1298
|
-
* turn/start にフォールバックして自己修復する (旧 self-heal を FIFO 内で継続。並行はしない)。
|
|
1299
|
-
* 各 item の promise は実 delivery の RPC 結果で settle する。
|
|
1414
|
+
* 通常送信の次ターン FIFO pump。activeTurnId がある間は何も App Server へ送らず、
|
|
1415
|
+
* matching turn/completed が activeTurnId を解除した後だけ先頭を turn/start する。
|
|
1416
|
+
* これにより通常クリックと明示的な turn/steer / turn/interrupt の意味を分離する。
|
|
1300
1417
|
*/
|
|
1301
1418
|
async _pumpInputs(session) {
|
|
1302
|
-
if (session.pumping) return
|
|
1419
|
+
if (session.pumping || session.activeTurnId) return
|
|
1303
1420
|
session.pumping = true
|
|
1304
1421
|
try {
|
|
1305
1422
|
while (session.inputQueue.length > 0) {
|
|
1423
|
+
if (session.activeTurnId) return
|
|
1306
1424
|
if (session.dead) {
|
|
1307
1425
|
this._drainRejectQueue(session, { reason: "queue_dead" })
|
|
1308
1426
|
return
|
|
1309
1427
|
}
|
|
1310
1428
|
const item = session.inputQueue[0]
|
|
1311
|
-
|
|
1312
|
-
// turn/completed が来て session.activeTurnId が null に戻ると、catch 時点の値では steer 試行が
|
|
1313
|
-
// start と誤判定され、self-heal リトライがスキップされて item が誤 reject される。
|
|
1314
|
-
const attemptedSteer = !!session.activeTurnId
|
|
1429
|
+
session.dispatchingInput = item
|
|
1315
1430
|
try {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
} else {
|
|
1337
|
-
// round4 major#2: turn/start dispatch の **前に** 世代を控える。start 応答後に
|
|
1338
|
-
// turn/started(+completed) が pump 再開前に処理されても、世代前進で「ターンが始まった」を
|
|
1339
|
-
// 検知でき、誤 timeout しない。
|
|
1340
|
-
const genBefore = session.turnGeneration
|
|
1341
|
-
const params = { threadId: session.threadId, input: item.input }
|
|
1342
|
-
if (item.model) params.model = item.model
|
|
1343
|
-
if (item.effort) params.effort = item.effort
|
|
1344
|
-
await session.client.request("turn/start", params)
|
|
1345
|
-
session.inputQueue.shift()
|
|
1346
|
-
this.logger?.info?.(
|
|
1347
|
-
{
|
|
1348
|
-
delivery_id: item.deliveryId || null,
|
|
1349
|
-
stream_id: item.sourceStreamId || session.stream_id,
|
|
1350
|
-
current_stream_id: session.stream_id,
|
|
1351
|
-
thread_id: session.threadId,
|
|
1352
|
-
attempted_steer: false,
|
|
1353
|
-
rpc_method: "turn/start",
|
|
1354
|
-
rpc_result: "accepted",
|
|
1355
|
-
},
|
|
1356
|
-
"codex input RPC settled",
|
|
1357
|
-
)
|
|
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) {
|
|
1358
1451
|
item.resolve({ method: "turn/start", deliveryId: item.deliveryId })
|
|
1359
|
-
// 次の item を steer にするため turn/started (= 世代前進) を待つ。timeout でハングを防ぐ。
|
|
1360
|
-
// timeout したら「ターンが正しく始まらなかった」とみなし残り queue を reject して打ち切る。
|
|
1361
|
-
const becameActive = await this._waitTurnActive(session, genBefore)
|
|
1362
|
-
if (!becameActive) {
|
|
1363
|
-
this._drainRejectQueue(session, { reason: "turn_start_timeout" })
|
|
1364
|
-
return
|
|
1365
|
-
}
|
|
1366
1452
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
stream_id: item.sourceStreamId || session.stream_id,
|
|
1373
|
-
current_stream_id: session.stream_id,
|
|
1374
|
-
thread_id: session.threadId,
|
|
1375
|
-
attempted_steer: true,
|
|
1376
|
-
rpc_method: "turn/steer",
|
|
1377
|
-
rpc_result: "retry_as_start",
|
|
1378
|
-
err: err instanceof Error ? err.message : String(err),
|
|
1379
|
-
},
|
|
1380
|
-
"codex input RPC settled",
|
|
1381
|
-
)
|
|
1382
|
-
// turn/steer 失敗 → 自己修復: activeTurnId を捨て、同 item を turn/start で 1 回だけリトライ
|
|
1383
|
-
// (shift しない = 同 item を次ループの start 経路で再処理)。並行はしない (単一 pump)。
|
|
1384
|
-
// 元の steer エラーを stash して、リトライも失敗したとき合成メッセージを作れるようにする。
|
|
1385
|
-
session.activeTurnId = null
|
|
1386
|
-
item.retriedAsStart = true
|
|
1387
|
-
item.priorSteerError = err instanceof Error ? err : new Error(String(err))
|
|
1388
|
-
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
|
|
1389
1458
|
}
|
|
1390
|
-
//
|
|
1391
|
-
//
|
|
1459
|
+
// 通常は active のままなので matching turn/completed まで停止する。start 応答前に
|
|
1460
|
+
// started+completed 済みの fast turn だけ、そのまま次の FIFO item へ進める。
|
|
1461
|
+
if (session.activeTurnId) return
|
|
1462
|
+
} catch (err) {
|
|
1392
1463
|
session.inputQueue.shift()
|
|
1393
|
-
session.
|
|
1394
|
-
|
|
1395
|
-
if (
|
|
1396
|
-
// steer→start リトライがともに失敗: 合成メッセージ + reason (main.mjs の classifyCodexInputError
|
|
1397
|
-
// が reason を優先採用。従来の単一 input() 実装と同じ診断を保つ)。
|
|
1398
|
-
e = Object.assign(
|
|
1399
|
-
new Error(
|
|
1400
|
-
`turn/steer failed (${item.priorSteerError.message}) and turn/start retry also failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1401
|
-
),
|
|
1402
|
-
{ reason: "turn_mismatch_retry_failed", cause: err },
|
|
1403
|
-
)
|
|
1404
|
-
} else {
|
|
1405
|
-
e = err instanceof Error ? err : new Error(String(err))
|
|
1406
|
-
if (!e.reason) e.reason = "start_error"
|
|
1407
|
-
}
|
|
1464
|
+
session.dispatchingInput = null
|
|
1465
|
+
const e = err instanceof Error ? err : new Error(String(err))
|
|
1466
|
+
if (!e.reason) e.reason = "start_error"
|
|
1408
1467
|
this.logger?.error?.(
|
|
1409
1468
|
{
|
|
1410
1469
|
delivery_id: item.deliveryId || null,
|
|
1470
|
+
input_id: item.inputId || null,
|
|
1411
1471
|
stream_id: item.sourceStreamId || session.stream_id,
|
|
1412
1472
|
current_stream_id: session.stream_id,
|
|
1413
1473
|
thread_id: session.threadId,
|
|
1414
|
-
|
|
1415
|
-
rpc_method: attemptedSteer ? "turn/steer" : "turn/start",
|
|
1474
|
+
rpc_method: "turn/start",
|
|
1416
1475
|
rpc_result: "failed",
|
|
1417
1476
|
err: e.message,
|
|
1418
1477
|
reason: e.reason || null,
|
|
1419
1478
|
},
|
|
1420
1479
|
"codex input RPC settled",
|
|
1421
1480
|
)
|
|
1422
|
-
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)
|
|
1423
1491
|
}
|
|
1424
1492
|
}
|
|
1425
1493
|
} finally {
|
|
1494
|
+
session.dispatchingInput = null
|
|
1426
1495
|
session.pumping = false
|
|
1427
1496
|
// pumping=false 直後の enqueue 競合窓を拾い直す (取りこぼし防止)。再帰は 1 段のみ。
|
|
1428
|
-
if (session.inputQueue.length > 0 && !session.dead) {
|
|
1497
|
+
if (session.inputQueue.length > 0 && !session.dead && !session.activeTurnId) {
|
|
1429
1498
|
void this._pumpInputs(session)
|
|
1430
1499
|
}
|
|
1431
1500
|
}
|
|
@@ -1484,15 +1553,24 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1484
1553
|
if (!session || !Array.isArray(session.inputQueue) || session.inputQueue.length === 0) return
|
|
1485
1554
|
const pending = session.inputQueue
|
|
1486
1555
|
session.inputQueue = []
|
|
1556
|
+
session.dispatchingInput = null
|
|
1487
1557
|
const reason = cause?.reason || "queue_dead"
|
|
1488
|
-
for (const
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
cause,
|
|
1493
|
-
}),
|
|
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 },
|
|
1494
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
|
+
}
|
|
1495
1572
|
}
|
|
1573
|
+
if (!session.dead) this._emitQueueState(session)
|
|
1496
1574
|
}
|
|
1497
1575
|
|
|
1498
1576
|
/** @returns {Promise<void>} */
|
|
@@ -1947,6 +2025,9 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1947
2025
|
reason: delivery?.reason || "echo_timeout",
|
|
1948
2026
|
}
|
|
1949
2027
|
}
|
|
2028
|
+
// Claude と同じ永続ストアへ記録し、hub-agent 再起動後に rollout を読む場合も
|
|
2029
|
+
// peer / orchestrator 注入を通常の user 依頼として表示しない。
|
|
2030
|
+
recordInjectSourcePersistent(session.cwd, text, source).catch(() => {})
|
|
1950
2031
|
// rollout は内部形式・flush 遅延の影響を受けるため primary oracle にしない。echo delivered を
|
|
1951
2032
|
// 返した後、注入前 offset + threadId + 本文 hash で非同期監査し、結果だけを構造化ログへ残す。
|
|
1952
2033
|
void this._deliveryAuditVerify(durableAudit)
|