@cocorograph/hub-agent 0.8.0 → 0.8.2
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 +183 -10
- package/src/codex-delivery-audit.mjs +177 -0
- package/src/codex-stream-bridge.mjs +405 -55
- package/src/main.mjs +98 -5
- package/src/sibling-inject.mjs +81 -9
package/src/main.mjs
CHANGED
|
@@ -327,7 +327,11 @@ export function isReadOnlyRpc(type) {
|
|
|
327
327
|
return (
|
|
328
328
|
type === "claude.history.request" ||
|
|
329
329
|
type === "claude.sessions.request" ||
|
|
330
|
-
type === "codex.sessions.request"
|
|
330
|
+
type === "codex.sessions.request" ||
|
|
331
|
+
// T06603 (2026-07-25): キュー snapshot の pull。in-memory 配列を読むだけで副作用が無く、
|
|
332
|
+
// かつ「直列キュー前方の重い処理で再同期が遅延する」と送信待ちチップの固着が長引く
|
|
333
|
+
// (再同期は固着を直すための経路なので、詰まりやすい経路に置いてはならない)。
|
|
334
|
+
type === "claude.queue.request"
|
|
331
335
|
)
|
|
332
336
|
}
|
|
333
337
|
|
|
@@ -2859,6 +2863,13 @@ const seenCodexChatInputIds = makeSeenInputIdRegistry()
|
|
|
2859
2863
|
// Promise へ合流させる (settle 時に元の .then/.catch が 1 回だけ ack/nack を出す)。
|
|
2860
2864
|
const codexInflightByInputId = new Map()
|
|
2861
2865
|
|
|
2866
|
+
// T06603 (2026-07-25): claude.input の **in-flight** dedup 用レジストリ (input_id -> Promise)。
|
|
2867
|
+
// 上の codex 版と同じ理由・同じ流儀。claude 側は sendMessage が resident query への
|
|
2868
|
+
// reconcile (applyFlagSettings の control request 往復) を await するため settle まで数秒
|
|
2869
|
+
// かかることがあり、その窓で ackWatchdog(5s) の再送が入ると `seen` 未登録のまま二重投入され、
|
|
2870
|
+
// 同一 input_id がキューに 2 件積まれて drain 後も 1 件残る (送信待ちチップが消えない)。
|
|
2871
|
+
const claudeInflightByInputId = new Map()
|
|
2872
|
+
|
|
2862
2873
|
/**
|
|
2863
2874
|
* input_id 付き pty.data (tracked 入力) の処理。write 成功で pty.input.ack を返し、
|
|
2864
2875
|
* stream 不在なら pty.error を返して browser の再 attach → 再送に繋げる。
|
|
@@ -2933,7 +2944,20 @@ export function handleClaudeChatInput(msg, ctx) {
|
|
|
2933
2944
|
ctx.client.send({ type: "claude.input.ack", stream_id, input_id })
|
|
2934
2945
|
return undefined
|
|
2935
2946
|
}
|
|
2936
|
-
|
|
2947
|
+
if (input_id && claudeInflightByInputId.has(input_id)) {
|
|
2948
|
+
// T06603 (2026-07-25): **処理中** の同一 input_id (browser の ackWatchdog が 5s で
|
|
2949
|
+
// 再送してくるが、こちらはまだ settle していない窓)。codex 側は round4 major#1 で
|
|
2950
|
+
// 対処済みだったが claude 側だけ欠けており、`seen` は settle 後にしか登録されないため
|
|
2951
|
+
// 再送がそのまま二重投入されていた (同じ本文が 2 回実行され、かつ同一 id の項目が
|
|
2952
|
+
// キューに 2 つ積まれて drain 後も 1 つ残る = 「実行されたのにキューから消えない」)。
|
|
2953
|
+
// 既存 Promise へ合流させ、ack/nack は元の 1 回だけ出す。
|
|
2954
|
+
ctx.logger?.info?.(
|
|
2955
|
+
{ stream_id, input_id },
|
|
2956
|
+
"claude.input in-flight dedup (join, no re-dispatch)",
|
|
2957
|
+
)
|
|
2958
|
+
return claudeInflightByInputId.get(input_id)
|
|
2959
|
+
}
|
|
2960
|
+
const p = ctx.claudeBridge
|
|
2937
2961
|
.input({
|
|
2938
2962
|
stream_id,
|
|
2939
2963
|
message: msg.message,
|
|
@@ -2972,6 +2996,13 @@ export function handleClaudeChatInput(msg, ctx) {
|
|
|
2972
2996
|
})
|
|
2973
2997
|
}
|
|
2974
2998
|
})
|
|
2999
|
+
.finally(() => {
|
|
3000
|
+
// in-flight レジストリから外す (成功時は seen が以降の再送を dedup、
|
|
3001
|
+
// 失敗時は再送で正当にリトライさせる)。codex 版と同じ流儀。
|
|
3002
|
+
if (input_id) claudeInflightByInputId.delete(input_id)
|
|
3003
|
+
})
|
|
3004
|
+
if (input_id) claudeInflightByInputId.set(input_id, p)
|
|
3005
|
+
return p
|
|
2975
3006
|
}
|
|
2976
3007
|
|
|
2977
3008
|
/**
|
|
@@ -3386,6 +3417,12 @@ async function dispatch(msg, ctx) {
|
|
|
3386
3417
|
// 旧 agent にはこのフラグが無く、browser は楽観アイテムの×を出さない (ゴースト
|
|
3387
3418
|
// 実行防止のフォールバック)。
|
|
3388
3419
|
supports_queue_input_id: true,
|
|
3420
|
+
// pull 型再同期 capability (T06603, 2026-07-25): この agent は
|
|
3421
|
+
// claude.queue.request に claude.queue.response で答え、queue_state に
|
|
3422
|
+
// 版番号 (seq) と受理済み input_id (accepted) を載せることを通知する。
|
|
3423
|
+
// 旧 agent はこのフラグが無く、browser は従来の「配信到達に依存する」
|
|
3424
|
+
// 挙動へフォールバックする (混在期間も安全に段階デプロイできる)。
|
|
3425
|
+
supports_queue_snapshot: true,
|
|
3389
3426
|
})
|
|
3390
3427
|
// dispatch全体はWSメッセージ順を守る直列キュー上にある。ここで履歴I/Oをawaitすると
|
|
3391
3428
|
// 同一接続のpermission/input等まで停止するため、readyの同期送信後は非同期でpushする。
|
|
@@ -3770,11 +3807,67 @@ async function dispatch(msg, ctx) {
|
|
|
3770
3807
|
}
|
|
3771
3808
|
return
|
|
3772
3809
|
}
|
|
3773
|
-
case "claude.queue.cancel":
|
|
3810
|
+
case "claude.queue.cancel": {
|
|
3774
3811
|
// 送信待ち (pending) メッセージを id 指定で取り消す (0.6.26)。
|
|
3775
|
-
|
|
3776
|
-
|
|
3812
|
+
// T06603 cross-review 4 巡目: 取り消せなかった (stream 不在) ときは browser へ
|
|
3813
|
+
// 失敗を返す。browser は要求時にチップを消さず「取消中」印を付けて権威の確認を
|
|
3814
|
+
// 待つため、無応答だと印が外れず固着する。request_id をエコーして相関させる。
|
|
3815
|
+
// T06603 cross-review 5 巡目: 結末 (cancelled / already_started / stream_missing) を
|
|
3816
|
+
// **必ず** 返す。「pending から消えた」だけでは取消成功と実行開始済みを区別できず、
|
|
3817
|
+
// 特に drain の started フレームを取りこぼしていると、browser は取消成功に見せかけた
|
|
3818
|
+
// まま実行を続行してしまう (発言バブルも欠けたまま)。request_id + input_id で相関させる。
|
|
3819
|
+
const cancelRes = {
|
|
3820
|
+
type: "claude.queue.response",
|
|
3821
|
+
stream_id: msg.stream_id,
|
|
3822
|
+
request_id: msg.request_id || null,
|
|
3823
|
+
input_id: msg.id || null,
|
|
3824
|
+
}
|
|
3825
|
+
if (!ctx.claudeBridge) {
|
|
3826
|
+
ctx.client.send({ ...cancelRes, outcome: "stream_missing" })
|
|
3827
|
+
return
|
|
3828
|
+
}
|
|
3829
|
+
const canceled = ctx.claudeBridge.cancelQueued({
|
|
3830
|
+
stream_id: msg.stream_id,
|
|
3831
|
+
id: msg.id,
|
|
3832
|
+
})
|
|
3833
|
+
// ⚠️ T06603 cross-review 6 巡目: **outcome を先に送る**。キュー snapshot の再表明を
|
|
3834
|
+
// 先に流すと、backend は到着順に relay するため browser は空 snapshot で
|
|
3835
|
+
// 「キューに無い + 受理済み = 実行完了」と判定してチップを消し、後続の outcome は
|
|
3836
|
+
// 対象不在で no-op になる (already_started のバブル補償が発火しない)。
|
|
3837
|
+
ctx.client.send({
|
|
3838
|
+
...cancelRes,
|
|
3839
|
+
outcome: canceled?.outcome || "stream_missing",
|
|
3840
|
+
})
|
|
3841
|
+
canceled?.flush?.()
|
|
3842
|
+
return
|
|
3843
|
+
}
|
|
3844
|
+
case "claude.queue.request": {
|
|
3845
|
+
// pull 型再同期 (T06603, 2026-07-25): browser が権威キュー snapshot を引きにくる。
|
|
3846
|
+
// 従来キュー表示の是正手段は「agent が turn settle 毎に force 再送する queue_state」
|
|
3847
|
+
// だけで、その配信が 1 通落ちると browser には自己修復の手段が無く送信待ちチップが
|
|
3848
|
+
// 恒久固着した。ここで「いつでも引き直せる」経路を与えるのが根治の中核。
|
|
3849
|
+
// 応答は要求元 stream へ返す (backend が cockpit_stream_<stream_id> へ relay)。
|
|
3850
|
+
const queueReq = {
|
|
3851
|
+
type: "claude.queue.response",
|
|
3852
|
+
stream_id: msg.stream_id,
|
|
3853
|
+
request_id: msg.request_id || null,
|
|
3854
|
+
}
|
|
3855
|
+
if (!ctx.claudeBridge) {
|
|
3856
|
+
ctx.client.send({ ...queueReq, error: "stream_missing" })
|
|
3857
|
+
return
|
|
3858
|
+
}
|
|
3859
|
+
const got = ctx.claudeBridge.queueSnapshot({ stream_id: msg.stream_id })
|
|
3860
|
+
if (!got) {
|
|
3861
|
+
ctx.client.send({ ...queueReq, error: "stream_missing" })
|
|
3862
|
+
return
|
|
3863
|
+
}
|
|
3864
|
+
ctx.client.send({
|
|
3865
|
+
...queueReq,
|
|
3866
|
+
session_id: got.session_id,
|
|
3867
|
+
event: got.snapshot,
|
|
3868
|
+
})
|
|
3777
3869
|
return
|
|
3870
|
+
}
|
|
3778
3871
|
case "claude.detach":
|
|
3779
3872
|
if (!ctx.claudeBridge) return
|
|
3780
3873
|
ctx.claudeBridge.detach({ stream_id: msg.stream_id })
|
package/src/sibling-inject.mjs
CHANGED
|
@@ -45,6 +45,16 @@ import { verifySiblingTokenAny, siblingTokenCwd } from "./sibling-token.mjs"
|
|
|
45
45
|
export const SIBLING_REQUESTS_DIR =
|
|
46
46
|
process.env.COCKPIT_SIBLING_REQUESTS_DIR || "/tmp/cockpit_sibling_requests"
|
|
47
47
|
|
|
48
|
+
const DELIVERY_UNCONFIRMED_HINT =
|
|
49
|
+
"turn RPC は受理されましたが userMessage echo を確認できませんでした。既に届いている可能性があり、" +
|
|
50
|
+
"再送すると重複配送になります。再送前に対象セッションの受信状態または rollout 監査を確認してください。"
|
|
51
|
+
const DELIVERY_FAILED_HINT =
|
|
52
|
+
"turn RPC の受付前または受付中に失敗しました。対象セッションを確認して再送してください。"
|
|
53
|
+
|
|
54
|
+
function deliveredStateFor(kind) {
|
|
55
|
+
return kind === "codex" ? "delivered" : "dispatched"
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
export class SiblingInjectBridge {
|
|
49
59
|
/**
|
|
50
60
|
* @param {{dir?: string, claudeBridge?: object, codexBridge?: object, logger?: object}} [opts]
|
|
@@ -186,11 +196,34 @@ export class SiblingInjectBridge {
|
|
|
186
196
|
} else if (isExplicit) {
|
|
187
197
|
// 明示宛先: session_id (claude) / stream_id|thread_id (codex) で cwd 一致下に確定解決。
|
|
188
198
|
// sendToStream は {status:"delivered"|"not_found"|"ambiguous"} を返す。
|
|
189
|
-
const r = bridge?.sendToStream
|
|
190
|
-
|
|
191
|
-
|
|
199
|
+
const r = bridge?.sendToStream
|
|
200
|
+
? await bridge.sendToStream(cwd, target_session_id, message, injectSource)
|
|
201
|
+
: { status: "not_found" }
|
|
192
202
|
if (r.status === "delivered") {
|
|
193
|
-
result = {
|
|
203
|
+
result = {
|
|
204
|
+
ok: true,
|
|
205
|
+
delivered_to: r.session,
|
|
206
|
+
delivery_id: r.delivery_id,
|
|
207
|
+
delivery_state: deliveredStateFor(target_cli_kind),
|
|
208
|
+
durable_state: r.durable_state,
|
|
209
|
+
accepted_via: r.accepted_via,
|
|
210
|
+
item_id: r.item_id,
|
|
211
|
+
}
|
|
212
|
+
} else if (r.status === "delivery_unconfirmed" || r.status === "delivery_failed") {
|
|
213
|
+
result = {
|
|
214
|
+
ok: false,
|
|
215
|
+
error: r.status,
|
|
216
|
+
delivered_to: r.session,
|
|
217
|
+
delivery_id: r.delivery_id,
|
|
218
|
+
delivery_state:
|
|
219
|
+
r.status === "delivery_unconfirmed" ? "accepted_unconfirmed" : "failed",
|
|
220
|
+
accepted_via: r.accepted_via,
|
|
221
|
+
reason: r.reason,
|
|
222
|
+
hint:
|
|
223
|
+
r.status === "delivery_unconfirmed"
|
|
224
|
+
? DELIVERY_UNCONFIRMED_HINT
|
|
225
|
+
: DELIVERY_FAILED_HINT,
|
|
226
|
+
}
|
|
194
227
|
} else if (r.status === "ambiguous") {
|
|
195
228
|
// 宛先 id が同一 cwd の複数 live セッションに一致し一意に定まらない。
|
|
196
229
|
// ⚠️ 解消手段は CLI で異なる (T06271 レビュー4 minor):
|
|
@@ -235,9 +268,34 @@ export class SiblingInjectBridge {
|
|
|
235
268
|
// (サイレント誤配送 + delivered_to に実配送先でない sameCwd[0] を報告する成功偽装) が
|
|
236
269
|
// あった。sendToCwd を status 返却にして解決と配送を 1 呼び出しに統合し、delivered_to は
|
|
237
270
|
// **実配送先 (r.session)** を返す。
|
|
238
|
-
const r = bridge?.sendToCwd
|
|
271
|
+
const r = bridge?.sendToCwd
|
|
272
|
+
? await bridge.sendToCwd(cwd, text, injectSource)
|
|
273
|
+
: { status: "not_found" }
|
|
239
274
|
if (r.status === "delivered") {
|
|
240
|
-
result = {
|
|
275
|
+
result = {
|
|
276
|
+
ok: true,
|
|
277
|
+
delivered_to: r.session ?? null,
|
|
278
|
+
delivery_id: r.delivery_id,
|
|
279
|
+
delivery_state: deliveredStateFor(target_cli_kind),
|
|
280
|
+
durable_state: r.durable_state,
|
|
281
|
+
accepted_via: r.accepted_via,
|
|
282
|
+
item_id: r.item_id,
|
|
283
|
+
}
|
|
284
|
+
} else if (r.status === "delivery_unconfirmed" || r.status === "delivery_failed") {
|
|
285
|
+
result = {
|
|
286
|
+
ok: false,
|
|
287
|
+
error: r.status,
|
|
288
|
+
delivered_to: r.session ?? null,
|
|
289
|
+
delivery_id: r.delivery_id,
|
|
290
|
+
delivery_state:
|
|
291
|
+
r.status === "delivery_unconfirmed" ? "accepted_unconfirmed" : "failed",
|
|
292
|
+
accepted_via: r.accepted_via,
|
|
293
|
+
reason: r.reason,
|
|
294
|
+
hint:
|
|
295
|
+
r.status === "delivery_unconfirmed"
|
|
296
|
+
? DELIVERY_UNCONFIRMED_HINT
|
|
297
|
+
: DELIVERY_FAILED_HINT,
|
|
298
|
+
}
|
|
241
299
|
} else if (r.status === "ambiguous") {
|
|
242
300
|
result = {
|
|
243
301
|
ok: false,
|
|
@@ -258,10 +316,24 @@ export class SiblingInjectBridge {
|
|
|
258
316
|
target_cli_kind,
|
|
259
317
|
op: op || "normal",
|
|
260
318
|
...(result.ok
|
|
261
|
-
? {
|
|
262
|
-
|
|
319
|
+
? {
|
|
320
|
+
delivered_to: result.delivered_to,
|
|
321
|
+
delivery_id: result.delivery_id,
|
|
322
|
+
delivery_state: result.delivery_state,
|
|
323
|
+
durable_state: result.durable_state,
|
|
324
|
+
}
|
|
325
|
+
: {
|
|
326
|
+
error: result.error,
|
|
327
|
+
delivery_id: result.delivery_id,
|
|
328
|
+
delivery_state: result.delivery_state,
|
|
329
|
+
live_sessions: result.live_sessions,
|
|
330
|
+
}),
|
|
263
331
|
},
|
|
264
|
-
result.ok
|
|
332
|
+
result.ok
|
|
333
|
+
? result.delivery_state === "delivered"
|
|
334
|
+
? "sibling-inject delivered"
|
|
335
|
+
: "sibling-inject dispatched"
|
|
336
|
+
: "sibling-inject failed",
|
|
265
337
|
)
|
|
266
338
|
await this._writeResult(request_id, result)
|
|
267
339
|
// 要求ファイルは処理後に必ず削除する (成否問わず、再処理させないため)。
|