@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.
@@ -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` は「末尾が `__codex` か」だけで決まる純関数。tmux / 設定 /
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 = "__codex"
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(["claude", "codex"])
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
- return sessionName.endsWith(CODEX_SESSION_SUFFIX) ? "codex" : "claude"
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
- return sessionName.endsWith(CODEX_SESSION_SUFFIX)
69
- ? sessionName.slice(0, -CODEX_SESSION_SUFFIX.length)
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 normalizeCliKind(cliKind) === "codex" ? `${base}${CODEX_SESSION_SUFFIX}` : base
94
+ return `${base}${SESSION_SUFFIXES[normalizeCliKind(cliKind)]}`
86
95
  }
@@ -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 nameability = cliKind === "codex" ? "nameable" : claudeCandidatesNameability(candidates)
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
- ? cliKind === "codex"
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 (cliKind === "codex") {
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 = target_cli_kind !== "claude" && target_cli_kind !== "codex"
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 === "codex" ? this.codexBridge : this.claudeBridge
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
  }