@cocorograph/hub-agent 0.8.114 → 0.8.115
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 +2 -1
- package/src/chat-removal-guard.mjs +1 -1
- package/src/chat-session-starter.mjs +3 -2
- package/src/codex-binary.mjs +43 -27
- package/src/config.mjs +9 -4
- package/src/dispatch-scheduler.mjs +13 -11
- package/src/grok-acp-client.mjs +130 -0
- package/src/grok-binary.mjs +50 -0
- package/src/grok-stream-bridge.mjs +756 -0
- package/src/hub-bundle.mjs +183 -26
- package/src/live-sessions.mjs +12 -11
- package/src/main.mjs +286 -56
- package/src/review-event-store.mjs +1 -1
- package/src/session-names.mjs +21 -12
- package/src/sibling-inject.mjs +12 -8
- package/src/ws-client.mjs +26 -0
package/src/main.mjs
CHANGED
|
@@ -55,6 +55,8 @@ import { AsyncTtlCache } from "./async-ttl-cache.mjs"
|
|
|
55
55
|
import { makePersistentSeenInputIdRegistry } from "./seen-input-id-store.mjs"
|
|
56
56
|
import { ClaudeStreamBridge } from "./claude-stream-bridge.mjs"
|
|
57
57
|
import { installClaudeDisplayMask } from "./claude-display-mask.mjs"
|
|
58
|
+
import { GrokStreamBridge } from "./grok-stream-bridge.mjs"
|
|
59
|
+
import { resolveGrokBinary } from "./grok-binary.mjs"
|
|
58
60
|
import { CodexMcpControl } from "./codex-mcp-control.mjs"
|
|
59
61
|
import { CodexStreamBridge, classifyCodexInputError } from "./codex-stream-bridge.mjs"
|
|
60
62
|
import { installCodexDisplayMask } from "./codex-result-mask.mjs"
|
|
@@ -280,7 +282,7 @@ export function withDeadline(promise, timeoutMs, onTimeout) {
|
|
|
280
282
|
* WS 受信順 = pane 反映順を守るため false (= 直列キューに残す)。
|
|
281
283
|
*/
|
|
282
284
|
export function isFastPathMessage(type) {
|
|
283
|
-
return type === "claude.interrupt" || type === "codex.interrupt"
|
|
285
|
+
return type === "claude.interrupt" || type === "codex.interrupt" || type === "grok.interrupt"
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
/**
|
|
@@ -316,7 +318,9 @@ export function isFastPathMessage(type) {
|
|
|
316
318
|
*/
|
|
317
319
|
export function defaultLaneCwd(config, mode) {
|
|
318
320
|
const configured = expandTilde(config?.claude_cwd || "") || ""
|
|
319
|
-
|
|
321
|
+
// Grok は handler(handleGrokMessage の attach)もこの関数で補完する。scheduler と同じ値を
|
|
322
|
+
// 使わないと、cwd 省略の attach と HOME 指定の停止が別レーンへ散る。Codex の契約は変えない。
|
|
323
|
+
if (configured || (mode !== "claude" && mode !== "grok")) return configured
|
|
320
324
|
return process.env.HOME || process.cwd()
|
|
321
325
|
}
|
|
322
326
|
|
|
@@ -325,6 +329,7 @@ export function isReadOnlyRpc(type) {
|
|
|
325
329
|
type === "claude.history.request" ||
|
|
326
330
|
type === "claude.sessions.request" ||
|
|
327
331
|
type === "codex.sessions.request" ||
|
|
332
|
+
type === "grok.sessions.request" ||
|
|
328
333
|
type === "codex.history.request" ||
|
|
329
334
|
type === "profile.list" ||
|
|
330
335
|
type === "codex.profile.list" ||
|
|
@@ -580,7 +585,7 @@ export function _resetBundleSyncStateForTest() {
|
|
|
580
585
|
/**
|
|
581
586
|
* ある target(cli_kind)について、要求 version が既に反映済みかを判定する。
|
|
582
587
|
* - codex: mcp_approval_applied_version(無ければ installed_version)が要求以上
|
|
583
|
-
* - claude: installed_version が要求以上
|
|
588
|
+
* - claude: installed_version と、Grok 導入済みなら grok_mcp_applied_version が要求以上
|
|
584
589
|
* auto_update:false のときは「対象外(=要求は満たされたものとして扱う=同期しない)」を返す。
|
|
585
590
|
*/
|
|
586
591
|
async function _bundleTargetSatisfied(cliKind, requestedVersion, readStateImpl, readConfigImpl) {
|
|
@@ -589,6 +594,10 @@ async function _bundleTargetSatisfied(cliKind, requestedVersion, readStateImpl,
|
|
|
589
594
|
if (!requestedVersion) return false
|
|
590
595
|
const filesVer = st.installed_version || "0.0.0"
|
|
591
596
|
if (compareBundleVersions(filesVer, requestedVersion) < 0) return false
|
|
597
|
+
if (cliKind === "claude" && existsSync(process.env.GROK_HOME || path.join(os.homedir(), ".grok"))) {
|
|
598
|
+
const grokVer = st.grok_mcp_applied_version || "0.0.0"
|
|
599
|
+
if (compareBundleVersions(grokVer, requestedVersion) < 0) return false
|
|
600
|
+
}
|
|
592
601
|
if (cliKind === "codex") {
|
|
593
602
|
const approvalVer = st.mcp_approval_applied_version || "0.0.0"
|
|
594
603
|
const approved = compareBundleVersions(approvalVer, requestedVersion) >= 0
|
|
@@ -708,6 +717,7 @@ export class DeferredRestartCoordinator {
|
|
|
708
717
|
constructor({
|
|
709
718
|
claudeBridge,
|
|
710
719
|
codexBridge,
|
|
720
|
+
grokBridge,
|
|
711
721
|
shutdown,
|
|
712
722
|
logger,
|
|
713
723
|
settleMs = 500,
|
|
@@ -719,6 +729,7 @@ export class DeferredRestartCoordinator {
|
|
|
719
729
|
} = {}) {
|
|
720
730
|
this.claudeBridge = claudeBridge
|
|
721
731
|
this.codexBridge = codexBridge
|
|
732
|
+
this.grokBridge = grokBridge
|
|
722
733
|
this.shutdown = shutdown
|
|
723
734
|
this.logger = logger
|
|
724
735
|
this.settleMs = settleMs
|
|
@@ -746,6 +757,7 @@ export class DeferredRestartCoordinator {
|
|
|
746
757
|
return !!(
|
|
747
758
|
this.claudeBridge?.hasActiveWork?.() ||
|
|
748
759
|
this.codexBridge?.hasActiveWork?.() ||
|
|
760
|
+
this.grokBridge?.hasActiveWork?.() ||
|
|
749
761
|
this.workProviders.some((provider) => provider?.hasActiveWork?.())
|
|
750
762
|
)
|
|
751
763
|
}
|
|
@@ -810,7 +822,45 @@ export class DeferredRestartCoordinator {
|
|
|
810
822
|
* `restore:` 購読は browser に存在しないため、ここで捨てないと Hub へ幽霊 stream が流れる。
|
|
811
823
|
* 起動処理から切り出してあるのは、この配線そのものをテストから通すため。
|
|
812
824
|
*/
|
|
813
|
-
export function wireBridgeEvents(client, { claudeBridge, codexBridge }) {
|
|
825
|
+
export function wireBridgeEvents(client, { claudeBridge, codexBridge, grokBridge, logger: bridgeLogger = logger }) {
|
|
826
|
+
if (grokBridge) {
|
|
827
|
+
grokBridge.on("grok_event", ({ stream_id, session_id, seq, update }) => {
|
|
828
|
+
sendBridgeMessage(client, { type: "grok.event", stream_id, thread_id: session_id, seq, event: update })
|
|
829
|
+
})
|
|
830
|
+
grokBridge.on("grok_permission", ({ stream_id, request_id, params }) => {
|
|
831
|
+
sendBridgeMessage(client, { type: "grok.permission.request", stream_id,
|
|
832
|
+
thread_id: grokBridge.getThreadId(stream_id), request_id, params })
|
|
833
|
+
})
|
|
834
|
+
// ターン終了・中断で破棄した承認待ち。browser はこれで該当カードを閉じる。
|
|
835
|
+
grokBridge.on("grok_permission_cancelled", ({ stream_id, session_id, request_ids }) => {
|
|
836
|
+
sendBridgeMessage(client, { type: "grok.permission.cancelled", stream_id,
|
|
837
|
+
...(typeof session_id === "string" && session_id ? { thread_id: session_id } : {}), request_ids })
|
|
838
|
+
})
|
|
839
|
+
// last_seq: 切断中に捨てた本文 chunk(ws-client)の末尾欠落を、後続 update が無くても browser が
|
|
840
|
+
// 検知できるようにする。受信済み seq より大きければ再 attach して history を取り直す。
|
|
841
|
+
const lastSeqField = (lastSeq) => (Number.isInteger(lastSeq) ? { last_seq: lastSeq } : {})
|
|
842
|
+
// thread_id: backend が同じ会話を開いている他端末へ fan-out するために要る(Codex の event と同じ)。
|
|
843
|
+
const threadField = (sessionId) => (typeof sessionId === "string" && sessionId ? { thread_id: sessionId } : {})
|
|
844
|
+
grokBridge.on("grok_queue_state", ({ stream_id, session_id, queue_state }) => {
|
|
845
|
+
sendBridgeMessage(client, { type: "grok.queue.state", stream_id, ...threadField(session_id), queue_state })
|
|
846
|
+
})
|
|
847
|
+
grokBridge.on("grok_turn_complete", ({ stream_id, session_id, stopReason, lastSeq }) => {
|
|
848
|
+
sendBridgeMessage(client, { type: "grok.turn.complete", stream_id, ...threadField(session_id),
|
|
849
|
+
stop_reason: stopReason, ...lastSeqField(lastSeq) })
|
|
850
|
+
})
|
|
851
|
+
grokBridge.on("grok_exit", ({ stream_id, session_id, reason, code, lastSeq }) => {
|
|
852
|
+
sendBridgeMessage(client, { type: "grok.exit", stream_id, thread_id: session_id, reason,
|
|
853
|
+
...(code !== undefined ? { code } : {}), ...lastSeqField(lastSeq) })
|
|
854
|
+
})
|
|
855
|
+
grokBridge.on("grok_error", ({ stream_id, session_id, message, lastSeq }) => {
|
|
856
|
+
if (!stream_id) {
|
|
857
|
+
bridgeLogger?.warn?.({ err: message }, "grok bridge error without stream")
|
|
858
|
+
return
|
|
859
|
+
}
|
|
860
|
+
sendBridgeMessage(client, { type: "grok.error", stream_id, ...threadField(session_id), error: message,
|
|
861
|
+
...lastSeqField(lastSeq) })
|
|
862
|
+
})
|
|
863
|
+
}
|
|
814
864
|
// stream-json モード (Sprint G): Claude Code を SDK 経由で起動し、stream イベントを
|
|
815
865
|
// そのまま browser に転送する。SDK 未インストール時は claudeBridge=null で全 attach が
|
|
816
866
|
// claude.error を返す経路に分岐 (dispatch 側で判定)。
|
|
@@ -962,6 +1012,21 @@ export async function applyStartupEnv({ logger, deps = {} }) {
|
|
|
962
1012
|
}
|
|
963
1013
|
}
|
|
964
1014
|
|
|
1015
|
+
// ACP のプロセスグループ停止期限とは別に、daemon 終了の待機上限を設ける。
|
|
1016
|
+
export async function shutdownGrokBridge(grokBridge, logger, timeoutMs = 6_000) {
|
|
1017
|
+
if (!grokBridge) return
|
|
1018
|
+
try {
|
|
1019
|
+
const result = await withDeadline(
|
|
1020
|
+
Promise.resolve().then(() => grokBridge.shutdown()),
|
|
1021
|
+
timeoutMs,
|
|
1022
|
+
() => logger?.warn?.({ timeout: true }, "grok shutdown incomplete"),
|
|
1023
|
+
)
|
|
1024
|
+
if (result?.unreaped > 0) logger?.warn?.(result, "grok shutdown incomplete")
|
|
1025
|
+
} catch (err) {
|
|
1026
|
+
logger?.warn?.({ err: err?.message }, "grok shutdown failed")
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
965
1030
|
export async function startDaemon({ version, claudeSdk } = {}) {
|
|
966
1031
|
const logRotationTimer = startLogRotation(logger, { isDeferred: hasInheritedOutputChild })
|
|
967
1032
|
startEventLoopMetrics(logger)
|
|
@@ -1023,6 +1088,10 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1023
1088
|
// codex app-server ベースの chat ブリッジ。resolver は各 spawn 時に再評価するため、
|
|
1024
1089
|
// daemon 起動時に一時的に unavailable でも bridge 自体は保持する。
|
|
1025
1090
|
const codexBridge = new CodexStreamBridge({ logger })
|
|
1091
|
+
const grokBridge = availableClis.includes("grok")
|
|
1092
|
+
? new GrokStreamBridge({ grokCmd: resolveGrokBinary().path, logger })
|
|
1093
|
+
: null
|
|
1094
|
+
ctx.grokBridge = grokBridge
|
|
1026
1095
|
|
|
1027
1096
|
// Codex の MCP 状態 control (agent-global, セッション非依存)。MCP は ~/.codex/config.toml
|
|
1028
1097
|
// = アカウント単位の設定で全セッション共有のため、claudeBridge の「MCP control 専用
|
|
@@ -1095,7 +1164,7 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1095
1164
|
logger.warn({ err: err.message }, "ws client error (will retry via close handler)")
|
|
1096
1165
|
})
|
|
1097
1166
|
|
|
1098
|
-
wireBridgeEvents(client, { claudeBridge, codexBridge })
|
|
1167
|
+
wireBridgeEvents(client, { claudeBridge, codexBridge, grokBridge, logger })
|
|
1099
1168
|
|
|
1100
1169
|
// Hub からのメッセージ dispatch は、接続世代 + 機能レーン単位で直列実行する。
|
|
1101
1170
|
//
|
|
@@ -1116,6 +1185,7 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1116
1185
|
client,
|
|
1117
1186
|
claudeBridge,
|
|
1118
1187
|
codexBridge,
|
|
1188
|
+
grokBridge,
|
|
1119
1189
|
codexMcpControl,
|
|
1120
1190
|
uploadManager,
|
|
1121
1191
|
}
|
|
@@ -1148,7 +1218,7 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1148
1218
|
}, 60_000)
|
|
1149
1219
|
|
|
1150
1220
|
// 会話の回収と常駐数制限を定期実行する。
|
|
1151
|
-
const stateLoop = startStateLoop({ client, logger, intervalMs: 5_000, claudeBridge, codexBridge })
|
|
1221
|
+
const stateLoop = startStateLoop({ client, logger, intervalMs: 5_000, claudeBridge, codexBridge, grokBridge })
|
|
1152
1222
|
// bundle hook (cockpit_session_event_hook.py) が /tmp/cockpit_session_events/<name>.json
|
|
1153
1223
|
// に書き出す UserPromptSubmit / Stop / ready の event を fs.watch で拾って WS push する。
|
|
1154
1224
|
// text マーカー判定 (detectStatusFromText) より精度が高い「ターン境界」の signal。
|
|
@@ -1164,9 +1234,10 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1164
1234
|
const siblingInjectBridge = new SiblingInjectBridge({
|
|
1165
1235
|
claudeBridge,
|
|
1166
1236
|
codexBridge,
|
|
1237
|
+
grokBridge,
|
|
1167
1238
|
logger,
|
|
1168
1239
|
starter: ({ cwd, cliKind }) => ensureChatSession({
|
|
1169
|
-
cwd, cliKind, bridges: { claudeBridge, codexBridge }, logger,
|
|
1240
|
+
cwd, cliKind, bridges: { claudeBridge, codexBridge, grokBridge }, logger,
|
|
1170
1241
|
resolveClaudeOptions: () => resolveClaudeAttachOptions(ctx.config),
|
|
1171
1242
|
}),
|
|
1172
1243
|
onRestartRequested: () => restartCoordinator.request(),
|
|
@@ -1209,6 +1280,7 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1209
1280
|
restartCoordinator?.stop?.()
|
|
1210
1281
|
claudeBridge?.shutdown?.()
|
|
1211
1282
|
codexBridge?.shutdown?.()
|
|
1283
|
+
await shutdownGrokBridge(grokBridge, logger)
|
|
1212
1284
|
// 受理済み input_id の保存を待ってから exit する(再起動後の再送で二重実行しないため)。
|
|
1213
1285
|
const persistence = await waitForClaudeInputPersistence()
|
|
1214
1286
|
if (persistence === "timeout") {
|
|
@@ -1223,6 +1295,7 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1223
1295
|
restartCoordinator = new DeferredRestartCoordinator({
|
|
1224
1296
|
claudeBridge,
|
|
1225
1297
|
codexBridge,
|
|
1298
|
+
grokBridge,
|
|
1226
1299
|
shutdown,
|
|
1227
1300
|
logger,
|
|
1228
1301
|
lifecycle,
|
|
@@ -1251,7 +1324,7 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1251
1324
|
)
|
|
1252
1325
|
})
|
|
1253
1326
|
|
|
1254
|
-
return { client, plugins, claudeBridge, codexBridge, codexMcpControl, uploadManager }
|
|
1327
|
+
return { client, plugins, claudeBridge, codexBridge, grokBridge, codexMcpControl, uploadManager }
|
|
1255
1328
|
}
|
|
1256
1329
|
|
|
1257
1330
|
const SESSION_EVENTS_DIR =
|
|
@@ -2160,7 +2233,7 @@ export function decideTurnActiveSource({
|
|
|
2160
2233
|
return "none"
|
|
2161
2234
|
}
|
|
2162
2235
|
|
|
2163
|
-
export function startStateLoop({ client, logger, intervalMs, claudeBridge, codexBridge }) {
|
|
2236
|
+
export function startStateLoop({ client, logger, intervalMs, claudeBridge, codexBridge, grokBridge }) {
|
|
2164
2237
|
// 総 live 上限 M のログ抑制状態。⚠️ **この state loop インスタンスが所有する**(module-global に
|
|
2165
2238
|
// 置くと、複数インスタンスや並行テストが互いのログを抑制してしまう)。「超過しているが全部
|
|
2166
2239
|
// 保護されていて刈れない」tick は 5 秒ごとに同じ行を出し続けるので、状態が変わったときと
|
|
@@ -2203,12 +2276,17 @@ export function startStateLoop({ client, logger, intervalMs, claudeBridge, codex
|
|
|
2203
2276
|
} catch (err) {
|
|
2204
2277
|
logger?.warn({ err: err?.message }, "gcAbandonedSessions failed")
|
|
2205
2278
|
}
|
|
2279
|
+
try {
|
|
2280
|
+
grokBridge?.gcAbandonedSessions?.()
|
|
2281
|
+
} catch (err) {
|
|
2282
|
+
logger?.warn?.({ err: err?.message }, "grok gcAbandonedSessions failed")
|
|
2283
|
+
}
|
|
2206
2284
|
// 総 live 上限 M(claude + codex 合算、既定 20)。per-cwd を 1 → K(既定 3)へ緩めたぶん、
|
|
2207
2285
|
// cwd 単位の境界だけでは総量が膨らむため、cwd をまたいだ天井をここで適用する。
|
|
2208
2286
|
// ⚠️ 両 CLI の reconcile が **終わった後**に呼ぶ(cwd 単位で畳んだ結果の生存数で判定する)。
|
|
2209
2287
|
try {
|
|
2210
2288
|
enforceTotalLiveBound({
|
|
2211
|
-
bridges: [claudeBridge, codexBridge].filter(Boolean),
|
|
2289
|
+
bridges: [claudeBridge, codexBridge, grokBridge].filter(Boolean),
|
|
2212
2290
|
logger,
|
|
2213
2291
|
logState: warmBoundsLogState,
|
|
2214
2292
|
})
|
|
@@ -2402,7 +2480,7 @@ export function handleStreamsSyncResponse(msg, ctx) {
|
|
|
2402
2480
|
const activeIds = new Set(
|
|
2403
2481
|
Array.isArray(msg?.active_stream_ids) ? msg.active_stream_ids : [],
|
|
2404
2482
|
)
|
|
2405
|
-
const activeByMode = { claude: new Set(), codex: new Set() }
|
|
2483
|
+
const activeByMode = { claude: new Set(), codex: new Set(), grok: new Set() }
|
|
2406
2484
|
const classifiedIds = new Set()
|
|
2407
2485
|
if (Array.isArray(msg?.active_streams)) {
|
|
2408
2486
|
for (const item of msg.active_streams) {
|
|
@@ -2440,6 +2518,14 @@ export function handleStreamsSyncResponse(msg, ctx) {
|
|
|
2440
2518
|
ctx.logger,
|
|
2441
2519
|
)
|
|
2442
2520
|
}
|
|
2521
|
+
if (activeByMode.grok.size > 0) {
|
|
2522
|
+
pruneInactiveChatSubscribers(
|
|
2523
|
+
ctx.grokBridge,
|
|
2524
|
+
activeByMode.grok,
|
|
2525
|
+
"grok",
|
|
2526
|
+
ctx.logger,
|
|
2527
|
+
)
|
|
2528
|
+
}
|
|
2443
2529
|
}
|
|
2444
2530
|
}
|
|
2445
2531
|
|
|
@@ -2495,6 +2581,7 @@ export function resetSeenClaudeChatInputIdsForTest() {
|
|
|
2495
2581
|
seenClaudeChatInputIds = makePersistentSeenInputIdRegistry(SEEN_INPUT_IDS_MAX)
|
|
2496
2582
|
}
|
|
2497
2583
|
const seenCodexChatInputIds = makeSeenInputIdRegistry()
|
|
2584
|
+
const seenGrokChatInputIds = makeSeenInputIdRegistry()
|
|
2498
2585
|
|
|
2499
2586
|
/**
|
|
2500
2587
|
* 通常終了(SIGTERM・安全な再起動)の直前に、処理中の claude.input と受理済み ID の保存を待つ。
|
|
@@ -2888,6 +2975,126 @@ export function buildAttachHistoryPromise({
|
|
|
2888
2975
|
}))
|
|
2889
2976
|
}
|
|
2890
2977
|
|
|
2978
|
+
/**
|
|
2979
|
+
* 要求の失敗を、bridge が通知するターンの失敗(phase なし)と区別する印。
|
|
2980
|
+
* attach は接続そのものの失敗。それ以外の要求の失敗はターンを終わらせない(browser はターン終了に使わない)。
|
|
2981
|
+
*/
|
|
2982
|
+
function grokRequestPhase(type) {
|
|
2983
|
+
return type === "grok.attach" ? { phase: "attach" } : { phase: "request", request_type: type }
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
export async function handleGrokMessage(msg, ctx) {
|
|
2987
|
+
const bridge = ctx.grokBridge
|
|
2988
|
+
const stream_id = msg.stream_id
|
|
2989
|
+
const send = (payload) => ctx.client.send({ ...payload, stream_id })
|
|
2990
|
+
if (msg.type === "grok.queue.cancel") {
|
|
2991
|
+
const response = { type: "grok.queue.response", request_id: msg.request_id || null, input_id: msg.id || null }
|
|
2992
|
+
let canceled
|
|
2993
|
+
try {
|
|
2994
|
+
canceled = bridge?.cancelQueued({ stream_id, id: msg.id })
|
|
2995
|
+
} catch (err) {
|
|
2996
|
+
ctx.logger?.warn?.(
|
|
2997
|
+
{ err: err?.message, stream_id, input_id: msg.id },
|
|
2998
|
+
"grok queued input cancel failed",
|
|
2999
|
+
)
|
|
3000
|
+
send({ ...response, error: err?.message || "cancel_failed" })
|
|
3001
|
+
return
|
|
3002
|
+
}
|
|
3003
|
+
send({ ...response, outcome: canceled?.outcome || "stream_missing" })
|
|
3004
|
+
canceled?.flush?.()
|
|
3005
|
+
return
|
|
3006
|
+
}
|
|
3007
|
+
if (msg.type === "grok.sessions.request") {
|
|
3008
|
+
const cwd = msg.cwd || ""
|
|
3009
|
+
const epoch = typeof msg.epoch === "number" ? msg.epoch : undefined
|
|
3010
|
+
const response = { type: "grok.sessions.response", cwd, epoch, sessions: [], session_operations: false }
|
|
3011
|
+
try {
|
|
3012
|
+
if (!bridge) throw new Error("grok_unavailable")
|
|
3013
|
+
const rows = await bridge.listThreads({ cwd, limit: typeof msg.limit === "number" ? msg.limit : undefined })
|
|
3014
|
+
const liveByThread = new Map(bridge.describeLiveSessions(cwd).map((s) => [s.thread_id, s]))
|
|
3015
|
+
response.sessions = rows.filter((row) => typeof row?.sessionId === "string").map((row) => {
|
|
3016
|
+
const updated = Date.parse(row.updatedAt)
|
|
3017
|
+
const live = liveByThread.get(row.sessionId)
|
|
3018
|
+
return { thread_id: row.sessionId, cwd: row.cwd ?? cwd,
|
|
3019
|
+
preview: typeof row.title === "string" ? row.title : "",
|
|
3020
|
+
updated_at_ms: Number.isFinite(updated) ? updated : null,
|
|
3021
|
+
live: !!live, busy: !!live?.busy }
|
|
3022
|
+
})
|
|
3023
|
+
} catch (err) {
|
|
3024
|
+
response.error = err?.message || String(err)
|
|
3025
|
+
}
|
|
3026
|
+
send(response)
|
|
3027
|
+
return
|
|
3028
|
+
}
|
|
3029
|
+
if (!bridge) {
|
|
3030
|
+
// Codex と同じく、input_id 付きの入力には NACK を返す(返さないと browser の再送が止まらない)。
|
|
3031
|
+
const input_id = msg.type === "grok.input" && typeof msg.input_id === "string" && msg.input_id ? msg.input_id : null
|
|
3032
|
+
if (input_id) send({ type: "grok.input.nack", input_id, reason: "stream_missing" })
|
|
3033
|
+
else send({ type: "grok.error", error: "grok_unavailable", ...grokRequestPhase(msg.type) })
|
|
3034
|
+
return
|
|
3035
|
+
}
|
|
3036
|
+
if (msg.type === "grok.input") {
|
|
3037
|
+
const input_id = typeof msg.input_id === "string" && msg.input_id ? msg.input_id : null
|
|
3038
|
+
if (input_id && seenGrokChatInputIds.has(input_id)) {
|
|
3039
|
+
send({ type: "grok.input.ack", input_id })
|
|
3040
|
+
return
|
|
3041
|
+
}
|
|
3042
|
+
try {
|
|
3043
|
+
const rejection = inputLifecycleRejection(ctx)
|
|
3044
|
+
if (rejection) throw new Error(rejection.reason)
|
|
3045
|
+
// 受付は同期で完了するため、ターン処理中の再送も上の受理済み ID で除外する。
|
|
3046
|
+
// ターンの失敗は bridge が監視し、grok_error で通知する。
|
|
3047
|
+
bridge.input({ stream_id, message: msg.message, inputId: input_id || undefined,
|
|
3048
|
+
model: msg.model, effort: msg.effort })
|
|
3049
|
+
if (input_id) {
|
|
3050
|
+
seenGrokChatInputIds.remember(input_id)
|
|
3051
|
+
send({ type: "grok.input.ack", input_id })
|
|
3052
|
+
}
|
|
3053
|
+
} catch (err) {
|
|
3054
|
+
if (input_id) send({ type: "grok.input.nack", input_id, reason: err?.message || String(err) })
|
|
3055
|
+
else send({ type: "grok.error", error: err?.message || String(err), ...grokRequestPhase(msg.type) })
|
|
3056
|
+
}
|
|
3057
|
+
return
|
|
3058
|
+
}
|
|
3059
|
+
if (msg.type === "grok.interrupt") {
|
|
3060
|
+
try {
|
|
3061
|
+
await bridge.interrupt({ stream_id })
|
|
3062
|
+
send({ type: "grok.interrupt.response", ok: true })
|
|
3063
|
+
} catch (err) {
|
|
3064
|
+
send({ type: "grok.interrupt.response", ok: false, error: err?.message || String(err) })
|
|
3065
|
+
}
|
|
3066
|
+
return
|
|
3067
|
+
}
|
|
3068
|
+
try {
|
|
3069
|
+
switch (msg.type) {
|
|
3070
|
+
case "grok.attach": {
|
|
3071
|
+
// claude_cwd → HOME の補完。scheduler のレーン分類と同じ defaultLaneCwd を使う。
|
|
3072
|
+
const cwd = msg.cwd || defaultLaneCwd(ctx.config, "grok")
|
|
3073
|
+
const info = await bridge.attach({ stream_id, cwd, model: msg.model, effort: msg.effort,
|
|
3074
|
+
resumeSessionId: msg.resume_session_id, attachId: msg.attach_id })
|
|
3075
|
+
send({ type: "grok.ready", thread_id: info.sessionId, resuming: info.resuming,
|
|
3076
|
+
warm: info.warm, history: info.history, history_truncated: info.historyTruncated,
|
|
3077
|
+
last_seq: info.lastSeq, turn_active: info.turnActive,
|
|
3078
|
+
queue_state: info.queueState, supports_queue_cancel_response: true })
|
|
3079
|
+
break
|
|
3080
|
+
}
|
|
3081
|
+
case "grok.permission.reply":
|
|
3082
|
+
await bridge.permissionReply({ stream_id, request_id: msg.request_id, result: msg.result })
|
|
3083
|
+
break
|
|
3084
|
+
case "grok.detach":
|
|
3085
|
+
await bridge.detach({ stream_id })
|
|
3086
|
+
break
|
|
3087
|
+
case "grok.keepalive":
|
|
3088
|
+
await bridge.keepalive({ stream_id })
|
|
3089
|
+
break
|
|
3090
|
+
}
|
|
3091
|
+
} catch (err) {
|
|
3092
|
+
// phase: "attach" は接続そのものの失敗(backend はこの stream の登録を外す)。それ以外の要求の
|
|
3093
|
+
// 失敗とターン失敗(bridge の grok_error)は会話が続くので、登録を外させない。
|
|
3094
|
+
send({ type: "grok.error", error: err?.message || String(err), ...grokRequestPhase(msg.type) })
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
|
|
2891
3098
|
/**
|
|
2892
3099
|
* SDK チャット配達保証 (S2, 2026-07-05): `codex.input` を処理し、browser が付与した
|
|
2893
3100
|
* `input_id` があれば `codex.input.ack` / `codex.input.nack` を返す (claude.input と
|
|
@@ -3437,9 +3644,11 @@ export async function stopChatSessionsUnder(dir, ctx, reason = "worktree-remove"
|
|
|
3437
3644
|
const live = describeLiveSessionsUnion({
|
|
3438
3645
|
claudeBridge: ctx.claudeBridge,
|
|
3439
3646
|
codexBridge: ctx.codexBridge,
|
|
3647
|
+
grokBridge: ctx.grokBridge,
|
|
3440
3648
|
})
|
|
3441
3649
|
const claudeCwds = new Set()
|
|
3442
3650
|
const codexStreams = new Set()
|
|
3651
|
+
const grokStreams = new Set()
|
|
3443
3652
|
let stopped = 0
|
|
3444
3653
|
// ⚠️ 例外を捕まえない。投げれば removeWorktree がその場で中断し、dir は残る (= 削除しない)。
|
|
3445
3654
|
// 「失敗した分を集めて最後にまとめて投げる」形も書けるが、削除しないのであれば残りを止める
|
|
@@ -3460,6 +3669,10 @@ export async function stopChatSessionsUnder(dir, ctx, reason = "worktree-remove"
|
|
|
3460
3669
|
stream_id: session.stream_id,
|
|
3461
3670
|
reason,
|
|
3462
3671
|
})
|
|
3672
|
+
} else if (session.kind === "grok") {
|
|
3673
|
+
if (grokStreams.has(session.stream_id)) continue
|
|
3674
|
+
grokStreams.add(session.stream_id)
|
|
3675
|
+
stopped += await ctx.grokBridge.stopSessionForStream({ stream_id: session.stream_id, reason })
|
|
3463
3676
|
}
|
|
3464
3677
|
}
|
|
3465
3678
|
return stopped
|
|
@@ -4014,6 +4227,16 @@ export async function dispatch(msg, ctx) {
|
|
|
4014
4227
|
// async (claude 版は SDK 呼び出しが同期/fire-and-forget)。失敗時は codex.error で
|
|
4015
4228
|
// browser に伝える。permission.reply の result は method ごとに shape が異なる
|
|
4016
4229
|
// (accept/decline 等) ため、正規化せず browser が組み立てたものをそのまま渡す。
|
|
4230
|
+
case "grok.attach":
|
|
4231
|
+
case "grok.input":
|
|
4232
|
+
case "grok.queue.cancel":
|
|
4233
|
+
case "grok.interrupt":
|
|
4234
|
+
case "grok.permission.reply":
|
|
4235
|
+
case "grok.detach":
|
|
4236
|
+
case "grok.keepalive":
|
|
4237
|
+
case "grok.sessions.request":
|
|
4238
|
+
await handleGrokMessage(msg, ctx)
|
|
4239
|
+
return
|
|
4017
4240
|
case "codex.attach": {
|
|
4018
4241
|
const stream_id = msg.stream_id
|
|
4019
4242
|
if (!ctx.codexBridge || !codexAvailableForDispatch(ctx)) {
|
|
@@ -4094,55 +4317,57 @@ export async function dispatch(msg, ctx) {
|
|
|
4094
4317
|
}
|
|
4095
4318
|
return
|
|
4096
4319
|
}
|
|
4320
|
+
case "grok.upload":
|
|
4097
4321
|
case "codex.upload": {
|
|
4098
|
-
//
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
// .cockpit-uploads/<日付>/ に保存し、保存パスを browser へ返す。browser は
|
|
4102
|
-
// そのパスを codex.input 本文に載せ、Codex がシェル/ファイルツールで読む
|
|
4103
|
-
// (Claude 版と同じ「パス参照方式」、Codex 側の model/effort 分離と同様に
|
|
4104
|
-
// アカウント/プロトコルが違うだけでファイル保存の仕組みは共有する)。
|
|
4105
|
-
if (!ctx.uploadManager) {
|
|
4106
|
-
ctx.client.send({
|
|
4107
|
-
type: "codex.upload.done",
|
|
4108
|
-
stream_id: msg.stream_id,
|
|
4109
|
-
upload_id: msg.upload_id,
|
|
4110
|
-
ok: false,
|
|
4111
|
-
error: "upload_unavailable",
|
|
4112
|
-
})
|
|
4113
|
-
return
|
|
4114
|
-
}
|
|
4115
|
-
try {
|
|
4116
|
-
const res = await ctx.uploadManager.addChunk({
|
|
4117
|
-
upload_id: msg.upload_id,
|
|
4118
|
-
filename: msg.filename,
|
|
4119
|
-
media_type: msg.media_type,
|
|
4120
|
-
total_size: msg.total_size,
|
|
4121
|
-
total_chunks: msg.total_chunks,
|
|
4122
|
-
chunk_index: msg.chunk_index,
|
|
4123
|
-
data: msg.data,
|
|
4124
|
-
cwd: msg.cwd || ctx.config?.claude_cwd || undefined,
|
|
4125
|
-
})
|
|
4126
|
-
if (res.done) {
|
|
4322
|
+
// パス参照方式の添付受信を共有し、返信型と既定 cwd だけを切り替える。
|
|
4323
|
+
async function handleUpload(doneType, getDefaultCwd) {
|
|
4324
|
+
if (!ctx.uploadManager) {
|
|
4127
4325
|
ctx.client.send({
|
|
4128
|
-
type:
|
|
4326
|
+
type: doneType,
|
|
4129
4327
|
stream_id: msg.stream_id,
|
|
4130
4328
|
upload_id: msg.upload_id,
|
|
4131
|
-
ok:
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4329
|
+
ok: false,
|
|
4330
|
+
error: "upload_unavailable",
|
|
4331
|
+
})
|
|
4332
|
+
return
|
|
4333
|
+
}
|
|
4334
|
+
try {
|
|
4335
|
+
const res = await ctx.uploadManager.addChunk({
|
|
4336
|
+
upload_id: msg.upload_id,
|
|
4337
|
+
filename: msg.filename,
|
|
4338
|
+
media_type: msg.media_type,
|
|
4339
|
+
total_size: msg.total_size,
|
|
4340
|
+
total_chunks: msg.total_chunks,
|
|
4341
|
+
chunk_index: msg.chunk_index,
|
|
4342
|
+
data: msg.data,
|
|
4343
|
+
cwd: msg.cwd || getDefaultCwd(),
|
|
4344
|
+
})
|
|
4345
|
+
if (res.done) {
|
|
4346
|
+
ctx.client.send({
|
|
4347
|
+
type: doneType,
|
|
4348
|
+
stream_id: msg.stream_id,
|
|
4349
|
+
upload_id: msg.upload_id,
|
|
4350
|
+
ok: true,
|
|
4351
|
+
path: res.path,
|
|
4352
|
+
filename: res.filename,
|
|
4353
|
+
media_type: res.media_type,
|
|
4354
|
+
size: res.size,
|
|
4355
|
+
})
|
|
4356
|
+
}
|
|
4357
|
+
} catch (err) {
|
|
4358
|
+
ctx.client.send({
|
|
4359
|
+
type: doneType,
|
|
4360
|
+
stream_id: msg.stream_id,
|
|
4361
|
+
upload_id: msg.upload_id,
|
|
4362
|
+
ok: false,
|
|
4363
|
+
error: err?.message || String(err),
|
|
4136
4364
|
})
|
|
4137
4365
|
}
|
|
4138
|
-
}
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
ok: false,
|
|
4144
|
-
error: err?.message || String(err),
|
|
4145
|
-
})
|
|
4366
|
+
}
|
|
4367
|
+
if (msg.type === "grok.upload") {
|
|
4368
|
+
await handleUpload("grok.upload.done", () => defaultLaneCwd(ctx.config, "grok"))
|
|
4369
|
+
} else {
|
|
4370
|
+
await handleUpload("codex.upload.done", () => ctx.config?.claude_cwd || undefined)
|
|
4146
4371
|
}
|
|
4147
4372
|
return
|
|
4148
4373
|
}
|
|
@@ -4779,11 +5004,13 @@ export async function dispatch(msg, ctx) {
|
|
|
4779
5004
|
try {
|
|
4780
5005
|
if (msg.scope === "cwd") {
|
|
4781
5006
|
const cwd = typeof msg.cwd === "string" ? msg.cwd : ""
|
|
4782
|
-
if (msg.cli_kind !== "claude" && msg.cli_kind !== "codex") result.error = "unsupported cli_kind"
|
|
5007
|
+
if (msg.cli_kind !== "claude" && msg.cli_kind !== "codex" && msg.cli_kind !== "grok") result.error = "unsupported cli_kind"
|
|
4783
5008
|
else if (!cwd || !path.isAbsolute(cwd)) result.error = "cwd required"
|
|
4784
5009
|
else if (msg.cli_kind === "claude") {
|
|
4785
5010
|
noteRestoreStop(ctx.sessionRestoreState, "claude", cwd)
|
|
4786
5011
|
result.stopped = await ctx.claudeBridge.stopSessionsFor({ cwd, reason: "chat-stop" })
|
|
5012
|
+
} else if (msg.cli_kind === "grok") {
|
|
5013
|
+
result.stopped = await ctx.grokBridge?.stopSessionsForCwd({ cwd, reason: "chat-stop" }) ?? 0
|
|
4787
5014
|
} else {
|
|
4788
5015
|
noteRestoreStop(ctx.sessionRestoreState, "codex", cwd)
|
|
4789
5016
|
result.stopped = await ctx.codexBridge.stopSessionsForCwd({ cwd, reason: "chat-stop" })
|
|
@@ -4801,6 +5028,9 @@ export async function dispatch(msg, ctx) {
|
|
|
4801
5028
|
noteRestoreStop(ctx.sessionRestoreState, "claude", cwd)
|
|
4802
5029
|
result.stopped = await ctx.claudeBridge.stopSessionForDeletion({ cwd, session_id, reason: "chat-stop" })
|
|
4803
5030
|
}
|
|
5031
|
+
} else if (msg.cli_kind === "grok") {
|
|
5032
|
+
if (typeof msg.stream_id !== "string" || !msg.stream_id) result.error = "stream_id required"
|
|
5033
|
+
else result.stopped = await ctx.grokBridge?.stopSessionForStream({ stream_id: msg.stream_id, reason: "chat-stop" }) ?? 0
|
|
4804
5034
|
} else if (msg.cli_kind === "codex") {
|
|
4805
5035
|
if (typeof msg.stream_id !== "string" || !msg.stream_id) result.error = "stream_id required"
|
|
4806
5036
|
else {
|
|
@@ -4822,7 +5052,7 @@ export async function dispatch(msg, ctx) {
|
|
|
4822
5052
|
case "chat.sessions.request": {
|
|
4823
5053
|
try {
|
|
4824
5054
|
const snapshot = await buildChatSessionsSnapshot({
|
|
4825
|
-
bridges: { claudeBridge: ctx.claudeBridge, codexBridge: ctx.codexBridge },
|
|
5055
|
+
bridges: { claudeBridge: ctx.claudeBridge, codexBridge: ctx.codexBridge, grokBridge: ctx.grokBridge },
|
|
4826
5056
|
logger: ctx.logger,
|
|
4827
5057
|
})
|
|
4828
5058
|
ctx.client.send({ type: "chat.sessions", request_id: msg.request_id, ...withSortAffectingAt(refreshChatSessionsSignals(snapshot, ctx.logger)) })
|
|
@@ -17,7 +17,7 @@ export function validReviewEvent(event) {
|
|
|
17
17
|
if (event.type !== "system" || event.subtype !== "cross_review" || event.schema_version !== 1) return false
|
|
18
18
|
if (typeof event.review_id !== "string" || !/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(event.review_id)) return false
|
|
19
19
|
if (!["started", "completed", "failed", "stopped"].includes(event.phase)) return false
|
|
20
|
-
if (!["claude", "codex"].includes(event.reviewer)) return false
|
|
20
|
+
if (!["claude", "codex", "grok"].includes(event.reviewer)) return false
|
|
21
21
|
if (!Number.isSafeInteger(event.round) || event.round <= 0) return false
|
|
22
22
|
if (typeof event.workdir !== "string" || !path.isAbsolute(event.workdir)) return false
|
|
23
23
|
if (typeof event.timestamp !== "string" || !/^\d{4}-\d\d-\d\dT/.test(event.timestamp) || !Number.isFinite(Date.parse(event.timestamp))) return false
|