@cocorograph/hub-agent 0.7.79 → 0.7.81

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/src/main.mjs CHANGED
@@ -2064,14 +2064,22 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
2064
2064
  const tick = async () => {
2065
2065
  if (stopped) return
2066
2066
  try {
2067
- // B11: 多端末共有時、クラッシュして detach を送らず消えた死端末を GC する
2067
+ // B11: 多端末共有 (T06542 で常時) で、クラッシュして detach を送らず消えた死端末を GC する
2068
2068
  // (活性なし TTL 超過の購読端末キーを掃除し、最後の 1 台が外れたら idle softDetach へ)。
2069
- // CHAT_SHARED_ENABLED 無効時は no-op。例外は state loop 全体の try/catch が拾う。
2069
+ // 例外は state loop 全体の try/catch が拾う。
2070
2070
  try {
2071
2071
  claudeBridge?.gcDeadTerminals?.()
2072
2072
  } catch (err) {
2073
2073
  logger?.warn({ err: err?.message }, "gcDeadTerminals failed")
2074
2074
  }
2075
+ // 子⑤ (T06544): claude 側 warm 境界(per-cwd=1 / N-cwd LRU)。従来 claude は per-session の
2076
+ // softDetach タイマー(7 日)だけで件数上限が無かったため、多数の cwd を渡り歩くと warm な
2077
+ // SDK 常駐 query が蓄積し得た。codex の reconcileSessions と対で cwd 単位に有界化する。
2078
+ try {
2079
+ claudeBridge?.reconcileWarmBounds?.()
2080
+ } catch (err) {
2081
+ logger?.warn({ err: err?.message }, "claude reconcileWarmBounds failed")
2082
+ }
2075
2083
  // Codex 版 (claude の gcDeadTerminals と対): detach を送らず消えた / turn/completed が
2076
2084
  // 来ず activeTurnId が固着したスタックセッションの app-server を回収する (プロセス
2077
2085
  // リーク・生成中固着の根治)。進行中の正常ターンは app-server 通知で touch され残る。
@@ -2559,6 +2567,13 @@ function makeSeenInputIdRegistry(max = SEEN_INPUT_IDS_MAX) {
2559
2567
  const seenClaudeChatInputIds = makeSeenInputIdRegistry()
2560
2568
  const seenCodexChatInputIds = makeSeenInputIdRegistry()
2561
2569
 
2570
+ // round4 major#1 (T06542): codex.input の **in-flight** dedup 用レジストリ (input_id -> Promise)。
2571
+ // `seen` は settle 後にしか登録されないため、frontend の ack watchdog (5s) が bridge の barrier/RPC
2572
+ // (turn/start→turn/started は数秒〜) より短く、同一 input_id が settle 前に再送されると bridge.input を
2573
+ // 二重に呼んで FIFO へ重複投入され、二重ターン/steer になる。処理中の input_id は再 dispatch せず既存
2574
+ // Promise へ合流させる (settle 時に元の .then/.catch が 1 回だけ ack/nack を出す)。
2575
+ const codexInflightByInputId = new Map()
2576
+
2562
2577
  /**
2563
2578
  * input_id 付き pty.data (tracked 入力) の処理。write 成功で pty.input.ack を返し、
2564
2579
  * stream 不在なら pty.error を返して browser の再 attach → 再送に繋げる。
@@ -2697,13 +2712,19 @@ export function handleCodexChatInput(msg, ctx) {
2697
2712
  return undefined
2698
2713
  }
2699
2714
  if (input_id && seenCodexChatInputIds.has(input_id)) {
2700
- // 再送 (ack 喪失想定)。turn/start・turn/steer を再送出せず ack のみ返す
2715
+ // 再送 (ack 喪失想定 / settle 済み)。turn/start・turn/steer を再送出せず ack のみ返す
2701
2716
  // (二重ターン実行を避ける。claude.input dedup と同じ考え方)。
2702
2717
  ctx.logger?.info?.({ stream_id, input_id }, "codex.input dedup (ack only)")
2703
2718
  ctx.client.send({ type: "codex.input.ack", stream_id, input_id })
2704
2719
  return undefined
2705
2720
  }
2706
- return ctx.codexBridge
2721
+ if (input_id && codexInflightByInputId.has(input_id)) {
2722
+ // round4 major#1: **処理中** の同一 input_id (watchdog の settle 前再送)。再 dispatch せず既存
2723
+ // Promise へ合流する (元の .then/.catch が settle 時に 1 回だけ ack/nack を出すので二重投入しない)。
2724
+ ctx.logger?.info?.({ stream_id, input_id }, "codex.input in-flight dedup (join, no re-dispatch)")
2725
+ return codexInflightByInputId.get(input_id)
2726
+ }
2727
+ const p = ctx.codexBridge
2707
2728
  .input({
2708
2729
  stream_id,
2709
2730
  message: msg.message,
@@ -2735,6 +2756,12 @@ export function handleCodexChatInput(msg, ctx) {
2735
2756
  ...(err.reason ? { reason: err.reason } : {}),
2736
2757
  })
2737
2758
  })
2759
+ .finally(() => {
2760
+ // in-flight レジストリから外す (成功時は seen が以降の再送を dedup、失敗時は再送で正当にリトライ)。
2761
+ if (input_id) codexInflightByInputId.delete(input_id)
2762
+ })
2763
+ if (input_id) codexInflightByInputId.set(input_id, p)
2764
+ return p
2738
2765
  }
2739
2766
 
2740
2767
  /**
@@ -15,8 +15,6 @@ import path from "node:path"
15
15
  import { spawnSync } from "node:child_process"
16
16
  import { fileURLToPath } from "node:url"
17
17
 
18
- import { readConfig } from "./config.mjs"
19
-
20
18
  const PLIST_LABEL = "co.cocorograph.hub-agent"
21
19
  const SYSTEMD_UNIT_NAME = "hub-agent.service"
22
20
  // Windows(WSL) のログオン keep-alive タスク名 (install.ps1 の $TASK_NAME と一致)。
@@ -55,7 +53,7 @@ function escapeXmlText(s) {
55
53
  .replaceAll(">", ">")
56
54
  }
57
55
 
58
- function expandTemplate(text, hubAgentBin, opts = {}) {
56
+ function expandTemplate(text, hubAgentBin) {
59
57
  // hub-agent デーモンの runtime 環境に必須な追加 env を、各プラットフォームの
60
58
  // 起動ファイル形式 (plist / systemd) に合わせて差し込むためのプレースホルダ展開。
61
59
  //
@@ -78,22 +76,8 @@ function expandTemplate(text, hubAgentBin, opts = {}) {
78
76
  systemdLine = `Environment=NODE_EXTRA_CA_CERTS=${nodeExtraCa}\n`
79
77
  }
80
78
 
81
- // 多端末共有チャット (HUB_AGENT_CHAT_SHARED)。NODE_EXTRA_CA_CERTS と同じ仕組みで
82
- // plist / systemd の EnvironmentVariables に焼き込む。値ソースは (a) installService
83
- // が agent.json.chat_shared を読んで渡す opts.chatShared、または (b) install-service
84
- // 実行時の process.env.HUB_AGENT_CHAT_SHARED。これにより agent 更新で install-service
85
- // が plist を再生成しても、agent.json に chat_shared=true があればフラグが維持される
86
- // (旧実装は手動 plist 追記が再生成で消える事故があった。2026-05-30)。
87
- const chatShared =
88
- opts.chatShared === true || process.env.HUB_AGENT_CHAT_SHARED === "1"
89
- let chatPlistEntry = ""
90
- let chatSystemdLine = ""
91
- if (chatShared) {
92
- chatPlistEntry =
93
- ` <key>HUB_AGENT_CHAT_SHARED</key>\n` +
94
- ` <string>1</string>\n`
95
- chatSystemdLine = `Environment=HUB_AGENT_CHAT_SHARED=1\n`
96
- }
79
+ // T06542: 多端末共有チャットは既定 ON 化しフラグ (HUB_AGENT_CHAT_SHARED) を撤去した。
80
+ // plist/systemd への env 焼き込みも不要になったため、テンプレートの placeholder ごと削除済み。
97
81
 
98
82
  return text
99
83
  .replaceAll("__HUB_AGENT_BIN__", hubAgentBin)
@@ -101,19 +85,6 @@ function expandTemplate(text, hubAgentBin, opts = {}) {
101
85
  .replaceAll("__PATH__", process.env.PATH || "/usr/local/bin:/usr/bin:/bin")
102
86
  .replaceAll("__NODE_EXTRA_CA_CERTS_PLIST_ENTRY__", plistEntry)
103
87
  .replaceAll("__NODE_EXTRA_CA_CERTS_SYSTEMD_LINE__", systemdLine)
104
- .replaceAll("__HUB_AGENT_CHAT_SHARED_PLIST_ENTRY__", chatPlistEntry)
105
- .replaceAll("__HUB_AGENT_CHAT_SHARED_SYSTEMD_LINE__", chatSystemdLine)
106
- }
107
-
108
- /** agent.json から多端末共有フラグ (chat_shared) を読む。読めなければ false。
109
- * install-service 時に plist/systemd へ焼き込む値ソースに使う (env 併用は expandTemplate 側)。 */
110
- async function readChatSharedFromConfig() {
111
- try {
112
- const config = await readConfig()
113
- return config?.chat_shared === true
114
- } catch {
115
- return false
116
- }
117
88
  }
118
89
 
119
90
  async function ensureDir(p) {
@@ -290,12 +261,9 @@ export async function installService({ bin } = {}) {
290
261
  const hubAgentBin = bin || detectHubAgentBin()
291
262
  await ensureLogFile()
292
263
 
293
- // agent.json の永続フラグを読み、テンプレ展開に渡す (再生成でも維持されるように)。
294
- const chatShared = await readChatSharedFromConfig()
295
-
296
264
  if (process.platform === "darwin") {
297
265
  const tpl = await readTemplate("co.cocorograph.hub-agent.plist")
298
- const expanded = expandTemplate(tpl, hubAgentBin, { chatShared })
266
+ const expanded = expandTemplate(tpl, hubAgentBin)
299
267
  const dest = macPlistPath()
300
268
  await ensureDir(path.dirname(dest))
301
269
  await fs.writeFile(dest, expanded, { mode: 0o644 })
@@ -317,7 +285,7 @@ export async function installService({ bin } = {}) {
317
285
 
318
286
  if (process.platform === "linux") {
319
287
  const tpl = await readTemplate("hub-agent.service")
320
- const expanded = expandTemplate(tpl, hubAgentBin, { chatShared })
288
+ const expanded = expandTemplate(tpl, hubAgentBin)
321
289
  const dest = linuxUnitPath()
322
290
  await ensureDir(path.dirname(dest))
323
291
  await fs.writeFile(dest, expanded, { mode: 0o644 })
@@ -52,7 +52,7 @@
52
52
  <string>__PATH__</string>
53
53
  <key>HOME</key>
54
54
  <string>__HOME__</string>
55
- __NODE_EXTRA_CA_CERTS_PLIST_ENTRY____HUB_AGENT_CHAT_SHARED_PLIST_ENTRY__ </dict>
55
+ __NODE_EXTRA_CA_CERTS_PLIST_ENTRY__ </dict>
56
56
 
57
57
  <!-- KeepAlive で過剰再起動した時に 10 秒スロットルする -->
58
58
  <key>ThrottleInterval</key>
@@ -12,7 +12,7 @@ Type=simple
12
12
  # 行に、未 set なら空文字列に」展開する。
13
13
  Environment=PATH=__PATH__
14
14
  Environment=HOME=__HOME__
15
- __NODE_EXTRA_CA_CERTS_SYSTEMD_LINE____HUB_AGENT_CHAT_SHARED_SYSTEMD_LINE__WorkingDirectory=__HOME__
15
+ __NODE_EXTRA_CA_CERTS_SYSTEMD_LINE__WorkingDirectory=__HOME__
16
16
  ExecStart=__HUB_AGENT_BIN__ start
17
17
  Restart=always
18
18
  RestartSec=10