@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/session-names.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* ## 規約 (2026-07-09 確定)
|
|
11
11
|
* claude → `<dir>` (後方互換。既存セッションはそのまま claude として解釈される)
|
|
12
12
|
* codex → `<dir>__codex` (予約サフィックス)
|
|
13
|
+
* grok → `<dir>__grok` (予約サフィックス)
|
|
13
14
|
*
|
|
14
15
|
* `__codex` を予約サフィックスに選べる根拠 (Phase0 スパイク済み):
|
|
15
16
|
* - frontend の `sanitizeSessionName` は `.` `:` のみ `-` へ置換し、`_` は保持する
|
|
@@ -17,19 +18,25 @@
|
|
|
17
18
|
*
|
|
18
19
|
* ## 不変条件
|
|
19
20
|
* - `sessionNameFor(baseSessionName(n), cliKindForSessionName(n)) === n` (往復同一)
|
|
20
|
-
* - `cliKindForSessionName`
|
|
21
|
+
* - `cliKindForSessionName` は「末尾の予約サフィックス」だけで決まる純関数。tmux / 設定 /
|
|
21
22
|
* ブリッジの状態を一切参照しない。これが「セッション行の CLI 種別を 1 箇所だけ見れば
|
|
22
23
|
* 済む」ための SoT である。
|
|
23
24
|
*
|
|
25
|
+
* backend 側の対応実装は `backend/apps/cockpit/session_names.py`。
|
|
24
26
|
* frontend 側の対応実装は `frontend/lib/cockpit/sessionKey.ts` (同じ規約を TypeScript で再掲)。
|
|
25
|
-
*
|
|
27
|
+
* 変更時は backend / frontend / agent の規約を揃えること。
|
|
26
28
|
*/
|
|
27
29
|
|
|
30
|
+
const SESSION_SUFFIXES = { claude: "", codex: "__codex", grok: "__grok" }
|
|
31
|
+
|
|
28
32
|
/** codex セッションを表す予約サフィックス。 */
|
|
29
|
-
export const CODEX_SESSION_SUFFIX =
|
|
33
|
+
export const CODEX_SESSION_SUFFIX = SESSION_SUFFIXES.codex
|
|
34
|
+
|
|
35
|
+
/** grok セッションを表す予約サフィックス。 */
|
|
36
|
+
export const GROK_SESSION_SUFFIX = SESSION_SUFFIXES.grok
|
|
30
37
|
|
|
31
38
|
/** 有効な cli_kind。 */
|
|
32
|
-
const VALID_CLI_KINDS = new Set(
|
|
39
|
+
const VALID_CLI_KINDS = new Set(Object.keys(SESSION_SUFFIXES))
|
|
33
40
|
|
|
34
41
|
/**
|
|
35
42
|
* 任意の入力を cli_kind へ正規化する。未知値 / null / undefined は "claude" (既定 CLI)。
|
|
@@ -39,7 +46,7 @@ const VALID_CLI_KINDS = new Set(["claude", "codex"])
|
|
|
39
46
|
* 方が退行が小さい (claude が圧倒的多数のセッション)。
|
|
40
47
|
*
|
|
41
48
|
* @param {unknown} value
|
|
42
|
-
* @returns {'claude'|'codex'}
|
|
49
|
+
* @returns {'claude'|'codex'|'grok'}
|
|
43
50
|
*/
|
|
44
51
|
export function normalizeCliKind(value) {
|
|
45
52
|
return VALID_CLI_KINDS.has(value) ? value : "claude"
|
|
@@ -49,11 +56,14 @@ export function normalizeCliKind(value) {
|
|
|
49
56
|
* tmux セッション名からその行の CLI 種別を導出する (構造的判定)。
|
|
50
57
|
*
|
|
51
58
|
* @param {string} sessionName
|
|
52
|
-
* @returns {'claude'|'codex'}
|
|
59
|
+
* @returns {'claude'|'codex'|'grok'}
|
|
53
60
|
*/
|
|
54
61
|
export function cliKindForSessionName(sessionName) {
|
|
55
62
|
if (typeof sessionName !== "string") return "claude"
|
|
56
|
-
|
|
63
|
+
for (const [kind, suffix] of Object.entries(SESSION_SUFFIXES)) {
|
|
64
|
+
if (suffix && sessionName.endsWith(suffix)) return kind
|
|
65
|
+
}
|
|
66
|
+
return "claude"
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
/**
|
|
@@ -65,15 +75,14 @@ export function cliKindForSessionName(sessionName) {
|
|
|
65
75
|
*/
|
|
66
76
|
export function baseSessionName(sessionName) {
|
|
67
77
|
if (typeof sessionName !== "string") return ""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
: sessionName
|
|
78
|
+
const suffix = SESSION_SUFFIXES[cliKindForSessionName(sessionName)]
|
|
79
|
+
return suffix ? sessionName.slice(0, -suffix.length) : sessionName
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
/**
|
|
74
83
|
* dir 名 (ベース名) と cli_kind から tmux セッション名を組み立てる。
|
|
75
84
|
*
|
|
76
|
-
* 既に `__codex` が付いたベース名を渡しても二重付与しない (冪等)。
|
|
85
|
+
* 既に `__codex` / `__grok` が付いたベース名を渡しても二重付与しない (冪等)。
|
|
77
86
|
*
|
|
78
87
|
* @param {string} dir
|
|
79
88
|
* @param {unknown} cliKind
|
|
@@ -82,5 +91,5 @@ export function baseSessionName(sessionName) {
|
|
|
82
91
|
export function sessionNameFor(dir, cliKind) {
|
|
83
92
|
const base = baseSessionName(String(dir || ""))
|
|
84
93
|
if (!base) return ""
|
|
85
|
-
return
|
|
94
|
+
return `${base}${SESSION_SUFFIXES[normalizeCliKind(cliKind)]}`
|
|
86
95
|
}
|
package/src/sibling-inject.mjs
CHANGED
|
@@ -337,7 +337,7 @@ function claudeCandidatesNameability(candidates) {
|
|
|
337
337
|
* 「hint 用」「フィールド用」「エラーコード用」と 3 回呼ぶ形にすると将来ずれるので、1 つの判定から
|
|
338
338
|
* 全部を組み立てて構造的に食い違えないようにしている。
|
|
339
339
|
*
|
|
340
|
-
* @param {string} cliKind 宛先 CLI ("codex" | "claude")
|
|
340
|
+
* @param {string} cliKind 宛先 CLI ("codex" | "claude" | "grok")
|
|
341
341
|
* @param {string} [targetSessionId] 明示宛先で指定された id (省略 = 宛先省略の曖昧)
|
|
342
342
|
* @param {object[]} [candidates] 曖昧になった候補一覧 (claude の名指し可否判定に使う)
|
|
343
343
|
* @returns {{error: string, hint: string, candidates_nameable: boolean}}
|
|
@@ -348,15 +348,16 @@ function ambiguousTargetOutcome(cliKind, targetSessionId, candidates) {
|
|
|
348
348
|
// codex は候補ごとに必ず一意な接続 ID (stream_id) を持つため、候補を見るまでもなく名指しで
|
|
349
349
|
// 解消できる (既定の宛先は会話 ID = thread_id だが、同じ会話に複数の接続がぶら下がると会話 ID
|
|
350
350
|
// では一意に定まらない。そのときに接続 ID で絞り込む、という降格した位置づけ)。
|
|
351
|
-
const
|
|
351
|
+
const streamAddressable = cliKind === "codex" || cliKind === "grok"
|
|
352
|
+
const nameability = streamAddressable ? "nameable" : claudeCandidatesNameability(candidates)
|
|
352
353
|
const nameable = nameability === "nameable"
|
|
353
354
|
const lead = targetSessionId
|
|
354
|
-
?
|
|
355
|
+
? streamAddressable
|
|
355
356
|
? `指定した id (${targetSessionId}) はこの cwd で複数の live セッションに一致します。`
|
|
356
357
|
: `指定した session_id (${targetSessionId}) がこの cwd で複数の live セッションに一致します。`
|
|
357
358
|
: `この cwd には live な ${cliKind} セッションが複数あるため、宛先を省略すると一意に定まりません。`
|
|
358
359
|
let remedy
|
|
359
|
-
if (
|
|
360
|
+
if (streamAddressable) {
|
|
360
361
|
// ⚠️ 「stream_id を指定」とだけ案内しない (2026-08-05)。--list / --list-all が既定で表示する
|
|
361
362
|
// 宛先は **会話 ID (thread_id)** で、stream_id は「同じ会話に複数の接続がある稀なケース」の
|
|
362
363
|
// 曖昧解消用へ降格した。利用者が画面で見ているのは --list が候補に併記する `stream=…` の
|
|
@@ -426,7 +427,7 @@ function queuedResult(r) {
|
|
|
426
427
|
|
|
427
428
|
export class SiblingInjectBridge {
|
|
428
429
|
/**
|
|
429
|
-
* @param {{dir?: string, claudeBridge?: object, codexBridge?: object, logger?: object,
|
|
430
|
+
* @param {{dir?: string, claudeBridge?: object, codexBridge?: object, grokBridge?: object, logger?: object,
|
|
430
431
|
* now?: () => number, processKill?: (pid: number, signal: number) => void,
|
|
431
432
|
* pidStartedAt?: (pid: number) => Promise<string>, onRestartRequested?: () => object,
|
|
432
433
|
* isInputBlocked?: () => boolean, watchFn?: typeof watch}} [opts]
|
|
@@ -435,6 +436,7 @@ export class SiblingInjectBridge {
|
|
|
435
436
|
dir = SIBLING_REQUESTS_DIR,
|
|
436
437
|
claudeBridge,
|
|
437
438
|
codexBridge,
|
|
439
|
+
grokBridge,
|
|
438
440
|
logger,
|
|
439
441
|
now = Date.now,
|
|
440
442
|
processKill = process.kill.bind(process),
|
|
@@ -447,6 +449,7 @@ export class SiblingInjectBridge {
|
|
|
447
449
|
this.dir = dir
|
|
448
450
|
this.claudeBridge = claudeBridge
|
|
449
451
|
this.codexBridge = codexBridge
|
|
452
|
+
this.grokBridge = grokBridge
|
|
450
453
|
this.logger = logger
|
|
451
454
|
/** @type {Set<string>} 二重処理防止 (request_id) */
|
|
452
455
|
this._seen = new Set()
|
|
@@ -679,7 +682,7 @@ export class SiblingInjectBridge {
|
|
|
679
682
|
* 経由し、`tmux.list_sessions` の `warm` と同じ判定を共有する。
|
|
680
683
|
*/
|
|
681
684
|
_bridges() {
|
|
682
|
-
return { claudeBridge: this.claudeBridge, codexBridge: this.codexBridge }
|
|
685
|
+
return { claudeBridge: this.claudeBridge, codexBridge: this.codexBridge, grokBridge: this.grokBridge }
|
|
683
686
|
}
|
|
684
687
|
|
|
685
688
|
async replayReviews(kind, cwd, sessionId) {
|
|
@@ -901,7 +904,7 @@ export class SiblingInjectBridge {
|
|
|
901
904
|
const hasRequestAck = Object.prototype.hasOwnProperty.call(body || {}, "request_ack")
|
|
902
905
|
const isRestart = op === "restart"
|
|
903
906
|
let result
|
|
904
|
-
const badKind =
|
|
907
|
+
const badKind = !["claude", "codex", "grok"].includes(target_cli_kind)
|
|
905
908
|
const delegatePhases = new Set(["started", "completed", "failed", "stopped"])
|
|
906
909
|
const badDelegateBody =
|
|
907
910
|
typeof delegation_id !== "string" ||
|
|
@@ -992,7 +995,8 @@ export class SiblingInjectBridge {
|
|
|
992
995
|
)
|
|
993
996
|
}
|
|
994
997
|
}
|
|
995
|
-
const bridge = target_cli_kind === "
|
|
998
|
+
const bridge = target_cli_kind === "grok" ? this.grokBridge
|
|
999
|
+
: target_cli_kind === "codex" ? this.codexBridge : this.claudeBridge
|
|
996
1000
|
if (isReviewEvent) {
|
|
997
1001
|
const matches = describeLiveSessionsUnion(this._bridges(), { cwd, cliKind: target_cli_kind })
|
|
998
1002
|
.filter((s) => (target_cli_kind === "codex" ? s.thread_id : s.session_id) === target_session_id)
|
package/src/ws-client.mjs
CHANGED
|
@@ -74,6 +74,16 @@ const BUFFERED_CTRL_TYPES = new Set([
|
|
|
74
74
|
"claude.permission.cancel",
|
|
75
75
|
"claude.input.ack",
|
|
76
76
|
"claude.input.nack",
|
|
77
|
+
"grok.event",
|
|
78
|
+
"grok.ready",
|
|
79
|
+
"grok.exit",
|
|
80
|
+
"grok.error",
|
|
81
|
+
"grok.permission.request",
|
|
82
|
+
// 破棄の通知も保存し、再接続後に要求より先に届いて取消済みの承認が復活するのを防ぐ。
|
|
83
|
+
"grok.permission.cancelled",
|
|
84
|
+
"grok.input.ack",
|
|
85
|
+
"grok.input.nack",
|
|
86
|
+
"grok.turn.complete",
|
|
77
87
|
"codex.event",
|
|
78
88
|
"codex.ready",
|
|
79
89
|
"codex.exit",
|
|
@@ -110,6 +120,19 @@ function isEphemeralCodexDeltaEvent(obj) {
|
|
|
110
120
|
return typeof method === "string" && method.toLowerCase().endsWith("/delta")
|
|
111
121
|
}
|
|
112
122
|
|
|
123
|
+
/** Grok ACP の本文 chunk(応答・思考・利用者発話)。高頻度で、切断中に積むと共有リングの
|
|
124
|
+
* 承認要求・ACK を押し出す。捨てた分は grok.ready の history(seq 付き)で復元する前提で、
|
|
125
|
+
* browser は seq の欠番、または turn.complete / exit / error の last_seq が受信済み seq より大きいとき
|
|
126
|
+
* 再 attach して history を取り直す(末尾の欠落は後続 update が無いので last_seq でしか分からない)。
|
|
127
|
+
* ⚠️ last_seq 付きの終端 frame もこのバッファの 30 秒期限で落ちうる。30 秒を超える切断の復元は
|
|
128
|
+
* バッファに頼らず、Codex と同じく backend が全タブへ送る agent.status online を受けた browser が
|
|
129
|
+
* 同じ attach_id で再 attach し、grok.ready の history / last_seq で取り直す(useCockpitCodexStream の
|
|
130
|
+
* agent.status 処理と同じ契約。Grok の画面側でも同じ処理を持つこと)。 */
|
|
131
|
+
const GROK_CHUNK_UPDATES = new Set(["agent_message_chunk", "agent_thought_chunk", "user_message_chunk"])
|
|
132
|
+
function isEphemeralGrokChunkEvent(obj) {
|
|
133
|
+
return GROK_CHUNK_UPDATES.has(obj?.event?.sessionUpdate)
|
|
134
|
+
}
|
|
135
|
+
|
|
113
136
|
export class WsClient extends EventEmitter {
|
|
114
137
|
get bufferedAmount() {
|
|
115
138
|
return this.ws?.readyState === WebSocket.OPEN ? this.ws.bufferedAmount : null
|
|
@@ -331,6 +354,9 @@ export class WsClient extends EventEmitter {
|
|
|
331
354
|
if (type === "codex.event" && isEphemeralCodexDeltaEvent(obj)) {
|
|
332
355
|
return false
|
|
333
356
|
}
|
|
357
|
+
if (type === "grok.event" && isEphemeralGrokChunkEvent(obj)) {
|
|
358
|
+
return false
|
|
359
|
+
}
|
|
334
360
|
this._bufferCtrl(obj)
|
|
335
361
|
return false
|
|
336
362
|
}
|