@cocorograph/hub-agent 0.7.37 → 0.7.42
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/chat-signals.mjs +45 -5
- package/src/claude-stream-bridge.mjs +138 -14
- package/src/codex-appserver-client.mjs +47 -3
- package/src/codex-stream-bridge.mjs +121 -7
- package/src/main.mjs +468 -38
- package/src/sibling-inject.mjs +150 -0
- package/src/sibling-token.mjs +62 -0
package/src/main.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { loadPlugins, runHookBroadcast, runHookChain } from "./plugin-loader.mjs
|
|
|
25
25
|
import { WsClient } from "./ws-client.mjs"
|
|
26
26
|
import { PtyBridge } from "./pty-bridge.mjs"
|
|
27
27
|
import { ClaudeStreamBridge } from "./claude-stream-bridge.mjs"
|
|
28
|
-
import { CodexStreamBridge } from "./codex-stream-bridge.mjs"
|
|
28
|
+
import { CodexStreamBridge, classifyCodexInputError } from "./codex-stream-bridge.mjs"
|
|
29
29
|
import { UploadManager } from "./claude-upload.mjs"
|
|
30
30
|
import { requestSelfUninstall } from "./service-install.mjs"
|
|
31
31
|
import {
|
|
@@ -93,6 +93,7 @@ import {
|
|
|
93
93
|
import { TuiPermissionBridge } from "./tui-permission-bridge.mjs"
|
|
94
94
|
import { TuiViewerRegistry } from "./tui-viewer-registry.mjs"
|
|
95
95
|
import { JsonlLiveWatchers } from "./jsonl-live-watchers.mjs"
|
|
96
|
+
import { SiblingInjectBridge } from "./sibling-inject.mjs"
|
|
96
97
|
import {
|
|
97
98
|
boundSessionId,
|
|
98
99
|
codexSessionUsages,
|
|
@@ -102,6 +103,7 @@ import {
|
|
|
102
103
|
getSessionUsages,
|
|
103
104
|
getUsage,
|
|
104
105
|
isFreshUnboundBind,
|
|
106
|
+
jsonlContextForCwd,
|
|
105
107
|
recordChatRateLimit,
|
|
106
108
|
recordChatUsageSnapshot,
|
|
107
109
|
turnActiveForCwd,
|
|
@@ -546,13 +548,23 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
546
548
|
status: "processing",
|
|
547
549
|
contextPct: pct,
|
|
548
550
|
inputPending: false,
|
|
551
|
+
// identity (S1): この cwd の chat 信号が「今どの CLI・どのセッションのものか」を
|
|
552
|
+
// 明示する。claude.attach で確定させた identity と一致すれば温存、別セッション/
|
|
553
|
+
// エンジンに切り替わっていれば chat-signals.mjs 側が自動でリセットする。
|
|
554
|
+
cliKind: "claude",
|
|
555
|
+
sessionId: session_id || null,
|
|
549
556
|
})
|
|
550
557
|
} catch {
|
|
551
558
|
/* ignore */
|
|
552
559
|
}
|
|
553
560
|
} else if (event?.type === "result") {
|
|
554
561
|
try {
|
|
555
|
-
recordChatActivity(cwd, {
|
|
562
|
+
recordChatActivity(cwd, {
|
|
563
|
+
status: "waiting",
|
|
564
|
+
inputPending: false,
|
|
565
|
+
cliKind: "claude",
|
|
566
|
+
sessionId: session_id || null,
|
|
567
|
+
})
|
|
556
568
|
} catch {
|
|
557
569
|
/* ignore */
|
|
558
570
|
}
|
|
@@ -620,17 +632,51 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
620
632
|
if (codexBridge) {
|
|
621
633
|
codexBridge.on("event", ({ stream_id, thread_id, cwd, notification }) => {
|
|
622
634
|
if (notification.method === "turn/started") {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
635
|
+
// S1 (2026-07): contextPct を渡していなかったため、Codex 選択中は状態ドット
|
|
636
|
+
// (processing/waiting) こそ更新されるがコンテキスト% は claude 由来の古い値の
|
|
637
|
+
// ままだった (症状③の一因)。codexUsageForThread は rollout ファイルの I/O を
|
|
638
|
+
// 伴うため fire-and-forget (await せず、取得できた時点で反映)。
|
|
639
|
+
;(async () => {
|
|
640
|
+
let contextPct = null
|
|
641
|
+
try {
|
|
642
|
+
const usage = await codexUsageForThread(thread_id)
|
|
643
|
+
contextPct = typeof usage?.context === "number" ? usage.context : null
|
|
644
|
+
} catch {
|
|
645
|
+
/* ignore */
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
recordChatActivity(cwd, {
|
|
649
|
+
status: "processing",
|
|
650
|
+
contextPct,
|
|
651
|
+
inputPending: false,
|
|
652
|
+
cliKind: "codex",
|
|
653
|
+
sessionId: thread_id || null,
|
|
654
|
+
})
|
|
655
|
+
} catch {
|
|
656
|
+
/* ignore */
|
|
657
|
+
}
|
|
658
|
+
})()
|
|
628
659
|
} else if (notification.method === "turn/completed") {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
660
|
+
;(async () => {
|
|
661
|
+
let contextPct = null
|
|
662
|
+
try {
|
|
663
|
+
const usage = await codexUsageForThread(thread_id)
|
|
664
|
+
contextPct = typeof usage?.context === "number" ? usage.context : null
|
|
665
|
+
} catch {
|
|
666
|
+
/* ignore */
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
recordChatActivity(cwd, {
|
|
670
|
+
status: "waiting",
|
|
671
|
+
contextPct,
|
|
672
|
+
inputPending: false,
|
|
673
|
+
cliKind: "codex",
|
|
674
|
+
sessionId: thread_id || null,
|
|
675
|
+
})
|
|
676
|
+
} catch {
|
|
677
|
+
/* ignore */
|
|
678
|
+
}
|
|
679
|
+
})()
|
|
634
680
|
}
|
|
635
681
|
client.send({ type: "codex.event", stream_id, thread_id, event: notification })
|
|
636
682
|
})
|
|
@@ -868,6 +914,14 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
868
914
|
await tuiViewerRegistry.start()
|
|
869
915
|
ctx.tuiViewerRegistry = tuiViewerRegistry
|
|
870
916
|
|
|
917
|
+
// sibling-inject (ask-sibling スキル, 2026-07-04): 同一 cwd の claude⇄codex
|
|
918
|
+
// 相互依頼のためのローカル IPC 受信口。cwd スコープ capability トークン
|
|
919
|
+
// (src/sibling-token.mjs) で検証した要求のみ claudeBridge/codexBridge.sendToCwd()
|
|
920
|
+
// へ橋渡しする。
|
|
921
|
+
const siblingInjectBridge = new SiblingInjectBridge({ claudeBridge, codexBridge, logger })
|
|
922
|
+
await siblingInjectBridge.start()
|
|
923
|
+
ctx.siblingInjectBridge = siblingInjectBridge
|
|
924
|
+
|
|
871
925
|
// jsonl ライブ追従 (T04784 逐次表示): TUI チャットの閲覧ハートビートを起点に対象
|
|
872
926
|
// session の jsonl を tail し、追記行を claude.jsonl.event として逐次 push する。
|
|
873
927
|
// これで session.event 合図 → 全体再 hydrate + 2.5s ポーリングの「どさっと」を廃せる。
|
|
@@ -926,6 +980,7 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
926
980
|
tuiPermissionBridge.stop()
|
|
927
981
|
tuiViewerRegistry.stop()
|
|
928
982
|
jsonlLiveWatchers.stop()
|
|
983
|
+
siblingInjectBridge.stop()
|
|
929
984
|
ptyBridge.shutdown()
|
|
930
985
|
claudeBridge?.shutdown?.()
|
|
931
986
|
codexBridge?.shutdown?.()
|
|
@@ -1405,6 +1460,59 @@ export function applyChatOverlay(paneStatus, paneContextPct, chat) {
|
|
|
1405
1460
|
return { status, contextPct }
|
|
1406
1461
|
}
|
|
1407
1462
|
|
|
1463
|
+
/**
|
|
1464
|
+
* S1 (2026-07): claude.attach / codex.attach の直後に、その cwd に対応する session.state を
|
|
1465
|
+
* 1 回だけ即時 push する。startStateLoop の周期 (既定 5s) を待つと、エンジン切替や新規
|
|
1466
|
+
* セッション選択の直後もサイドバーのドーナツ/ステータスドットが前セッションの値のまま
|
|
1467
|
+
* 数秒間残ってしまう (症状③④)。claude.tui.bind が既に持つ「即時ステータススナップショット」
|
|
1468
|
+
* (main.mjs 内 detectSessionState 直後の push) と同じ発想で、チャット (SDK/app-server) 側にも
|
|
1469
|
+
* 対称の即時 push を用意する。
|
|
1470
|
+
*
|
|
1471
|
+
* cwd → session_name の解決: claude.attach / codex.attach は tmux の session_name を知らない
|
|
1472
|
+
* (browser は cwd/stream_id しか送らない) ため、listSessionStates で全 tmux session を走査し
|
|
1473
|
+
* cwd が一致するものへ push する (通常は 1 cwd = 1 tmux session)。tmux 自体が無い / 一致が
|
|
1474
|
+
* 無ければ何もしない (安全側、次の poll 系フォールバックに委ねる)。
|
|
1475
|
+
*
|
|
1476
|
+
* `listSessionStatesFn` は単体テスト用の DI ポイント (既定は実 `listSessionStates`、tmux を
|
|
1477
|
+
* 起動する)。実運用では省略してよい。
|
|
1478
|
+
*
|
|
1479
|
+
* @param {{ client: object, plugins: object, logger?: object, cwd: string, cliKind: 'claude'|'codex', sessionId: string|null, contextPct?: number|null, listSessionStatesFn?: Function }} args
|
|
1480
|
+
*/
|
|
1481
|
+
export async function pushImmediateChatState({
|
|
1482
|
+
client,
|
|
1483
|
+
plugins,
|
|
1484
|
+
logger,
|
|
1485
|
+
cwd,
|
|
1486
|
+
cliKind,
|
|
1487
|
+
sessionId,
|
|
1488
|
+
contextPct = 0,
|
|
1489
|
+
listSessionStatesFn = listSessionStates,
|
|
1490
|
+
}) {
|
|
1491
|
+
if (!cwd || !client) return
|
|
1492
|
+
try {
|
|
1493
|
+
const states = await listSessionStatesFn({ plugins, logger })
|
|
1494
|
+
for (const s of states) {
|
|
1495
|
+
if (s.cwd !== cwd) continue
|
|
1496
|
+
client.send({
|
|
1497
|
+
type: "session.state",
|
|
1498
|
+
session_name: s.session_name,
|
|
1499
|
+
status: "waiting",
|
|
1500
|
+
context_pct: typeof contextPct === "number" ? contextPct : 0,
|
|
1501
|
+
permission_mode: s.permission_mode ?? null,
|
|
1502
|
+
stable: true,
|
|
1503
|
+
proc_busy: false,
|
|
1504
|
+
child_busy: false,
|
|
1505
|
+
stalled: false,
|
|
1506
|
+
turn_active: false,
|
|
1507
|
+
cli_kind: cliKind,
|
|
1508
|
+
chat_session_id: sessionId ?? null,
|
|
1509
|
+
})
|
|
1510
|
+
}
|
|
1511
|
+
} catch (err) {
|
|
1512
|
+
logger?.warn?.({ err: err?.message, cwd }, "pushImmediateChatState failed")
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1408
1516
|
/**
|
|
1409
1517
|
* turnActiveForCwd / jsonlContextForCwd に渡す bound session_id を解決する純関数。
|
|
1410
1518
|
*
|
|
@@ -1628,6 +1736,13 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
1628
1736
|
turnActive = null
|
|
1629
1737
|
}
|
|
1630
1738
|
|
|
1739
|
+
// S1 (2026-07): チャット信号 (getChatSignal) が identity (cliKind/sessionId) を
|
|
1740
|
+
// 持っていれば、それをそのまま session.state に同梱する。frontend はこれで「今
|
|
1741
|
+
// 表示中のエンジン/セッションと一致する push か」を判定し、不一致の値を適用しない
|
|
1742
|
+
// (前セッションの値を上書きし続ける症状③④の frontend 側の受け口)。
|
|
1743
|
+
const chatCliKind = chat?.cliKind ?? null
|
|
1744
|
+
const chatSessionId = chat?.sessionId ?? null
|
|
1745
|
+
|
|
1631
1746
|
const prev = lastByName.get(s.session_name)
|
|
1632
1747
|
if (
|
|
1633
1748
|
!prev ||
|
|
@@ -1638,7 +1753,9 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
1638
1753
|
prev.proc_busy !== procBusy ||
|
|
1639
1754
|
prev.child_busy !== childBusy ||
|
|
1640
1755
|
prev.stalled !== stalled ||
|
|
1641
|
-
prev.turn_active !== turnActive
|
|
1756
|
+
prev.turn_active !== turnActive ||
|
|
1757
|
+
prev.cli_kind !== chatCliKind ||
|
|
1758
|
+
prev.chat_session_id !== chatSessionId
|
|
1642
1759
|
) {
|
|
1643
1760
|
lastByName.set(s.session_name, {
|
|
1644
1761
|
status,
|
|
@@ -1649,6 +1766,8 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
1649
1766
|
child_busy: childBusy,
|
|
1650
1767
|
stalled,
|
|
1651
1768
|
turn_active: turnActive,
|
|
1769
|
+
cli_kind: chatCliKind,
|
|
1770
|
+
chat_session_id: chatSessionId,
|
|
1652
1771
|
})
|
|
1653
1772
|
// 計装 (2026-06-19, 既定 OFF=2026-06-20): 状態変化時に値を記録する。常時 info はログ肥大の
|
|
1654
1773
|
// 主因のため HUB_AGENT_STATE_TRACE=1 のときだけ出す。childBusy/outputActive を分けて出し、
|
|
@@ -1666,6 +1785,8 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
1666
1785
|
stable,
|
|
1667
1786
|
context_pct: contextPct,
|
|
1668
1787
|
turn_active: turnActive,
|
|
1788
|
+
cli_kind: chatCliKind,
|
|
1789
|
+
chat_session_id: chatSessionId,
|
|
1669
1790
|
},
|
|
1670
1791
|
"session.state push",
|
|
1671
1792
|
)
|
|
@@ -1685,6 +1806,11 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
1685
1806
|
// turn_active: ステータスドット 4 状態統一の権威ソース。null は旧クライアント互換
|
|
1686
1807
|
// (frontend は欠落時に旧 prompt_submit/stale 降格ロジックへフォールバック)。
|
|
1687
1808
|
turn_active: turnActive,
|
|
1809
|
+
// cli_kind / chat_session_id (S1, 2026-07): このセッションの現在の chat 信号が
|
|
1810
|
+
// どの CLI・どのセッションのものか。チャット信号が無い (TUI 専用/未対応) 行では
|
|
1811
|
+
// null (旧クライアント互換、frontend は無条件マージにフォールバック)。
|
|
1812
|
+
cli_kind: chatCliKind,
|
|
1813
|
+
chat_session_id: chatSessionId,
|
|
1688
1814
|
})
|
|
1689
1815
|
}
|
|
1690
1816
|
// 消滅セッションの GC (perf監査/メモリリーク対策): tmux から消えたセッションの状態 Map を
|
|
@@ -1906,6 +2032,31 @@ function rememberInputId(input_id) {
|
|
|
1906
2032
|
}
|
|
1907
2033
|
}
|
|
1908
2034
|
|
|
2035
|
+
/**
|
|
2036
|
+
* SDK チャット配達保証 (S2, 2026-07-05) 用の冪等化レジストリ。
|
|
2037
|
+
*
|
|
2038
|
+
* `claude.input` / `codex.input` に browser が付与する `input_id` を記憶し、ack
|
|
2039
|
+
* ウォッチドッグ再送 (browser 側が 5s 以内に ack/nack を受け取れず再送するケース) で
|
|
2040
|
+
* 同じメッセージが 2 回ターン実行されないようにする。tracked pty.data
|
|
2041
|
+
* (`seenInputIds` 上) と同じ設計だが、チャンネル別に Set を分ける
|
|
2042
|
+
* (input_id の採番元は browser 側で共通の可能性があるが、pty と chat で
|
|
2043
|
+
* 意味論が異なる衝突を避けるため)。
|
|
2044
|
+
*/
|
|
2045
|
+
function makeSeenInputIdRegistry(max = SEEN_INPUT_IDS_MAX) {
|
|
2046
|
+
const seen = new Set()
|
|
2047
|
+
return {
|
|
2048
|
+
has: (id) => seen.has(id),
|
|
2049
|
+
remember: (id) => {
|
|
2050
|
+
seen.add(id)
|
|
2051
|
+
while (seen.size > max) {
|
|
2052
|
+
seen.delete(seen.values().next().value)
|
|
2053
|
+
}
|
|
2054
|
+
},
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
const seenClaudeChatInputIds = makeSeenInputIdRegistry()
|
|
2058
|
+
const seenCodexChatInputIds = makeSeenInputIdRegistry()
|
|
2059
|
+
|
|
1909
2060
|
/**
|
|
1910
2061
|
* input_id 付き pty.data (tracked 入力) の処理。write 成功で pty.input.ack を返し、
|
|
1911
2062
|
* stream 不在なら pty.error を返して browser の再 attach → 再送に繋げる。
|
|
@@ -1949,6 +2100,138 @@ export function handleUntrackedPtyData(msg, ctx) {
|
|
|
1949
2100
|
}
|
|
1950
2101
|
}
|
|
1951
2102
|
|
|
2103
|
+
/**
|
|
2104
|
+
* SDK チャット配達保証 (S2, 2026-07-05): `claude.input` を処理し、browser が付与した
|
|
2105
|
+
* `input_id` があれば `claude.input.ack` / `claude.input.nack` を返す。無ければ従来
|
|
2106
|
+
* どおり無応答の fire-and-forget (旧 frontend / 内部呼び出しとの後方互換)。
|
|
2107
|
+
*
|
|
2108
|
+
* `ClaudeStreamBridge.input()` の戻り値 `{ ok, reason? }` は「ターン完走」ではなく
|
|
2109
|
+
* 「キュー/常駐 query への投入が完了したか」を表す (ack を素早く返すため)。
|
|
2110
|
+
* テストから直接呼べるよう dispatch から切り出して export する
|
|
2111
|
+
* (handleTrackedPtyData と同じ設計)。
|
|
2112
|
+
*/
|
|
2113
|
+
export function handleClaudeChatInput(msg, ctx) {
|
|
2114
|
+
const stream_id = msg.stream_id
|
|
2115
|
+
const input_id = typeof msg.input_id === "string" && msg.input_id ? msg.input_id : null
|
|
2116
|
+
if (!ctx.claudeBridge) {
|
|
2117
|
+
if (input_id) {
|
|
2118
|
+
ctx.client.send({
|
|
2119
|
+
type: "claude.input.nack",
|
|
2120
|
+
stream_id,
|
|
2121
|
+
input_id,
|
|
2122
|
+
reason: "stream_missing",
|
|
2123
|
+
})
|
|
2124
|
+
}
|
|
2125
|
+
return undefined
|
|
2126
|
+
}
|
|
2127
|
+
if (input_id && seenClaudeChatInputIds.has(input_id)) {
|
|
2128
|
+
// 再送 (ack 喪失想定)。同じターンを 2 回実行しないよう sendMessage を
|
|
2129
|
+
// 呼び直さず ack のみ返す (tracked pty.data の dedup と同じ考え方)。
|
|
2130
|
+
ctx.logger?.info?.({ stream_id, input_id }, "claude.input dedup (ack only)")
|
|
2131
|
+
ctx.client.send({ type: "claude.input.ack", stream_id, input_id })
|
|
2132
|
+
return undefined
|
|
2133
|
+
}
|
|
2134
|
+
return ctx.claudeBridge
|
|
2135
|
+
.input({
|
|
2136
|
+
stream_id,
|
|
2137
|
+
message: msg.message,
|
|
2138
|
+
// ultracode (0.6.28): browser がこのメッセージ単位で送るワンショット指定。
|
|
2139
|
+
// true のときだけそのターンを xhigh effort + dynamic-workflow で実行する。
|
|
2140
|
+
ultracode: msg.ultracode === true,
|
|
2141
|
+
})
|
|
2142
|
+
.then((result) => {
|
|
2143
|
+
if (!input_id) return
|
|
2144
|
+
if (result?.ok) {
|
|
2145
|
+
seenClaudeChatInputIds.remember(input_id)
|
|
2146
|
+
ctx.client.send({ type: "claude.input.ack", stream_id, input_id })
|
|
2147
|
+
} else {
|
|
2148
|
+
ctx.client.send({
|
|
2149
|
+
type: "claude.input.nack",
|
|
2150
|
+
stream_id,
|
|
2151
|
+
input_id,
|
|
2152
|
+
reason: result?.reason || "queue_dead",
|
|
2153
|
+
})
|
|
2154
|
+
}
|
|
2155
|
+
})
|
|
2156
|
+
.catch((err) => {
|
|
2157
|
+
ctx.logger?.error?.(
|
|
2158
|
+
{ stream_id, err: err?.message },
|
|
2159
|
+
"claude.input dispatch threw unexpectedly",
|
|
2160
|
+
)
|
|
2161
|
+
if (input_id) {
|
|
2162
|
+
ctx.client.send({
|
|
2163
|
+
type: "claude.input.nack",
|
|
2164
|
+
stream_id,
|
|
2165
|
+
input_id,
|
|
2166
|
+
reason: "error",
|
|
2167
|
+
})
|
|
2168
|
+
}
|
|
2169
|
+
})
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/**
|
|
2173
|
+
* SDK チャット配達保証 (S2, 2026-07-05): `codex.input` を処理し、browser が付与した
|
|
2174
|
+
* `input_id` があれば `codex.input.ack` / `codex.input.nack` を返す (claude.input と
|
|
2175
|
+
* 対称)。`CodexStreamBridge.input()` は `turn/start` / `turn/steer` の JSON-RPC 呼び出し
|
|
2176
|
+
* そのものを await する設計のため、resolve/reject がそのまま ack/nack の判定になる。
|
|
2177
|
+
* 既存の `codex.error` 通知も維持する (input_id を知らない旧 frontend や、他の消費者向け)。
|
|
2178
|
+
* テストから直接呼べるよう dispatch から切り出して export する。
|
|
2179
|
+
*/
|
|
2180
|
+
export function handleCodexChatInput(msg, ctx) {
|
|
2181
|
+
const stream_id = msg.stream_id
|
|
2182
|
+
const input_id = typeof msg.input_id === "string" && msg.input_id ? msg.input_id : null
|
|
2183
|
+
if (!ctx.codexBridge) {
|
|
2184
|
+
if (input_id) {
|
|
2185
|
+
ctx.client.send({
|
|
2186
|
+
type: "codex.input.nack",
|
|
2187
|
+
stream_id,
|
|
2188
|
+
input_id,
|
|
2189
|
+
reason: "stream_missing",
|
|
2190
|
+
})
|
|
2191
|
+
}
|
|
2192
|
+
return undefined
|
|
2193
|
+
}
|
|
2194
|
+
if (input_id && seenCodexChatInputIds.has(input_id)) {
|
|
2195
|
+
// 再送 (ack 喪失想定)。turn/start・turn/steer を再送出せず ack のみ返す
|
|
2196
|
+
// (二重ターン実行を避ける。claude.input dedup と同じ考え方)。
|
|
2197
|
+
ctx.logger?.info?.({ stream_id, input_id }, "codex.input dedup (ack only)")
|
|
2198
|
+
ctx.client.send({ type: "codex.input.ack", stream_id, input_id })
|
|
2199
|
+
return undefined
|
|
2200
|
+
}
|
|
2201
|
+
return ctx.codexBridge
|
|
2202
|
+
.input({
|
|
2203
|
+
stream_id,
|
|
2204
|
+
message: msg.message,
|
|
2205
|
+
model: msg.model || undefined,
|
|
2206
|
+
effort: msg.effort || undefined,
|
|
2207
|
+
})
|
|
2208
|
+
.then(() => {
|
|
2209
|
+
if (!input_id) return
|
|
2210
|
+
seenCodexChatInputIds.remember(input_id)
|
|
2211
|
+
ctx.client.send({ type: "codex.input.ack", stream_id, input_id })
|
|
2212
|
+
})
|
|
2213
|
+
.catch((err) => {
|
|
2214
|
+
if (input_id) {
|
|
2215
|
+
ctx.client.send({
|
|
2216
|
+
type: "codex.input.nack",
|
|
2217
|
+
stream_id,
|
|
2218
|
+
input_id,
|
|
2219
|
+
reason: classifyCodexInputError(err),
|
|
2220
|
+
})
|
|
2221
|
+
}
|
|
2222
|
+
// 既存の error 通知も維持する (input_id を知らない旧 frontend や、
|
|
2223
|
+
// input_id 以外の理由でエラー表示を必要とする他の消費者向け)。S3 Codex 堅牢化
|
|
2224
|
+
// 由来の err.reason ('timeout' / 'turn_mismatch_retry_failed') があれば
|
|
2225
|
+
// 引き続き codex.error にも乗せる (S2 導入前からの互換維持)。
|
|
2226
|
+
ctx.client.send({
|
|
2227
|
+
type: "codex.error",
|
|
2228
|
+
stream_id,
|
|
2229
|
+
error: err.message,
|
|
2230
|
+
...(err.reason ? { reason: err.reason } : {}),
|
|
2231
|
+
})
|
|
2232
|
+
})
|
|
2233
|
+
}
|
|
2234
|
+
|
|
1952
2235
|
/**
|
|
1953
2236
|
* 離席中キュー投入 (claude.tui.queue.flush) の冪等化用に、処理済み flush_id を記憶する。
|
|
1954
2237
|
* WS 再送で同じ flush が二重に届いても二重 paste しないため。上限を超えたら全消去 (best-effort)。
|
|
@@ -2280,9 +2563,11 @@ async function dispatch(msg, ctx) {
|
|
|
2280
2563
|
// cwd は明示指定が無ければ agent.json の cwd → HOME の順で fallback
|
|
2281
2564
|
// (将来 cockpit から worktree path を指定する用)。
|
|
2282
2565
|
// model / permissionMode は agent.json の値も使えるよう fallback。
|
|
2566
|
+
const cwdArg = msg.cwd || ctx.config?.claude_cwd || undefined
|
|
2567
|
+
const resumeSessionId = msg.resume_session_id || null
|
|
2283
2568
|
const info = ctx.claudeBridge.attach({
|
|
2284
2569
|
stream_id,
|
|
2285
|
-
cwd:
|
|
2570
|
+
cwd: cwdArg,
|
|
2286
2571
|
model: msg.model || ctx.config?.claude_model || null,
|
|
2287
2572
|
permissionMode:
|
|
2288
2573
|
msg.permission_mode ||
|
|
@@ -2297,13 +2582,67 @@ async function dispatch(msg, ctx) {
|
|
|
2297
2582
|
// effort (Opus 4.6+): browser がチャット既定 (agent 設定由来) を送る。
|
|
2298
2583
|
// 空文字/未指定なら null (SDK / Claude Code 既定 = Opus は high)。
|
|
2299
2584
|
effort: msg.effort || ctx.config?.claude_effort || null,
|
|
2300
|
-
resumeSessionId
|
|
2585
|
+
resumeSessionId,
|
|
2301
2586
|
})
|
|
2302
2587
|
ctx.client.send({
|
|
2303
2588
|
type: "claude.ready",
|
|
2304
2589
|
stream_id,
|
|
2305
2590
|
resuming: info.resuming,
|
|
2306
2591
|
})
|
|
2592
|
+
// S1 (2026-07): セッション状態の identity 正本化。TUI の claude.tui.bind
|
|
2593
|
+
// (main.mjs 内 clearChatSignal 呼び出し箇所参照) と対称に、SDK チャットの
|
|
2594
|
+
// attach 時点でも前セッションのチャット信号を引き継がない。
|
|
2595
|
+
// - fresh (resume 無し): 新規セッションで session_id はまだ確定しない
|
|
2596
|
+
// (SDK が最初の assistant イベントで通知する) ため null。前の信号を破棄し
|
|
2597
|
+
// ゼロ値を即時 push してドーナツ/ドットを即座にリセットする (症状④)。
|
|
2598
|
+
// - resume: bound session の jsonl から実 context% を解決し、cli_kind='claude' +
|
|
2599
|
+
// 実 session_id で identity を確定させて即時 push する (症状③)。
|
|
2600
|
+
const resolvedCwd = cwdArg || process.env.HOME || process.cwd()
|
|
2601
|
+
if (resolvedCwd) {
|
|
2602
|
+
if (!resumeSessionId) {
|
|
2603
|
+
clearChatSignal(resolvedCwd)
|
|
2604
|
+
recordChatActivity(resolvedCwd, {
|
|
2605
|
+
status: "waiting",
|
|
2606
|
+
contextPct: 0,
|
|
2607
|
+
inputPending: false,
|
|
2608
|
+
cliKind: "claude",
|
|
2609
|
+
sessionId: null,
|
|
2610
|
+
})
|
|
2611
|
+
pushImmediateChatState({
|
|
2612
|
+
client: ctx.client,
|
|
2613
|
+
plugins: ctx.plugins,
|
|
2614
|
+
logger: ctx.logger,
|
|
2615
|
+
cwd: resolvedCwd,
|
|
2616
|
+
cliKind: "claude",
|
|
2617
|
+
sessionId: null,
|
|
2618
|
+
contextPct: 0,
|
|
2619
|
+
}).catch(() => {})
|
|
2620
|
+
} else {
|
|
2621
|
+
jsonlContextForCwd(resolvedCwd, resumeSessionId)
|
|
2622
|
+
.catch(() => null)
|
|
2623
|
+
.then((jsonlCtx) => {
|
|
2624
|
+
const contextPct =
|
|
2625
|
+
typeof jsonlCtx?.percent === "number" ? jsonlCtx.percent : 0
|
|
2626
|
+
recordChatActivity(resolvedCwd, {
|
|
2627
|
+
status: "waiting",
|
|
2628
|
+
contextPct,
|
|
2629
|
+
inputPending: false,
|
|
2630
|
+
cliKind: "claude",
|
|
2631
|
+
sessionId: resumeSessionId,
|
|
2632
|
+
})
|
|
2633
|
+
return pushImmediateChatState({
|
|
2634
|
+
client: ctx.client,
|
|
2635
|
+
plugins: ctx.plugins,
|
|
2636
|
+
logger: ctx.logger,
|
|
2637
|
+
cwd: resolvedCwd,
|
|
2638
|
+
cliKind: "claude",
|
|
2639
|
+
sessionId: resumeSessionId,
|
|
2640
|
+
contextPct,
|
|
2641
|
+
})
|
|
2642
|
+
})
|
|
2643
|
+
.catch(() => {})
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2307
2646
|
} catch (err) {
|
|
2308
2647
|
ctx.client.send({
|
|
2309
2648
|
type: "claude.error",
|
|
@@ -2315,14 +2654,7 @@ async function dispatch(msg, ctx) {
|
|
|
2315
2654
|
}
|
|
2316
2655
|
case "claude.input":
|
|
2317
2656
|
// message: { role: 'user', content: string | Array } を期待
|
|
2318
|
-
|
|
2319
|
-
ctx.claudeBridge.input({
|
|
2320
|
-
stream_id: msg.stream_id,
|
|
2321
|
-
message: msg.message,
|
|
2322
|
-
// ultracode (0.6.28): browser がこのメッセージ単位で送るワンショット指定。
|
|
2323
|
-
// true のときだけそのターンを xhigh effort + dynamic-workflow で実行する。
|
|
2324
|
-
ultracode: msg.ultracode === true,
|
|
2325
|
-
})
|
|
2657
|
+
handleClaudeChatInput(msg, ctx)
|
|
2326
2658
|
return
|
|
2327
2659
|
case "claude.upload": {
|
|
2328
2660
|
// Cockpit チャットモードの添付ファイル受信 (チャンク 1 件)。全チャンク到着で
|
|
@@ -3271,14 +3603,16 @@ async function dispatch(msg, ctx) {
|
|
|
3271
3603
|
return
|
|
3272
3604
|
}
|
|
3273
3605
|
try {
|
|
3606
|
+
const resolvedCwd = msg.cwd || ctx.config?.claude_cwd || undefined
|
|
3607
|
+
const resumeThreadId = msg.resume_thread_id || null
|
|
3274
3608
|
const info = await ctx.codexBridge.attach({
|
|
3275
3609
|
stream_id,
|
|
3276
|
-
cwd:
|
|
3610
|
+
cwd: resolvedCwd,
|
|
3277
3611
|
model: msg.model || undefined,
|
|
3278
3612
|
effort: msg.effort || undefined,
|
|
3279
3613
|
approvalPolicy: msg.approval_policy || undefined,
|
|
3280
3614
|
sandbox: msg.sandbox || undefined,
|
|
3281
|
-
resumeThreadId
|
|
3615
|
+
resumeThreadId,
|
|
3282
3616
|
})
|
|
3283
3617
|
ctx.client.send({
|
|
3284
3618
|
type: "codex.ready",
|
|
@@ -3290,34 +3624,130 @@ async function dispatch(msg, ctx) {
|
|
|
3290
3624
|
// 変更は不要 (_AGENT_PER_STREAM_TYPES の codex.ready がそのまま運ぶ)。
|
|
3291
3625
|
history: info.history || [],
|
|
3292
3626
|
})
|
|
3627
|
+
// S1 (2026-07): claude.attach と対称の identity 正本化。Codex は thread/start
|
|
3628
|
+
// 応答時点で threadId が確定するため (Claude の fresh とは異なり)、fresh でも
|
|
3629
|
+
// 既に分かっている threadId を sessionId として記録する (直後の turn/started が
|
|
3630
|
+
// 同じ threadId を伴って届いても identity 一致でリセットが起きないように)。
|
|
3631
|
+
// - fresh (resume 無し): ゼロ値 + 確定済み threadId で即時 push (症状④)。
|
|
3632
|
+
// - resume: codexUsageForThread で実 context% を解決し即時 push (症状③)。
|
|
3633
|
+
if (resolvedCwd) {
|
|
3634
|
+
if (!resumeThreadId) {
|
|
3635
|
+
clearChatSignal(resolvedCwd)
|
|
3636
|
+
recordChatActivity(resolvedCwd, {
|
|
3637
|
+
status: "waiting",
|
|
3638
|
+
contextPct: 0,
|
|
3639
|
+
inputPending: false,
|
|
3640
|
+
cliKind: "codex",
|
|
3641
|
+
sessionId: info.threadId || null,
|
|
3642
|
+
})
|
|
3643
|
+
pushImmediateChatState({
|
|
3644
|
+
client: ctx.client,
|
|
3645
|
+
plugins: ctx.plugins,
|
|
3646
|
+
logger: ctx.logger,
|
|
3647
|
+
cwd: resolvedCwd,
|
|
3648
|
+
cliKind: "codex",
|
|
3649
|
+
sessionId: info.threadId || null,
|
|
3650
|
+
contextPct: 0,
|
|
3651
|
+
}).catch(() => {})
|
|
3652
|
+
} else {
|
|
3653
|
+
codexUsageForThread(info.threadId)
|
|
3654
|
+
.catch(() => null)
|
|
3655
|
+
.then((usage) => {
|
|
3656
|
+
const contextPct = typeof usage?.context === "number" ? usage.context : 0
|
|
3657
|
+
recordChatActivity(resolvedCwd, {
|
|
3658
|
+
status: "waiting",
|
|
3659
|
+
contextPct,
|
|
3660
|
+
inputPending: false,
|
|
3661
|
+
cliKind: "codex",
|
|
3662
|
+
sessionId: info.threadId,
|
|
3663
|
+
})
|
|
3664
|
+
return pushImmediateChatState({
|
|
3665
|
+
client: ctx.client,
|
|
3666
|
+
plugins: ctx.plugins,
|
|
3667
|
+
logger: ctx.logger,
|
|
3668
|
+
cwd: resolvedCwd,
|
|
3669
|
+
cliKind: "codex",
|
|
3670
|
+
sessionId: info.threadId,
|
|
3671
|
+
contextPct,
|
|
3672
|
+
})
|
|
3673
|
+
})
|
|
3674
|
+
.catch(() => {})
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3293
3677
|
} catch (err) {
|
|
3294
3678
|
ctx.client.send({
|
|
3295
3679
|
type: "codex.error",
|
|
3296
3680
|
stream_id,
|
|
3297
3681
|
error: err.message,
|
|
3682
|
+
// S3 Codex 堅牢化: request() タイムアウト (codex-appserver-client.mjs) は
|
|
3683
|
+
// err.reason='timeout' を付与する。frontend が理由別の文言/表示を出し分け
|
|
3684
|
+
// られるよう素通しする (無ければ undefined = キー自体を省略)。
|
|
3685
|
+
...(err.reason ? { reason: err.reason } : {}),
|
|
3298
3686
|
})
|
|
3299
3687
|
}
|
|
3300
3688
|
return
|
|
3301
3689
|
}
|
|
3302
|
-
case "codex.
|
|
3303
|
-
//
|
|
3304
|
-
// (
|
|
3305
|
-
//
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3690
|
+
case "codex.upload": {
|
|
3691
|
+
// B4 (T05699 残タスク, 2026-07-04): Cockpit Codexチャットの添付ファイル受信
|
|
3692
|
+
// (チャンク1件)。claude.upload と同じ UploadManager を再利用する (cwd ベースで
|
|
3693
|
+
// CLI 非依存の実装のため無改修で流用可能)。全チャンク到着で cwd 配下の
|
|
3694
|
+
// .cockpit-uploads/<日付>/ に保存し、保存パスを browser へ返す。browser は
|
|
3695
|
+
// そのパスを codex.input 本文に載せ、Codex がシェル/ファイルツールで読む
|
|
3696
|
+
// (Claude 版と同じ「パス参照方式」、Codex 側の model/effort 分離と同様に
|
|
3697
|
+
// アカウント/プロトコルが違うだけでファイル保存の仕組みは共有する)。
|
|
3698
|
+
if (!ctx.uploadManager) {
|
|
3699
|
+
ctx.client.send({
|
|
3700
|
+
type: "codex.upload.done",
|
|
3309
3701
|
stream_id: msg.stream_id,
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3702
|
+
upload_id: msg.upload_id,
|
|
3703
|
+
ok: false,
|
|
3704
|
+
error: "upload_unavailable",
|
|
3705
|
+
})
|
|
3706
|
+
return
|
|
3707
|
+
}
|
|
3708
|
+
try {
|
|
3709
|
+
const res = await ctx.uploadManager.addChunk({
|
|
3710
|
+
upload_id: msg.upload_id,
|
|
3711
|
+
filename: msg.filename,
|
|
3712
|
+
media_type: msg.media_type,
|
|
3713
|
+
total_size: msg.total_size,
|
|
3714
|
+
total_chunks: msg.total_chunks,
|
|
3715
|
+
chunk_index: msg.chunk_index,
|
|
3716
|
+
data: msg.data,
|
|
3717
|
+
cwd: msg.cwd || ctx.config?.claude_cwd || undefined,
|
|
3313
3718
|
})
|
|
3314
|
-
.
|
|
3719
|
+
if (res.done) {
|
|
3315
3720
|
ctx.client.send({
|
|
3316
|
-
type: "codex.
|
|
3721
|
+
type: "codex.upload.done",
|
|
3317
3722
|
stream_id: msg.stream_id,
|
|
3318
|
-
|
|
3723
|
+
upload_id: msg.upload_id,
|
|
3724
|
+
ok: true,
|
|
3725
|
+
path: res.path,
|
|
3726
|
+
filename: res.filename,
|
|
3727
|
+
media_type: res.media_type,
|
|
3728
|
+
size: res.size,
|
|
3319
3729
|
})
|
|
3730
|
+
}
|
|
3731
|
+
} catch (err) {
|
|
3732
|
+
ctx.client.send({
|
|
3733
|
+
type: "codex.upload.done",
|
|
3734
|
+
stream_id: msg.stream_id,
|
|
3735
|
+
upload_id: msg.upload_id,
|
|
3736
|
+
ok: false,
|
|
3737
|
+
error: err?.message || String(err),
|
|
3320
3738
|
})
|
|
3739
|
+
}
|
|
3740
|
+
return
|
|
3741
|
+
}
|
|
3742
|
+
case "codex.input":
|
|
3743
|
+
// model/effort: turn/start (非 busy 時) にのみ反映される override。turn/steer
|
|
3744
|
+
// (busy 時) は公式スキーマ上 model/effort 非対応のため、その場合は
|
|
3745
|
+
// codex-stream-bridge.mjs 側で無視され次の非 busy な input() から効く。
|
|
3746
|
+
// S2/S3 統合 (2026-07-05): S3 の自己修復 (turn/steer 失敗 → turn/start 自動
|
|
3747
|
+
// リトライ) が付与する err.reason ('timeout' / 'turn_mismatch_retry_failed') を
|
|
3748
|
+
// classifyCodexInputError が優先採用し、input_id 付きなら codex.input.nack の
|
|
3749
|
+
// reason としてそのまま使う (handleCodexChatInput 内)。
|
|
3750
|
+
handleCodexChatInput(msg, ctx)
|
|
3321
3751
|
return
|
|
3322
3752
|
case "codex.permission.reply":
|
|
3323
3753
|
if (!ctx.codexBridge) return
|