@cocorograph/hub-agent 0.8.84 → 0.8.86

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.84",
3
+ "version": "0.8.86",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -12,7 +12,7 @@
12
12
  # 1. macOS なら Homebrew (なければ install)
13
13
  # - 管理者権限が無い環境では自動で $HOME/homebrew にユーザーローカル
14
14
  # インストールに切り替わる (HUB_AGENT_USER_BREW=1 で強制も可)
15
- # 2. tmux + node + python3 (なければ install via brew / apt 等)
15
+ # 2. node + python3 (なければ install via brew / apt 等)
16
16
  # 3. GNU coreutils の timeout (補助コマンド。macOS では brew で入れて gtimeout への
17
17
  # symlink を ~/.local/bin に作成。Linux は存在確認のみで、不在なら手動導入を案内)
18
18
  # 4. npm i -g @cocorograph/hub-agent
@@ -1377,7 +1377,7 @@ macos_perm_guidance() {
1377
1377
  cat <<EOF
1378
1378
 
1379
1379
  📣 macOS の権限ダイアログについて
1380
- hub-agent は tmux セッションとローカルの CLI (Claude / Codex) を中継するため、初回起動時に macOS から
1380
+ hub-agent はローカルの CLI (Claude / Codex) を中継するため、初回起動時に macOS から
1381
1381
  「node がローカルネットワーク上の機器を検出することを求めています」
1382
1382
  「node がアクセシビリティを制御することを求めています」
1383
1383
  「node がほかのアプリからのデータへのアクセス権を求めています」
@@ -1396,13 +1396,11 @@ EOF
1396
1396
 
1397
1397
  main() {
1398
1398
  color_step "hub-agent ワンライナーセットアップを開始"
1399
- STEP_TOTAL=16
1399
+ STEP_TOTAL=15
1400
1400
  STEP_NUM=0
1401
1401
 
1402
1402
  step_header "Homebrew"
1403
1403
  ensure_brew
1404
- step_header "tmux"
1405
- ensure_pkg tmux tmux tmux
1406
1404
  step_header "Node.js (Active LTS)"
1407
1405
  ensure_node_version
1408
1406
  step_header "Build tools (native addons)"
@@ -73,6 +73,31 @@ function attachedOf(item) {
73
73
  return null
74
74
  }
75
75
 
76
+ /** listChatTargets の戻り値を cwd 引きの Map にする。 */
77
+ export function chatTargetsByCwd(targets) {
78
+ return new Map((targets ?? []).map((target) => [target.cwd, target]))
79
+ }
80
+
81
+ /**
82
+ * cwd から一覧の行キー (session_name) を解決する。**行名の正本はここ**。
83
+ *
84
+ * ⚠️ sessionNameFor は `__codex` を付け外しするだけで basename 化しない。cwd を直接
85
+ * 渡すと絶対パスがそのまま名前になり、一覧の行にも通知の宛先にも一致しない。
86
+ *
87
+ * `targetByCwd` は任意。現在 listChatTargets が返す名前は workspace / worktree とも
88
+ * `sanitizeTmuxName(basename(cwd))` と同値なので、渡しても渡さなくても結果は変わらない
89
+ * (この同値は test/chat-targets-naming.test.mjs で固定している)。target 固有の名前を
90
+ * 将来入れるなら、targets を引かない呼び出し元 (state loop の通知) も同時に直すこと。
91
+ *
92
+ * ⚠️ cwd が無い記述子で path.basename に undefined を渡すと throw し、呼び出し元の map ごと
93
+ * 失敗して会話一覧が丸ごと空になる。名前を解決できない 1 本のために全部を失わない。
94
+ */
95
+ export function chatRowSessionName(cwd, cliKind, targetByCwd) {
96
+ const key = typeof cwd === "string" ? cwd : ""
97
+ const dir = targetByCwd?.get(key)?.name ?? (key ? sanitizeTmuxName(path.basename(key)) : "")
98
+ return dir ? sessionNameFor(dir, cliKind) : ""
99
+ }
100
+
76
101
  /** ローカル検出した宛先と、毎回取得するライブ会話を返す。 */
77
102
  export async function buildChatSessionsSnapshot({
78
103
  bridges,
@@ -81,15 +106,11 @@ export async function buildChatSessionsSnapshot({
81
106
  }) {
82
107
  const descriptors = describeLiveSessionsUnion(bridges)
83
108
  const { targets, errors } = await listChatTargets({ logger })
84
- const targetByCwd = new Map(targets.map((target) => [target.cwd, target]))
109
+ const targetByCwd = chatTargetsByCwd(targets)
85
110
  const named = descriptors.map((descriptor) => {
86
111
  const item = { ...descriptor }
87
- // ⚠️ cwd が無い記述子で path.basename に undefined を渡すと throw し、map ごと失敗して
88
- // 会話一覧が丸ごと空になる (呼び出し側の catch が targets:[] live:[] を返す)。名前を
89
- // 解決できない 1 本のために全部を失わない。
90
112
  const cwd = typeof item.cwd === "string" ? item.cwd : ""
91
- const dir = targetByCwd.get(cwd)?.name ?? (cwd ? sanitizeTmuxName(path.basename(cwd)) : "")
92
- return { item, cwd, sessionName: dir ? sessionNameFor(dir, item.kind) : "" }
113
+ return { item, cwd, sessionName: chatRowSessionName(cwd, item.kind, targetByCwd) }
93
114
  })
94
115
 
95
116
  // ⚠️ session_name は dir 名から作るので、別 workspace 配下の同名 worktree は同じ名前へ
package/src/main.mjs CHANGED
@@ -94,7 +94,7 @@ import {
94
94
  } from "./claude-credentials.mjs"
95
95
  import { createWorktreeDir, listWorktreeNameHistory, removeWorktree as removeWorktreeDir } from "./tmux.mjs"
96
96
  import { expandTilde, prepareChatWorkspace, isCwdUnavailableError } from "./chat-workspace.mjs"
97
- import { buildChatSessionsSnapshot, refreshChatSessionsSignals } from "./chat-sessions-snapshot.mjs"
97
+ import { buildChatSessionsSnapshot, chatRowSessionName, refreshChatSessionsSignals } from "./chat-sessions-snapshot.mjs"
98
98
  import { chatTargetsCache } from "./chat-targets.mjs"
99
99
  import { ensureChatSession } from "./chat-session-starter.mjs"
100
100
  import { beginRemoval } from "./chat-removal-guard.mjs"
@@ -1881,12 +1881,17 @@ export function filterCodexSessionsByAuthority(codexSessions, producerByCwd, seq
1881
1881
  *
1882
1882
  * @returns {{ev: string, at: number}|null}
1883
1883
  */
1884
- export function chatSessionEventForTick({ chat, prevEv, prevTurnAt, directAt }) {
1885
- const ev = chat.inputPending
1884
+ /** 現在のチャット状態から境界イベント名を導く。初回観測の基準値もここから取る。 */
1885
+ export function deriveChatEvent(chat) {
1886
+ return chat.inputPending
1886
1887
  ? "input_required"
1887
1888
  : chat.status === "processing"
1888
1889
  ? "prompt_submit"
1889
1890
  : "stop"
1891
+ }
1892
+
1893
+ export function chatSessionEventForTick({ chat, prevEv, prevTurnAt, directAt }) {
1894
+ const ev = deriveChatEvent(chat)
1890
1895
  if (!(chat.turnAt > prevTurnAt && ev !== prevEv)) return null
1891
1896
  const at = ev === "prompt_submit" && typeof directAt === "number" && directAt > 0
1892
1897
  ? directAt
@@ -1966,6 +1971,7 @@ export function startStateLoop({ client, logger, intervalMs, claudeBridge, codex
1966
1971
  // 保護されていて刈れない」tick は 5 秒ごとに同じ行を出し続けるので、状態が変わったときと
1967
1972
  // 一定間隔でだけ記録する(warm-bounds-total.mjs の _shouldLog)。
1968
1973
  const warmBoundsLogState = { lastUnresolved: 0, lastLoggedAtMs: 0 }
1974
+ const previousChatEvents = new Map()
1969
1975
  let stopped = false
1970
1976
 
1971
1977
  const tick = async () => {
@@ -2012,6 +2018,66 @@ export function startStateLoop({ client, logger, intervalMs, claudeBridge, codex
2012
2018
  // 実コンテキスト窓サイズ (1M ベータ等) を反映。contextPctFromUsage の分母を
2013
2019
  // 5s ごとに最新化し、ドーナツが 200k 固定で振り切れるのを防ぐ。
2014
2020
  await refreshContextWindow()
2021
+ // 配送前に停止契約を取り直す。タイマー解除は進行中の tick を止めないので、
2022
+ // await をまたいだ後の観測で通知を出さないのはここだけが担保する。
2023
+ if (stopped) return
2024
+ try {
2025
+ // ⚠️ ここで listChatTargets を引かないこと。TTL は 3 秒 (chat-targets.mjs) で 5 秒 tick では
2026
+ // 毎回失効し、全 worktree に対して `git branch --show-current` をタイムアウト無しで
2027
+ // 直列実行する (tmux.mjs)。無関係な worktree の Git 処理待ちで全会話の通知観測が止まる。
2028
+ // 現在の target 名は workspace / worktree とも sanitizeTmuxName(basename(cwd)) と同値
2029
+ // なので、cwd から直接導出する。この同値は test/chat-targets-naming.test.mjs で固定する。
2030
+ for (const kind of ["claude", "codex"]) {
2031
+ for (const cwd of representativeRows(kind)) {
2032
+ // 観測キーは行名でなく cwd+kind。同名 worktree は 1 行へ畳まれるため、
2033
+ // 行名で観測を共用すると別 cwd の状態が混ざる。
2034
+ const key = `${kind}\u0000${cwd}`
2035
+ const chat = readRowChatState(cwd, kind)
2036
+ const sessionName = chat ? chatRowSessionName(cwd, kind) : ""
2037
+ if (!chat || !sessionName) {
2038
+ previousChatEvents.delete(key)
2039
+ continue
2040
+ }
2041
+ const directAt = getPromptAcceptedAt(cwd, kind)
2042
+ const previous = previousChatEvents.get(key)
2043
+ // 起動時の過去状態は通知せず、次の変化を比較する基準だけを保存する。
2044
+ if (!previous) {
2045
+ previousChatEvents.set(key, { ev: deriveChatEvent(chat), turnAt: chat.turnAt, status: chat.status })
2046
+ continue
2047
+ }
2048
+ const event = chatSessionEventForTick({
2049
+ chat, prevEv: previous.ev, prevTurnAt: previous.turnAt, directAt,
2050
+ })
2051
+ // ⚠️ status は event の有無に関わらず毎 tick 更新する。イベントが出たときだけ
2052
+ // 更新すると、代表の終了が「イベントなし」で素通りしたときに古い status が残る。
2053
+ if (!event) {
2054
+ previousChatEvents.set(key, { ...previous, status: chat.status })
2055
+ continue
2056
+ }
2057
+ // 代表会話が終了して実体が戻ってきた遷移。ターンの完了ではない。
2058
+ const revived = previous.status === null && chat.status !== null
2059
+ previousChatEvents.set(key, { ev: event.ev, turnAt: chat.turnAt, status: chat.status })
2060
+ // ⚠️ 行は代表会話が終了しても消えず、status だけが null になる。これを "完了" と
2061
+ // して送ると、処理中の会話を閉じただけで完了通知が出る。復帰も同じで、終了中に
2062
+ // 承認待ちを送った後に別の会話が waiting で代表になると、ev が
2063
+ // input_required → stop と変わって偽の完了になる。
2064
+ // 一方 inputPending は同 cwd の生存会話すべてから集約されるので、代表終了後に
2065
+ // 背景の会話が承認待ちになったときは送る必要がある。stop だけを絞る。
2066
+ // prompt_submit はこの経路の対象外 (確定仕様として stop / input_required のみ通知する)。
2067
+ const deliverable = event.ev === "input_required"
2068
+ || (event.ev === "stop" && chat.status !== null && !revived)
2069
+ if (!deliverable) continue
2070
+ // ⚠️ chat の読み取りから publish まで await を挟まない。
2071
+ // 既知の取りこぼし: 前回 tick も今回 tick も waiting で、その間にターンが
2072
+ // 開始・完了すると ev が stop のまま変わらないため、通知は遅延ではなく欠落する。
2073
+ // 5 秒 tick を前提にした仕様。
2074
+ publishSessionEvent({ client, logger }, sessionName, event.ev, event.at, { arm: false })
2075
+ .catch(err => logger?.warn({ err: err?.message }, "chat session.event publish failed"))
2076
+ }
2077
+ }
2078
+ } catch (err) {
2079
+ logger?.warn({ err: err?.message }, "chat session.event tick failed")
2080
+ }
2015
2081
  } catch (err) {
2016
2082
  logger?.warn({ err: err.message }, "state loop tick failed")
2017
2083
  }