@cocorograph/hub-agent 0.8.113 → 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/claude-stream-bridge.mjs +7 -0
- package/src/codex-binary.mjs +43 -27
- package/src/config.mjs +9 -4
- package/src/dispatch-scheduler.mjs +13 -11
- package/src/enroll.mjs +5 -0
- 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 +295 -57
- 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"
|
|
@@ -166,6 +168,7 @@ const BUNDLE_MANIFEST_PATH =
|
|
|
166
168
|
export function resolveClaudeAttachOptions(config, msg = {}) {
|
|
167
169
|
return {
|
|
168
170
|
model: msg.model || config?.claude_model || null,
|
|
171
|
+
chromeEnabled: config?.claude_chrome_enabled === true,
|
|
169
172
|
permissionMode:
|
|
170
173
|
msg.permission_mode ||
|
|
171
174
|
config?.claude_permission_mode ||
|
|
@@ -279,7 +282,7 @@ export function withDeadline(promise, timeoutMs, onTimeout) {
|
|
|
279
282
|
* WS 受信順 = pane 反映順を守るため false (= 直列キューに残す)。
|
|
280
283
|
*/
|
|
281
284
|
export function isFastPathMessage(type) {
|
|
282
|
-
return type === "claude.interrupt" || type === "codex.interrupt"
|
|
285
|
+
return type === "claude.interrupt" || type === "codex.interrupt" || type === "grok.interrupt"
|
|
283
286
|
}
|
|
284
287
|
|
|
285
288
|
/**
|
|
@@ -315,7 +318,9 @@ export function isFastPathMessage(type) {
|
|
|
315
318
|
*/
|
|
316
319
|
export function defaultLaneCwd(config, mode) {
|
|
317
320
|
const configured = expandTilde(config?.claude_cwd || "") || ""
|
|
318
|
-
|
|
321
|
+
// Grok は handler(handleGrokMessage の attach)もこの関数で補完する。scheduler と同じ値を
|
|
322
|
+
// 使わないと、cwd 省略の attach と HOME 指定の停止が別レーンへ散る。Codex の契約は変えない。
|
|
323
|
+
if (configured || (mode !== "claude" && mode !== "grok")) return configured
|
|
319
324
|
return process.env.HOME || process.cwd()
|
|
320
325
|
}
|
|
321
326
|
|
|
@@ -324,6 +329,7 @@ export function isReadOnlyRpc(type) {
|
|
|
324
329
|
type === "claude.history.request" ||
|
|
325
330
|
type === "claude.sessions.request" ||
|
|
326
331
|
type === "codex.sessions.request" ||
|
|
332
|
+
type === "grok.sessions.request" ||
|
|
327
333
|
type === "codex.history.request" ||
|
|
328
334
|
type === "profile.list" ||
|
|
329
335
|
type === "codex.profile.list" ||
|
|
@@ -579,7 +585,7 @@ export function _resetBundleSyncStateForTest() {
|
|
|
579
585
|
/**
|
|
580
586
|
* ある target(cli_kind)について、要求 version が既に反映済みかを判定する。
|
|
581
587
|
* - codex: mcp_approval_applied_version(無ければ installed_version)が要求以上
|
|
582
|
-
* - claude: installed_version が要求以上
|
|
588
|
+
* - claude: installed_version と、Grok 導入済みなら grok_mcp_applied_version が要求以上
|
|
583
589
|
* auto_update:false のときは「対象外(=要求は満たされたものとして扱う=同期しない)」を返す。
|
|
584
590
|
*/
|
|
585
591
|
async function _bundleTargetSatisfied(cliKind, requestedVersion, readStateImpl, readConfigImpl) {
|
|
@@ -588,6 +594,10 @@ async function _bundleTargetSatisfied(cliKind, requestedVersion, readStateImpl,
|
|
|
588
594
|
if (!requestedVersion) return false
|
|
589
595
|
const filesVer = st.installed_version || "0.0.0"
|
|
590
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
|
+
}
|
|
591
601
|
if (cliKind === "codex") {
|
|
592
602
|
const approvalVer = st.mcp_approval_applied_version || "0.0.0"
|
|
593
603
|
const approved = compareBundleVersions(approvalVer, requestedVersion) >= 0
|
|
@@ -707,6 +717,7 @@ export class DeferredRestartCoordinator {
|
|
|
707
717
|
constructor({
|
|
708
718
|
claudeBridge,
|
|
709
719
|
codexBridge,
|
|
720
|
+
grokBridge,
|
|
710
721
|
shutdown,
|
|
711
722
|
logger,
|
|
712
723
|
settleMs = 500,
|
|
@@ -718,6 +729,7 @@ export class DeferredRestartCoordinator {
|
|
|
718
729
|
} = {}) {
|
|
719
730
|
this.claudeBridge = claudeBridge
|
|
720
731
|
this.codexBridge = codexBridge
|
|
732
|
+
this.grokBridge = grokBridge
|
|
721
733
|
this.shutdown = shutdown
|
|
722
734
|
this.logger = logger
|
|
723
735
|
this.settleMs = settleMs
|
|
@@ -745,6 +757,7 @@ export class DeferredRestartCoordinator {
|
|
|
745
757
|
return !!(
|
|
746
758
|
this.claudeBridge?.hasActiveWork?.() ||
|
|
747
759
|
this.codexBridge?.hasActiveWork?.() ||
|
|
760
|
+
this.grokBridge?.hasActiveWork?.() ||
|
|
748
761
|
this.workProviders.some((provider) => provider?.hasActiveWork?.())
|
|
749
762
|
)
|
|
750
763
|
}
|
|
@@ -809,7 +822,45 @@ export class DeferredRestartCoordinator {
|
|
|
809
822
|
* `restore:` 購読は browser に存在しないため、ここで捨てないと Hub へ幽霊 stream が流れる。
|
|
810
823
|
* 起動処理から切り出してあるのは、この配線そのものをテストから通すため。
|
|
811
824
|
*/
|
|
812
|
-
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
|
+
}
|
|
813
864
|
// stream-json モード (Sprint G): Claude Code を SDK 経由で起動し、stream イベントを
|
|
814
865
|
// そのまま browser に転送する。SDK 未インストール時は claudeBridge=null で全 attach が
|
|
815
866
|
// claude.error を返す経路に分岐 (dispatch 側で判定)。
|
|
@@ -961,6 +1012,21 @@ export async function applyStartupEnv({ logger, deps = {} }) {
|
|
|
961
1012
|
}
|
|
962
1013
|
}
|
|
963
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
|
+
|
|
964
1030
|
export async function startDaemon({ version, claudeSdk } = {}) {
|
|
965
1031
|
const logRotationTimer = startLogRotation(logger, { isDeferred: hasInheritedOutputChild })
|
|
966
1032
|
startEventLoopMetrics(logger)
|
|
@@ -1022,6 +1088,10 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1022
1088
|
// codex app-server ベースの chat ブリッジ。resolver は各 spawn 時に再評価するため、
|
|
1023
1089
|
// daemon 起動時に一時的に unavailable でも bridge 自体は保持する。
|
|
1024
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
|
|
1025
1095
|
|
|
1026
1096
|
// Codex の MCP 状態 control (agent-global, セッション非依存)。MCP は ~/.codex/config.toml
|
|
1027
1097
|
// = アカウント単位の設定で全セッション共有のため、claudeBridge の「MCP control 専用
|
|
@@ -1094,7 +1164,7 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1094
1164
|
logger.warn({ err: err.message }, "ws client error (will retry via close handler)")
|
|
1095
1165
|
})
|
|
1096
1166
|
|
|
1097
|
-
wireBridgeEvents(client, { claudeBridge, codexBridge })
|
|
1167
|
+
wireBridgeEvents(client, { claudeBridge, codexBridge, grokBridge, logger })
|
|
1098
1168
|
|
|
1099
1169
|
// Hub からのメッセージ dispatch は、接続世代 + 機能レーン単位で直列実行する。
|
|
1100
1170
|
//
|
|
@@ -1115,6 +1185,7 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1115
1185
|
client,
|
|
1116
1186
|
claudeBridge,
|
|
1117
1187
|
codexBridge,
|
|
1188
|
+
grokBridge,
|
|
1118
1189
|
codexMcpControl,
|
|
1119
1190
|
uploadManager,
|
|
1120
1191
|
}
|
|
@@ -1147,7 +1218,7 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1147
1218
|
}, 60_000)
|
|
1148
1219
|
|
|
1149
1220
|
// 会話の回収と常駐数制限を定期実行する。
|
|
1150
|
-
const stateLoop = startStateLoop({ client, logger, intervalMs: 5_000, claudeBridge, codexBridge })
|
|
1221
|
+
const stateLoop = startStateLoop({ client, logger, intervalMs: 5_000, claudeBridge, codexBridge, grokBridge })
|
|
1151
1222
|
// bundle hook (cockpit_session_event_hook.py) が /tmp/cockpit_session_events/<name>.json
|
|
1152
1223
|
// に書き出す UserPromptSubmit / Stop / ready の event を fs.watch で拾って WS push する。
|
|
1153
1224
|
// text マーカー判定 (detectStatusFromText) より精度が高い「ターン境界」の signal。
|
|
@@ -1163,9 +1234,10 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1163
1234
|
const siblingInjectBridge = new SiblingInjectBridge({
|
|
1164
1235
|
claudeBridge,
|
|
1165
1236
|
codexBridge,
|
|
1237
|
+
grokBridge,
|
|
1166
1238
|
logger,
|
|
1167
1239
|
starter: ({ cwd, cliKind }) => ensureChatSession({
|
|
1168
|
-
cwd, cliKind, bridges: { claudeBridge, codexBridge }, logger,
|
|
1240
|
+
cwd, cliKind, bridges: { claudeBridge, codexBridge, grokBridge }, logger,
|
|
1169
1241
|
resolveClaudeOptions: () => resolveClaudeAttachOptions(ctx.config),
|
|
1170
1242
|
}),
|
|
1171
1243
|
onRestartRequested: () => restartCoordinator.request(),
|
|
@@ -1208,6 +1280,7 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1208
1280
|
restartCoordinator?.stop?.()
|
|
1209
1281
|
claudeBridge?.shutdown?.()
|
|
1210
1282
|
codexBridge?.shutdown?.()
|
|
1283
|
+
await shutdownGrokBridge(grokBridge, logger)
|
|
1211
1284
|
// 受理済み input_id の保存を待ってから exit する(再起動後の再送で二重実行しないため)。
|
|
1212
1285
|
const persistence = await waitForClaudeInputPersistence()
|
|
1213
1286
|
if (persistence === "timeout") {
|
|
@@ -1222,6 +1295,7 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1222
1295
|
restartCoordinator = new DeferredRestartCoordinator({
|
|
1223
1296
|
claudeBridge,
|
|
1224
1297
|
codexBridge,
|
|
1298
|
+
grokBridge,
|
|
1225
1299
|
shutdown,
|
|
1226
1300
|
logger,
|
|
1227
1301
|
lifecycle,
|
|
@@ -1250,7 +1324,7 @@ export async function startDaemon({ version, claudeSdk } = {}) {
|
|
|
1250
1324
|
)
|
|
1251
1325
|
})
|
|
1252
1326
|
|
|
1253
|
-
return { client, plugins, claudeBridge, codexBridge, codexMcpControl, uploadManager }
|
|
1327
|
+
return { client, plugins, claudeBridge, codexBridge, grokBridge, codexMcpControl, uploadManager }
|
|
1254
1328
|
}
|
|
1255
1329
|
|
|
1256
1330
|
const SESSION_EVENTS_DIR =
|
|
@@ -2159,7 +2233,7 @@ export function decideTurnActiveSource({
|
|
|
2159
2233
|
return "none"
|
|
2160
2234
|
}
|
|
2161
2235
|
|
|
2162
|
-
export function startStateLoop({ client, logger, intervalMs, claudeBridge, codexBridge }) {
|
|
2236
|
+
export function startStateLoop({ client, logger, intervalMs, claudeBridge, codexBridge, grokBridge }) {
|
|
2163
2237
|
// 総 live 上限 M のログ抑制状態。⚠️ **この state loop インスタンスが所有する**(module-global に
|
|
2164
2238
|
// 置くと、複数インスタンスや並行テストが互いのログを抑制してしまう)。「超過しているが全部
|
|
2165
2239
|
// 保護されていて刈れない」tick は 5 秒ごとに同じ行を出し続けるので、状態が変わったときと
|
|
@@ -2202,12 +2276,17 @@ export function startStateLoop({ client, logger, intervalMs, claudeBridge, codex
|
|
|
2202
2276
|
} catch (err) {
|
|
2203
2277
|
logger?.warn({ err: err?.message }, "gcAbandonedSessions failed")
|
|
2204
2278
|
}
|
|
2279
|
+
try {
|
|
2280
|
+
grokBridge?.gcAbandonedSessions?.()
|
|
2281
|
+
} catch (err) {
|
|
2282
|
+
logger?.warn?.({ err: err?.message }, "grok gcAbandonedSessions failed")
|
|
2283
|
+
}
|
|
2205
2284
|
// 総 live 上限 M(claude + codex 合算、既定 20)。per-cwd を 1 → K(既定 3)へ緩めたぶん、
|
|
2206
2285
|
// cwd 単位の境界だけでは総量が膨らむため、cwd をまたいだ天井をここで適用する。
|
|
2207
2286
|
// ⚠️ 両 CLI の reconcile が **終わった後**に呼ぶ(cwd 単位で畳んだ結果の生存数で判定する)。
|
|
2208
2287
|
try {
|
|
2209
2288
|
enforceTotalLiveBound({
|
|
2210
|
-
bridges: [claudeBridge, codexBridge].filter(Boolean),
|
|
2289
|
+
bridges: [claudeBridge, codexBridge, grokBridge].filter(Boolean),
|
|
2211
2290
|
logger,
|
|
2212
2291
|
logState: warmBoundsLogState,
|
|
2213
2292
|
})
|
|
@@ -2324,13 +2403,17 @@ export function startStateLoop({ client, logger, intervalMs, claudeBridge, codex
|
|
|
2324
2403
|
*/
|
|
2325
2404
|
export async function maybeSyncClaudeSettings(msg, ctx) {
|
|
2326
2405
|
const hasClaudeFields =
|
|
2327
|
-
typeof msg?.claude_model === "string" || typeof msg?.claude_permission_mode === "string"
|
|
2406
|
+
typeof msg?.claude_model === "string" || typeof msg?.claude_permission_mode === "string" ||
|
|
2407
|
+
typeof msg?.claude_chrome_enabled === "boolean"
|
|
2328
2408
|
const hasCodexFields =
|
|
2329
2409
|
typeof msg?.codex_model === "string" || typeof msg?.codex_effort === "string"
|
|
2330
2410
|
if (!hasClaudeFields && !hasCodexFields) return
|
|
2331
2411
|
|
|
2332
2412
|
const nextModel = typeof msg.claude_model === "string" ? msg.claude_model : ctx.config.claude_model || ""
|
|
2333
2413
|
const nextMode = typeof msg.claude_permission_mode === "string" ? msg.claude_permission_mode : ctx.config.claude_permission_mode || ""
|
|
2414
|
+
const nextChromeEnabled = typeof msg.claude_chrome_enabled === "boolean"
|
|
2415
|
+
? msg.claude_chrome_enabled
|
|
2416
|
+
: ctx.config.claude_chrome_enabled
|
|
2334
2417
|
const nextCodexModel = typeof msg.codex_model === "string" ? msg.codex_model : ctx.config.codex_model || ""
|
|
2335
2418
|
const nextCodexEffort = typeof msg.codex_effort === "string" ? msg.codex_effort : ctx.config.codex_effort || ""
|
|
2336
2419
|
const prevModel = ctx.config.claude_model || ""
|
|
@@ -2340,6 +2423,7 @@ export async function maybeSyncClaudeSettings(msg, ctx) {
|
|
|
2340
2423
|
if (
|
|
2341
2424
|
nextModel === prevModel &&
|
|
2342
2425
|
nextMode === prevMode &&
|
|
2426
|
+
nextChromeEnabled === ctx.config.claude_chrome_enabled &&
|
|
2343
2427
|
nextCodexModel === prevCodexModel &&
|
|
2344
2428
|
nextCodexEffort === prevCodexEffort
|
|
2345
2429
|
) {
|
|
@@ -2347,6 +2431,7 @@ export async function maybeSyncClaudeSettings(msg, ctx) {
|
|
|
2347
2431
|
}
|
|
2348
2432
|
ctx.config.claude_model = nextModel
|
|
2349
2433
|
ctx.config.claude_permission_mode = nextMode
|
|
2434
|
+
if (typeof nextChromeEnabled === "boolean") ctx.config.claude_chrome_enabled = nextChromeEnabled
|
|
2350
2435
|
ctx.config.codex_model = nextCodexModel
|
|
2351
2436
|
ctx.config.codex_effort = nextCodexEffort
|
|
2352
2437
|
try {
|
|
@@ -2356,6 +2441,7 @@ export async function maybeSyncClaudeSettings(msg, ctx) {
|
|
|
2356
2441
|
hub_url: ctx.config.hub_url,
|
|
2357
2442
|
claude_model: nextModel,
|
|
2358
2443
|
claude_permission_mode: nextMode,
|
|
2444
|
+
claude_chrome_enabled: nextChromeEnabled,
|
|
2359
2445
|
codex_model: nextCodexModel,
|
|
2360
2446
|
codex_effort: nextCodexEffort,
|
|
2361
2447
|
})
|
|
@@ -2394,7 +2480,7 @@ export function handleStreamsSyncResponse(msg, ctx) {
|
|
|
2394
2480
|
const activeIds = new Set(
|
|
2395
2481
|
Array.isArray(msg?.active_stream_ids) ? msg.active_stream_ids : [],
|
|
2396
2482
|
)
|
|
2397
|
-
const activeByMode = { claude: new Set(), codex: new Set() }
|
|
2483
|
+
const activeByMode = { claude: new Set(), codex: new Set(), grok: new Set() }
|
|
2398
2484
|
const classifiedIds = new Set()
|
|
2399
2485
|
if (Array.isArray(msg?.active_streams)) {
|
|
2400
2486
|
for (const item of msg.active_streams) {
|
|
@@ -2432,6 +2518,14 @@ export function handleStreamsSyncResponse(msg, ctx) {
|
|
|
2432
2518
|
ctx.logger,
|
|
2433
2519
|
)
|
|
2434
2520
|
}
|
|
2521
|
+
if (activeByMode.grok.size > 0) {
|
|
2522
|
+
pruneInactiveChatSubscribers(
|
|
2523
|
+
ctx.grokBridge,
|
|
2524
|
+
activeByMode.grok,
|
|
2525
|
+
"grok",
|
|
2526
|
+
ctx.logger,
|
|
2527
|
+
)
|
|
2528
|
+
}
|
|
2435
2529
|
}
|
|
2436
2530
|
}
|
|
2437
2531
|
|
|
@@ -2487,6 +2581,7 @@ export function resetSeenClaudeChatInputIdsForTest() {
|
|
|
2487
2581
|
seenClaudeChatInputIds = makePersistentSeenInputIdRegistry(SEEN_INPUT_IDS_MAX)
|
|
2488
2582
|
}
|
|
2489
2583
|
const seenCodexChatInputIds = makeSeenInputIdRegistry()
|
|
2584
|
+
const seenGrokChatInputIds = makeSeenInputIdRegistry()
|
|
2490
2585
|
|
|
2491
2586
|
/**
|
|
2492
2587
|
* 通常終了(SIGTERM・安全な再起動)の直前に、処理中の claude.input と受理済み ID の保存を待つ。
|
|
@@ -2880,6 +2975,126 @@ export function buildAttachHistoryPromise({
|
|
|
2880
2975
|
}))
|
|
2881
2976
|
}
|
|
2882
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
|
+
|
|
2883
3098
|
/**
|
|
2884
3099
|
* SDK チャット配達保証 (S2, 2026-07-05): `codex.input` を処理し、browser が付与した
|
|
2885
3100
|
* `input_id` があれば `codex.input.ack` / `codex.input.nack` を返す (claude.input と
|
|
@@ -3429,9 +3644,11 @@ export async function stopChatSessionsUnder(dir, ctx, reason = "worktree-remove"
|
|
|
3429
3644
|
const live = describeLiveSessionsUnion({
|
|
3430
3645
|
claudeBridge: ctx.claudeBridge,
|
|
3431
3646
|
codexBridge: ctx.codexBridge,
|
|
3647
|
+
grokBridge: ctx.grokBridge,
|
|
3432
3648
|
})
|
|
3433
3649
|
const claudeCwds = new Set()
|
|
3434
3650
|
const codexStreams = new Set()
|
|
3651
|
+
const grokStreams = new Set()
|
|
3435
3652
|
let stopped = 0
|
|
3436
3653
|
// ⚠️ 例外を捕まえない。投げれば removeWorktree がその場で中断し、dir は残る (= 削除しない)。
|
|
3437
3654
|
// 「失敗した分を集めて最後にまとめて投げる」形も書けるが、削除しないのであれば残りを止める
|
|
@@ -3452,6 +3669,10 @@ export async function stopChatSessionsUnder(dir, ctx, reason = "worktree-remove"
|
|
|
3452
3669
|
stream_id: session.stream_id,
|
|
3453
3670
|
reason,
|
|
3454
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 })
|
|
3455
3676
|
}
|
|
3456
3677
|
}
|
|
3457
3678
|
return stopped
|
|
@@ -4006,6 +4227,16 @@ export async function dispatch(msg, ctx) {
|
|
|
4006
4227
|
// async (claude 版は SDK 呼び出しが同期/fire-and-forget)。失敗時は codex.error で
|
|
4007
4228
|
// browser に伝える。permission.reply の result は method ごとに shape が異なる
|
|
4008
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
|
|
4009
4240
|
case "codex.attach": {
|
|
4010
4241
|
const stream_id = msg.stream_id
|
|
4011
4242
|
if (!ctx.codexBridge || !codexAvailableForDispatch(ctx)) {
|
|
@@ -4086,55 +4317,57 @@ export async function dispatch(msg, ctx) {
|
|
|
4086
4317
|
}
|
|
4087
4318
|
return
|
|
4088
4319
|
}
|
|
4320
|
+
case "grok.upload":
|
|
4089
4321
|
case "codex.upload": {
|
|
4090
|
-
//
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
// .cockpit-uploads/<日付>/ に保存し、保存パスを browser へ返す。browser は
|
|
4094
|
-
// そのパスを codex.input 本文に載せ、Codex がシェル/ファイルツールで読む
|
|
4095
|
-
// (Claude 版と同じ「パス参照方式」、Codex 側の model/effort 分離と同様に
|
|
4096
|
-
// アカウント/プロトコルが違うだけでファイル保存の仕組みは共有する)。
|
|
4097
|
-
if (!ctx.uploadManager) {
|
|
4098
|
-
ctx.client.send({
|
|
4099
|
-
type: "codex.upload.done",
|
|
4100
|
-
stream_id: msg.stream_id,
|
|
4101
|
-
upload_id: msg.upload_id,
|
|
4102
|
-
ok: false,
|
|
4103
|
-
error: "upload_unavailable",
|
|
4104
|
-
})
|
|
4105
|
-
return
|
|
4106
|
-
}
|
|
4107
|
-
try {
|
|
4108
|
-
const res = await ctx.uploadManager.addChunk({
|
|
4109
|
-
upload_id: msg.upload_id,
|
|
4110
|
-
filename: msg.filename,
|
|
4111
|
-
media_type: msg.media_type,
|
|
4112
|
-
total_size: msg.total_size,
|
|
4113
|
-
total_chunks: msg.total_chunks,
|
|
4114
|
-
chunk_index: msg.chunk_index,
|
|
4115
|
-
data: msg.data,
|
|
4116
|
-
cwd: msg.cwd || ctx.config?.claude_cwd || undefined,
|
|
4117
|
-
})
|
|
4118
|
-
if (res.done) {
|
|
4322
|
+
// パス参照方式の添付受信を共有し、返信型と既定 cwd だけを切り替える。
|
|
4323
|
+
async function handleUpload(doneType, getDefaultCwd) {
|
|
4324
|
+
if (!ctx.uploadManager) {
|
|
4119
4325
|
ctx.client.send({
|
|
4120
|
-
type:
|
|
4326
|
+
type: doneType,
|
|
4121
4327
|
stream_id: msg.stream_id,
|
|
4122
4328
|
upload_id: msg.upload_id,
|
|
4123
|
-
ok:
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
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),
|
|
4128
4364
|
})
|
|
4129
4365
|
}
|
|
4130
|
-
}
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
ok: false,
|
|
4136
|
-
error: err?.message || String(err),
|
|
4137
|
-
})
|
|
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)
|
|
4138
4371
|
}
|
|
4139
4372
|
return
|
|
4140
4373
|
}
|
|
@@ -4771,11 +5004,13 @@ export async function dispatch(msg, ctx) {
|
|
|
4771
5004
|
try {
|
|
4772
5005
|
if (msg.scope === "cwd") {
|
|
4773
5006
|
const cwd = typeof msg.cwd === "string" ? msg.cwd : ""
|
|
4774
|
-
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"
|
|
4775
5008
|
else if (!cwd || !path.isAbsolute(cwd)) result.error = "cwd required"
|
|
4776
5009
|
else if (msg.cli_kind === "claude") {
|
|
4777
5010
|
noteRestoreStop(ctx.sessionRestoreState, "claude", cwd)
|
|
4778
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
|
|
4779
5014
|
} else {
|
|
4780
5015
|
noteRestoreStop(ctx.sessionRestoreState, "codex", cwd)
|
|
4781
5016
|
result.stopped = await ctx.codexBridge.stopSessionsForCwd({ cwd, reason: "chat-stop" })
|
|
@@ -4793,6 +5028,9 @@ export async function dispatch(msg, ctx) {
|
|
|
4793
5028
|
noteRestoreStop(ctx.sessionRestoreState, "claude", cwd)
|
|
4794
5029
|
result.stopped = await ctx.claudeBridge.stopSessionForDeletion({ cwd, session_id, reason: "chat-stop" })
|
|
4795
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
|
|
4796
5034
|
} else if (msg.cli_kind === "codex") {
|
|
4797
5035
|
if (typeof msg.stream_id !== "string" || !msg.stream_id) result.error = "stream_id required"
|
|
4798
5036
|
else {
|
|
@@ -4814,7 +5052,7 @@ export async function dispatch(msg, ctx) {
|
|
|
4814
5052
|
case "chat.sessions.request": {
|
|
4815
5053
|
try {
|
|
4816
5054
|
const snapshot = await buildChatSessionsSnapshot({
|
|
4817
|
-
bridges: { claudeBridge: ctx.claudeBridge, codexBridge: ctx.codexBridge },
|
|
5055
|
+
bridges: { claudeBridge: ctx.claudeBridge, codexBridge: ctx.codexBridge, grokBridge: ctx.grokBridge },
|
|
4818
5056
|
logger: ctx.logger,
|
|
4819
5057
|
})
|
|
4820
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
|