@cocorograph/hub-agent 0.7.59 → 0.7.60
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/codex-stream-bridge.mjs +54 -2
- package/src/main.mjs +8 -0
package/package.json
CHANGED
|
@@ -77,6 +77,41 @@ function flattenThreadItems(thread) {
|
|
|
77
77
|
return items
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* resume 応答から Codex の「権威的なターン進行状態」を取り出す。
|
|
82
|
+
*
|
|
83
|
+
* ⚠️ Codex は Claude と違い、ターンが走っているかを **明示フィールド** で持つ (公式
|
|
84
|
+
* app-server スキーマで確認):
|
|
85
|
+
* - Thread.status.type = notLoaded | idle | systemError | active
|
|
86
|
+
* - Turn.status = completed | interrupted | failed | inProgress
|
|
87
|
+
* そのため「会話履歴の末尾が user か assistant か」からの推測 (Claude 由来の
|
|
88
|
+
* deriveTurnActiveFromHistory) は不要かつ有害 (ツール実行のみで終わったターンや中断
|
|
89
|
+
* ターンで末尾が user メッセージになり、生成中スピナーが永久固着する既知バグの原因)。
|
|
90
|
+
*
|
|
91
|
+
* resume 時は app-server が新プロセスで、実際に走っているターンは無いため Codex は
|
|
92
|
+
* 通常 `idle` を返す。ゆえに `threadStatus === "active"` を唯一の権威 turn-active
|
|
93
|
+
* シグナルとして採用し、フロントの hydrate 時 turnActive をこれで決めさせる。
|
|
94
|
+
*
|
|
95
|
+
* @param {object|undefined} thread thread/resume の result.thread
|
|
96
|
+
* @returns {{threadStatus: string|null, lastTurnStatus: string|null, turnActive: boolean, activeTurnId: string|null}}
|
|
97
|
+
*/
|
|
98
|
+
export function deriveResumeTurnState(thread) {
|
|
99
|
+
const threadStatus = typeof thread?.status?.type === "string" ? thread.status.type : null
|
|
100
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : []
|
|
101
|
+
const lastTurn = turns.length ? turns[turns.length - 1] : null
|
|
102
|
+
const lastTurnStatus = typeof lastTurn?.status === "string" ? lastTurn.status : null
|
|
103
|
+
// 権威シグナル: スレッドが active のときだけ「ターン実行中」。fresh resume は idle。
|
|
104
|
+
const turnActive = threadStatus === "active"
|
|
105
|
+
// active かつ最後のターンが inProgress のときのみ activeTurnId を復元する
|
|
106
|
+
// (input() が turn/steer へ正しく分岐できるように)。それ以外は null のまま
|
|
107
|
+
// = 次の入力は turn/start になり、固着しない。
|
|
108
|
+
const activeTurnId =
|
|
109
|
+
turnActive && lastTurnStatus === "inProgress" && typeof lastTurn?.id === "string"
|
|
110
|
+
? lastTurn.id
|
|
111
|
+
: null
|
|
112
|
+
return { threadStatus, lastTurnStatus, turnActive, activeTurnId }
|
|
113
|
+
}
|
|
114
|
+
|
|
80
115
|
/**
|
|
81
116
|
* 配達保証 (S2, 2026-07-05): `input()` が reject したエラーを `codex.input.nack` の
|
|
82
117
|
* `reason` へ分類する。
|
|
@@ -134,7 +169,8 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
134
169
|
}
|
|
135
170
|
|
|
136
171
|
/**
|
|
137
|
-
* @returns {Promise<{resuming: boolean, threadId: string, history: object[]
|
|
172
|
+
* @returns {Promise<{resuming: boolean, threadId: string, history: object[],
|
|
173
|
+
* threadStatus: string|null, lastTurnStatus: string|null, turnActive: boolean}>}
|
|
138
174
|
*/
|
|
139
175
|
async attach({ stream_id, cwd, model, effort, approvalPolicy, sandbox, resumeThreadId }) {
|
|
140
176
|
if (this.sessions.has(stream_id)) {
|
|
@@ -173,7 +209,23 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
173
209
|
// 新規スレッド (thread/start) は履歴が無いので空配列。resume 時のみ平坦化する
|
|
174
210
|
// (thread/start のレスポンスは turns を持たない仕様のため呼んでも常に空)。
|
|
175
211
|
const history = resuming ? flattenThreadItems(result?.thread) : []
|
|
176
|
-
|
|
212
|
+
// 案C: 権威的なターン状態を resume 応答から取り出してフロントへ渡す。
|
|
213
|
+
// これによりフロントは会話末尾からの推測をやめ、Thread.status で turnActive を
|
|
214
|
+
// 決められる (生成中スピナー固着バグの根治)。新規スレッドは常に idle 相当。
|
|
215
|
+
const turnState = resuming
|
|
216
|
+
? deriveResumeTurnState(result?.thread)
|
|
217
|
+
: { threadStatus: "idle", lastTurnStatus: null, turnActive: false, activeTurnId: null }
|
|
218
|
+
// Codex が「ターン実行中」と権威的に報告した場合のみ activeTurnId を復元する
|
|
219
|
+
// (通常の fresh resume では null = 次入力が turn/start になり固着しない)。
|
|
220
|
+
session.activeTurnId = turnState.activeTurnId
|
|
221
|
+
return {
|
|
222
|
+
resuming,
|
|
223
|
+
threadId: session.threadId,
|
|
224
|
+
history,
|
|
225
|
+
threadStatus: turnState.threadStatus,
|
|
226
|
+
lastTurnStatus: turnState.lastTurnStatus,
|
|
227
|
+
turnActive: turnState.turnActive,
|
|
228
|
+
}
|
|
177
229
|
} catch (err) {
|
|
178
230
|
this.sessions.delete(stream_id)
|
|
179
231
|
client.stop()
|
package/src/main.mjs
CHANGED
|
@@ -3952,6 +3952,14 @@ async function dispatch(msg, ctx) {
|
|
|
3952
3952
|
// consumers.py の per-stream relay は content-blind なので backend 側の
|
|
3953
3953
|
// 変更は不要 (_AGENT_PER_STREAM_TYPES の codex.ready がそのまま運ぶ)。
|
|
3954
3954
|
history: info.history || [],
|
|
3955
|
+
// 案C (権威的ターン状態): フロントは会話末尾からの推測 (deriveTurnActive
|
|
3956
|
+
// FromHistory) をやめ、Codex が明示する Thread.status で turnActive を決める。
|
|
3957
|
+
// resume 後は新プロセスで実行中ターンが無く thread_status='idle' が返るため、
|
|
3958
|
+
// 生成中スピナー固着 (中断/ツールのみ終了ターンで末尾が user メッセージになる
|
|
3959
|
+
// ケース) が根治する。
|
|
3960
|
+
turn_active: info.turnActive === true,
|
|
3961
|
+
thread_status: info.threadStatus || null,
|
|
3962
|
+
last_turn_status: info.lastTurnStatus || null,
|
|
3955
3963
|
})
|
|
3956
3964
|
// S1 (2026-07): claude.attach と対称の identity 正本化。Codex は thread/start
|
|
3957
3965
|
// 応答時点で threadId が確定するため (Claude の fresh とは異なり)、fresh でも
|