@cocorograph/hub-agent 0.8.115 → 0.8.117

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.8.115",
3
+ "version": "0.8.117",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -34,35 +34,14 @@ import {
34
34
  } from "./tool-result-mask.mjs"
35
35
  import { createClaudeDisplayMasker, claudeMaskFailure } from "./claude-display-mask.mjs"
36
36
  import { copyPermissionInput, preparePermissionInput, restorePermissionInput, PERMISSION_MASK_DENIAL } from "./permission-display-mask.mjs"
37
- import { getOrCreateSiblingToken } from "./sibling-token.mjs"
37
+ import { buildSiblingEnv } from "./sibling-token.mjs"
38
38
  import { recordInjectSourcePersistent } from "./inject-source-store.mjs"
39
39
  import { readPositiveIntEnv, sanitizePositiveInt } from "./positive-int-env.mjs"
40
40
  import { nextSessionSeq } from "./session-seq.mjs"
41
41
 
42
- /**
43
- * sibling-inject (2026-07-04) 用: spawn する claude プロセス自身の環境変数に、
44
- * この cwd の capability トークンを乗せる。ディスクには書かず、プロセス環境変数
45
- * としてのみ渡す (src/sibling-token.mjs のコメント参照)。
46
- * @param {string} cwd
47
- * @returns {NodeJS.ProcessEnv & {COCKPIT_SIBLING_TOKEN: string, COCKPIT_SIBLING_CWD: string,
48
- * COCKPIT_DELEGATE_EVENT_ACK: string}}
49
- */
42
+ /** sibling-inject 用の spawn 環境 (共通処理は sibling-token.mjs の buildSiblingEnv)。 */
50
43
  function siblingEnvFor(cwd) {
51
- return {
52
- ...process.env,
53
- COCKPIT_SIBLING_TOKEN: getOrCreateSiblingToken(cwd),
54
- COCKPIT_SIBLING_CWD: cwd,
55
- // Spawn-time display capability; rejection by an older daemon does not stop the review.
56
- COCKPIT_REVIEW_EVENTS: "1",
57
- // hub-agent が spawn した Claude 配下 producer だけが delegate-event の受理 ACK を
58
- // 要求するための capability。能力は spawn 時点で固定されるため、hub-agent を旧版へ
59
- // ロールバックしても既存 producer は ACK を要求し続け、timeout し得る。
60
- // 旧 bundle はこの値を無視し、従来どおり fire-and-forget のまま動く。
61
- COCKPIT_DELEGATE_EVENT_ACK: "1",
62
- // ask-sibling スキル (cockpit_orch.py) が「自分は claude/codex どちらか」を
63
- // 環境変数から確実に判定できるようにする (ヒューリスティック不要)。
64
- COCKPIT_SIBLING_SELF_KIND: "claude",
65
- }
44
+ return buildSiblingEnv(cwd, { selfKind: "claude", delegateEventAck: true })
66
45
  }
67
46
 
68
47
  /** browser 切断後、セッションを生かしたまま再接続を待つ idle TTL (ミリ秒)。これを過ぎ、
@@ -68,7 +68,7 @@ import {
68
68
  snapshotCodexDeliveryAudit,
69
69
  verifyCodexDeliveryAudit,
70
70
  } from "./codex-delivery-audit.mjs"
71
- import { getOrCreateSiblingToken } from "./sibling-token.mjs"
71
+ import { buildSiblingEnv } from "./sibling-token.mjs"
72
72
  import { readPositiveIntEnv, sanitizePositiveInt } from "./positive-int-env.mjs"
73
73
  import { nextSessionSeq } from "./session-seq.mjs"
74
74
  import { codexRolloutPresence, listCodexThreadsFromDisk } from "./usage.mjs"
@@ -192,25 +192,10 @@ async function buildCodexHostCapabilities({
192
192
  }
193
193
  }
194
194
 
195
- /**
196
- * sibling-inject (2026-07-04) 用: spawn する codex app-server プロセス自身の
197
- * 環境変数に、この cwd の capability トークンを乗せる。ディスクには書かず、
198
- * プロセス環境変数としてのみ渡す (src/sibling-token.mjs のコメント参照、
199
- * claude-stream-bridge.mjs の同名ヘルパーと対)。
200
- * @param {string} cwd
201
- * @returns {NodeJS.ProcessEnv}
202
- */
195
+ /** sibling-inject 用の spawn 環境 (共通処理は sibling-token.mjs の buildSiblingEnv)。
196
+ * Codex には delegate-event の ACK capability を付けない。 */
203
197
  function siblingEnvFor(cwd) {
204
- return {
205
- ...process.env,
206
- COCKPIT_SIBLING_TOKEN: getOrCreateSiblingToken(cwd),
207
- COCKPIT_SIBLING_CWD: cwd,
208
- // Spawn-time display capability; rejection by an older daemon does not stop the review.
209
- COCKPIT_REVIEW_EVENTS: "1",
210
- // ask-sibling スキル (cockpit_orch.py) が「自分は claude/codex どちらか」を
211
- // 環境変数から確実に判定できるようにする (ヒューリスティック不要)。
212
- COCKPIT_SIBLING_SELF_KIND: "codex",
213
- }
198
+ return buildSiblingEnv(cwd, { selfKind: "codex" })
214
199
  }
215
200
 
216
201
  // ── Stage 5 (2026-07-19): bounded warm pool + 長 idle TTL への再設計 ───────────
@@ -3,23 +3,13 @@ import { EventEmitter } from 'node:events'
3
3
  import { readPositiveIntEnv, sanitizePositiveInt } from './positive-int-env.mjs'
4
4
  import { GrokAcpClient } from './grok-acp-client.mjs'
5
5
  import { isUnderRemoval } from './chat-removal-guard.mjs'
6
- import { getOrCreateSiblingToken } from './sibling-token.mjs'
6
+ import { buildSiblingEnv } from './sibling-token.mjs'
7
7
 
8
8
  // hub-bundle.mjs の HUB_MCP_NAMES と揃える。重い依存を避け、ここで定義する。
9
9
  const HUB_MCP_NAMES = new Set(['hub', 'hub_staff'])
10
10
 
11
11
  function siblingEnvFor(cwd, inherited = process.env) {
12
- const env = {
13
- ...inherited,
14
- COCKPIT_SIBLING_TOKEN: getOrCreateSiblingToken(cwd),
15
- COCKPIT_SIBLING_CWD: cwd,
16
- COCKPIT_REVIEW_EVENTS: '1',
17
- COCKPIT_DELEGATE_EVENT_ACK: '1',
18
- COCKPIT_SIBLING_SELF_KIND: 'grok',
19
- }
20
- delete env.CLAUDE_CODE_SESSION_ID
21
- delete env.CODEX_THREAD_ID
22
- return env
12
+ return buildSiblingEnv(cwd, { selfKind: 'grok', delegateEventAck: true, stripParentSessionIds: true, inherited })
23
13
  }
24
14
 
25
15
  // 完了済み配送 ID の保持上限。上限を超えた古い完了済み ID の再送は再実行されうる。
package/src/main.mjs CHANGED
@@ -1574,14 +1574,31 @@ export function withSortAffectingAt(snapshot, getSortAt = getSortAffectingAt) {
1574
1574
  const live = Array.isArray(snapshot?.live) ? snapshot.live : []
1575
1575
  return {
1576
1576
  ...snapshot,
1577
- live: live.map((item) => {
1578
- const name = typeof item?.session_name === "string" ? item.session_name : ""
1579
- const at = name ? getSortAt(name) : undefined
1580
- return typeof at === "number" && at > 0 ? { ...item, sort_affecting_at: at } : item
1581
- }),
1577
+ live: live.map((item) => attachSortAffectingAt(item, getSortAt)),
1582
1578
  }
1583
1579
  }
1584
1580
 
1581
+ /** 1 行 (`session_name` を持つ) に送信時刻を載せる。chat.sessions と sidebar_snapshot で共有する。 */
1582
+ function attachSortAffectingAt(item, getSortAt) {
1583
+ const name = typeof item?.session_name === "string" ? item.session_name : ""
1584
+ const at = name ? getSortAt(name) : undefined
1585
+ return typeof at === "number" && at > 0 ? { ...item, sort_affecting_at: at } : item
1586
+ }
1587
+
1588
+ /**
1589
+ * `chat.sidebar_snapshot.response` の各行に、その行の「最後にユーザーが送信した時刻」
1590
+ * (`sort_affecting_at`) を載せる。
1591
+ *
1592
+ * `chat.sessions` は live な会話にしか送信時刻を載せない。サイドバーは、ユーザーが×で閉じて
1593
+ * いないのにプロセスが落ちて一覧から消えた会話も「起きている」段に残して送信時刻順に並べる
1594
+ * ため、再読み込み後はここでしか送信時刻を復元できない (2026-10-24)。
1595
+ * 値は withSortAffectingAt と同じ (sidecar で永続化済みのもの)。送信履歴の無い行は
1596
+ * キーごと付けない。
1597
+ */
1598
+ export function withSnapshotSortAt(messages, getSortAt = getSortAffectingAt) {
1599
+ return (Array.isArray(messages) ? messages : []).map((item) => attachSortAffectingAt(item, getSortAt))
1600
+ }
1601
+
1585
1602
  // sort 時刻 sidecar の接尾辞。`.json` にしない (readAllSessionEvents / pushFile が
1586
1603
  // `.endsWith(".json")` で event ファイルと誤認しないように)。
1587
1604
  const SORT_AT_SUFFIX = ".sortat"
@@ -3705,6 +3722,61 @@ export function createClaudeAttachTiming(ctx, msg) {
3705
3722
  }
3706
3723
  }
3707
3724
 
3725
+ /**
3726
+ * Cockpit チャットの添付ファイル受信 (チャンク 1 件) を Claude / Codex / Grok で共有する。
3727
+ * CLI ごとに違うのは返信型と既定 cwd だけ。既定 cwd は uploadManager がある場合に限り、
3728
+ * msg.cwd が無いときだけ評価する。完了チャンクだけに done を返し、継続中チャンクは応答しない
3729
+ * (browser は自分のチャンク送出完了で進捗を把握する)。
3730
+ * @param {object} msg
3731
+ * @param {object} ctx
3732
+ * @param {string} doneType
3733
+ * @param {() => string | undefined} getDefaultCwd
3734
+ */
3735
+ async function handleChatUpload(msg, ctx, doneType, getDefaultCwd) {
3736
+ if (!ctx.uploadManager) {
3737
+ ctx.client.send({
3738
+ type: doneType,
3739
+ stream_id: msg.stream_id,
3740
+ upload_id: msg.upload_id,
3741
+ ok: false,
3742
+ error: "upload_unavailable",
3743
+ })
3744
+ return
3745
+ }
3746
+ try {
3747
+ const res = await ctx.uploadManager.addChunk({
3748
+ upload_id: msg.upload_id,
3749
+ filename: msg.filename,
3750
+ media_type: msg.media_type,
3751
+ total_size: msg.total_size,
3752
+ total_chunks: msg.total_chunks,
3753
+ chunk_index: msg.chunk_index,
3754
+ data: msg.data,
3755
+ cwd: msg.cwd || getDefaultCwd(),
3756
+ })
3757
+ if (res.done) {
3758
+ ctx.client.send({
3759
+ type: doneType,
3760
+ stream_id: msg.stream_id,
3761
+ upload_id: msg.upload_id,
3762
+ ok: true,
3763
+ path: res.path,
3764
+ filename: res.filename,
3765
+ media_type: res.media_type,
3766
+ size: res.size,
3767
+ })
3768
+ }
3769
+ } catch (err) {
3770
+ ctx.client.send({
3771
+ type: doneType,
3772
+ stream_id: msg.stream_id,
3773
+ upload_id: msg.upload_id,
3774
+ ok: false,
3775
+ error: err?.message || String(err),
3776
+ })
3777
+ }
3778
+ }
3779
+
3708
3780
  /**
3709
3781
  * DispatchScheduler から呼ばれる本番メッセージ入口。
3710
3782
  * export はハンドラ契約を subprocess なしで検証するテスト専用の公開面でもある。
@@ -3928,56 +4000,12 @@ export async function dispatch(msg, ctx) {
3928
4000
  // message: { role: 'user', content: string | Array } を期待
3929
4001
  handleClaudeChatInput(msg, ctx)
3930
4002
  return
3931
- case "claude.upload": {
4003
+ case "claude.upload":
3932
4004
  // Cockpit チャットモードの添付ファイル受信 (チャンク 1 件)。全チャンク到着で
3933
4005
  // cwd 配下の .cockpit-uploads/<日付>/ に保存し、保存パスを browser へ返す。
3934
4006
  // browser はそのパスを claude.input 本文に載せ、Claude が Read ツールで読む。
3935
- if (!ctx.uploadManager) {
3936
- ctx.client.send({
3937
- type: "claude.upload.done",
3938
- stream_id: msg.stream_id,
3939
- upload_id: msg.upload_id,
3940
- ok: false,
3941
- error: "upload_unavailable",
3942
- })
3943
- return
3944
- }
3945
- try {
3946
- const res = await ctx.uploadManager.addChunk({
3947
- upload_id: msg.upload_id,
3948
- filename: msg.filename,
3949
- media_type: msg.media_type,
3950
- total_size: msg.total_size,
3951
- total_chunks: msg.total_chunks,
3952
- chunk_index: msg.chunk_index,
3953
- data: msg.data,
3954
- cwd: msg.cwd || ctx.config?.claude_cwd || undefined,
3955
- })
3956
- if (res.done) {
3957
- // 完了チャンクのみ done を返す。継続中チャンクは応答しない
3958
- // (browser は自分のチャンク送出完了で進捗を把握する)。
3959
- ctx.client.send({
3960
- type: "claude.upload.done",
3961
- stream_id: msg.stream_id,
3962
- upload_id: msg.upload_id,
3963
- ok: true,
3964
- path: res.path,
3965
- filename: res.filename,
3966
- media_type: res.media_type,
3967
- size: res.size,
3968
- })
3969
- }
3970
- } catch (err) {
3971
- ctx.client.send({
3972
- type: "claude.upload.done",
3973
- stream_id: msg.stream_id,
3974
- upload_id: msg.upload_id,
3975
- ok: false,
3976
- error: err?.message || String(err),
3977
- })
3978
- }
4007
+ await handleChatUpload(msg, ctx, "claude.upload.done", () => ctx.config?.claude_cwd || undefined)
3979
4008
  return
3980
- }
3981
4009
  case "claude.permission.reply": {
3982
4010
  if (!ctx.claudeBridge) return
3983
4011
  ctx.claudeBridge.permissionReply({
@@ -4318,59 +4346,11 @@ export async function dispatch(msg, ctx) {
4318
4346
  return
4319
4347
  }
4320
4348
  case "grok.upload":
4321
- case "codex.upload": {
4322
- // パス参照方式の添付受信を共有し、返信型と既定 cwd だけを切り替える。
4323
- async function handleUpload(doneType, getDefaultCwd) {
4324
- if (!ctx.uploadManager) {
4325
- ctx.client.send({
4326
- type: doneType,
4327
- stream_id: msg.stream_id,
4328
- upload_id: msg.upload_id,
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),
4364
- })
4365
- }
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)
4371
- }
4349
+ await handleChatUpload(msg, ctx, "grok.upload.done", () => defaultLaneCwd(ctx.config, "grok"))
4350
+ return
4351
+ case "codex.upload":
4352
+ await handleChatUpload(msg, ctx, "codex.upload.done", () => ctx.config?.claude_cwd || undefined)
4372
4353
  return
4373
- }
4374
4354
  case "codex.input":
4375
4355
  // 通常送信は idle なら turn/start、busy なら hub-agent の次ターン FIFO。
4376
4356
  // turn/steer / turn/interrupt は明示操作の別経路として扱う。
@@ -5066,10 +5046,10 @@ export async function dispatch(msg, ctx) {
5066
5046
  }
5067
5047
  case "chat.sidebar_snapshot.request": {
5068
5048
  try {
5069
- const messages = await readSidebarMessageSnapshot(msg.targets, {
5049
+ const messages = withSnapshotSortAt(await readSidebarMessageSnapshot(msg.targets, {
5070
5050
  claudeBridge: ctx.claudeBridge,
5071
5051
  codexBridge: ctx.codexBridge,
5072
- })
5052
+ }))
5073
5053
  ctx.client.send({
5074
5054
  type: "chat.sidebar_snapshot.response",
5075
5055
  request_id: msg.request_id,
@@ -425,6 +425,16 @@ function queuedResult(r) {
425
425
  }
426
426
  }
427
427
 
428
+ // 配送前に未受理と確定した busy だけに再試行印を付ける。
429
+ function busyResult() {
430
+ return {
431
+ ok: false,
432
+ error: "target_busy",
433
+ retryable: true,
434
+ hint: "宛先の未完了配送が上限に達しているため、このメッセージは受理されていません。混雑が落ち着いてから再送してください。",
435
+ }
436
+ }
437
+
428
438
  export class SiblingInjectBridge {
429
439
  /**
430
440
  * @param {{dir?: string, claudeBridge?: object, codexBridge?: object, grokBridge?: object, logger?: object,
@@ -1099,6 +1109,8 @@ export class SiblingInjectBridge {
1099
1109
  }
1100
1110
  } else if (r.status === "delivery_queued") {
1101
1111
  result = queuedResult(r)
1112
+ } else if (r.status === "busy") {
1113
+ result = busyResult()
1102
1114
  } else if (r.status === "delivery_unconfirmed" || r.status === "delivery_failed") {
1103
1115
  result = {
1104
1116
  ok: false,
@@ -1195,6 +1207,8 @@ export class SiblingInjectBridge {
1195
1207
  }
1196
1208
  } else if (r.status === "delivery_queued") {
1197
1209
  result = queuedResult(r)
1210
+ } else if (r.status === "busy") {
1211
+ result = busyResult()
1198
1212
  } else if (r.status === "delivery_unconfirmed" || r.status === "delivery_failed") {
1199
1213
  result = {
1200
1214
  ok: false,
@@ -100,6 +100,41 @@ export function siblingTokenCwd(token) {
100
100
  return null
101
101
  }
102
102
 
103
+ /**
104
+ * spawn する CLI プロセス自身の環境変数に、この cwd の sibling 連携用 capability を乗せる。
105
+ * ディスクには書かず、プロセス環境変数としてのみ渡す。継承元の環境オブジェクトは変更しない。
106
+ * @param {string} cwd
107
+ * @param {{ selfKind: "claude" | "codex" | "grok", delegateEventAck?: boolean,
108
+ * stripParentSessionIds?: boolean, inherited?: NodeJS.ProcessEnv }} options
109
+ * - selfKind: ask-sibling スキル (cockpit_orch.py) が「自分はどの CLI か」を環境変数から
110
+ * 確実に判定するための値。
111
+ * - delegateEventAck: hub-agent が spawn した producer だけが delegate-event の受理 ACK を
112
+ * 要求するための capability。spawn 時点で固定されるため、hub-agent を旧版へロールバック
113
+ * しても既存 producer は ACK を要求し続け、timeout し得る。旧 bundle はこの値を無視する。
114
+ * Codex には付けない。
115
+ * - stripParentSessionIds: 親 CLI のセッション ID を子へ継承させない (Grok)。
116
+ * @returns {NodeJS.ProcessEnv}
117
+ */
118
+ export function buildSiblingEnv(cwd, { selfKind, delegateEventAck = false, stripParentSessionIds = false,
119
+ inherited = process.env }) {
120
+ const env = {
121
+ ...inherited,
122
+ COCKPIT_SIBLING_TOKEN: getOrCreateSiblingToken(cwd),
123
+ COCKPIT_SIBLING_CWD: cwd,
124
+ // Spawn-time display capability; rejection by an older daemon does not stop the review.
125
+ COCKPIT_REVIEW_EVENTS: "1",
126
+ ...(delegateEventAck ? { COCKPIT_DELEGATE_EVENT_ACK: "1" } : {}),
127
+ COCKPIT_SIBLING_SELF_KIND: selfKind,
128
+ }
129
+ // ACK を付けない CLI では、継承元に ACK があっても子へ持ち込まない。
130
+ if (!delegateEventAck) delete env.COCKPIT_DELEGATE_EVENT_ACK
131
+ if (stripParentSessionIds) {
132
+ delete env.CLAUDE_CODE_SESSION_ID
133
+ delete env.CODEX_THREAD_ID
134
+ }
135
+ return env
136
+ }
137
+
103
138
  /** テスト用: トークンストアを全クリアする。 */
104
139
  export function _resetSiblingTokens() {
105
140
  tokensByCwd.clear()