@cocorograph/hub-agent 0.8.103 → 0.8.105

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.103",
3
+ "version": "0.8.105",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -51,8 +51,8 @@
51
51
  * session 起動を止めない(ensureClaudeMd と同じく best-effort)。
52
52
  *
53
53
  * 関連: backend Stage 1 / 1.5
54
- * GET /api/admin/project/claude-md-by-dir/?dir_name=...
55
- * PUT /api/admin/project/claude-md-by-dir/?dir_name=... body: { claude_md, expected_hash? }
54
+ * GET /api/user/project/claude-md-by-dir/?dir_name=...
55
+ * PUT /api/user/project/claude-md-by-dir/?dir_name=... body: { claude_md, expected_hash? }
56
56
  * expected_hash は楽観ロック。GET で読んだ hash を必ず添えて送る(不一致なら 409)。
57
57
  * **空文字も有効なトークン**(「Hub が空である」ことを期待する)。falsy 判定で
58
58
  * 落とすとキーごと消えて照合されず、盲目的な上書きになる。
@@ -106,7 +106,7 @@ function cliKindForConfig(configFilename) {
106
106
  */
107
107
  function claudeMdByDirUrl({ hubUrl, dirName, cliKind }) {
108
108
  const base =
109
- `${hubUrl.replace(/\/+$/, "")}/api/admin/project/claude-md-by-dir/` +
109
+ `${hubUrl.replace(/\/+$/, "")}/api/user/project/claude-md-by-dir/` +
110
110
  `?dir_name=${encodeURIComponent(dirName)}`
111
111
  return cliKind ? `${base}&cli_kind=${encodeURIComponent(cliKind)}` : base
112
112
  }
@@ -49,7 +49,9 @@ export class CodexAppServerClient extends EventEmitter {
49
49
  this._requestTimeoutMs = resolveRequestTimeoutMs(requestTimeoutMs)
50
50
  this.child = null
51
51
  this.closed = false
52
- this._buf = ""
52
+ // 改行待ちの受信断片 (Buffer のまま保持する)。_onStdout の説明を参照。
53
+ this._pendingChunks = []
54
+ this._pendingNeedsRescan = false
53
55
  this._reqId = 0
54
56
  this._pending = new Map()
55
57
  }
@@ -143,24 +145,59 @@ export class CodexAppServerClient extends EventEmitter {
143
145
  })
144
146
  }
145
147
 
148
+ /**
149
+ * stdout の受信断片を改行区切りの JSON-RPC 行へ組み立てる。
150
+ *
151
+ * - 改行は**新しく届いた断片の中だけ**探す。以前は断片のたびに蓄積済み文字列全体を先頭から
152
+ * 探し直しており、thread/resume・thread/read の応答 (履歴全体を 1 行で返す。実測で最大 55MB)
153
+ * では断片数 × 蓄積量の二乗になって、55MB の応答 1 本に約 4.4 秒かかっていた (まとめて
154
+ * 解析すれば約 60ms)。
155
+ * - 文字列化は**行が揃ってから 1 回だけ**行う。断片ごとに `toString("utf8")` すると、断片の
156
+ * 境目にまたがった日本語等のマルチバイト文字が U+FFFD (「�」) に化ける。改行 (0x0A) は
157
+ * UTF-8 のマルチバイト列の途中には現れないので、改行位置で Buffer を切れば文字は割れない。
158
+ */
146
159
  _onStdout(chunk) {
147
- this._buf += chunk.toString("utf8")
160
+ let buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8")
161
+ if (this._pendingNeedsRescan) {
162
+ // 前回 listener の例外で中断した。持ち越し分に完結した行が残っているので先頭から探し直す。
163
+ buf = Buffer.concat([...this._pendingChunks, buf])
164
+ this._pendingChunks = []
165
+ this._pendingNeedsRescan = false
166
+ }
167
+ let start = 0
148
168
  let idx
149
- while ((idx = this._buf.indexOf("\n")) >= 0) {
150
- const line = this._buf.slice(0, idx)
151
- this._buf = this._buf.slice(idx + 1)
152
- if (!line.trim()) continue
153
- let msg
154
- try {
155
- msg = JSON.parse(line)
156
- } catch (err) {
157
- this.logger?.warn?.(
158
- { err: err.message, line: line.slice(0, 500) },
159
- "codex app-server: unparsable line (skipped)",
160
- )
161
- continue
169
+ let completed = false
170
+ // listener が例外を投げても、この断片の未処理分を失わない (旧実装と同じく次回へ持ち越す)。
171
+ try {
172
+ while ((idx = buf.indexOf(0x0a, start)) >= 0) {
173
+ const tail = buf.subarray(start, idx)
174
+ const line = (
175
+ this._pendingChunks.length
176
+ ? Buffer.concat([...this._pendingChunks, tail])
177
+ : tail
178
+ ).toString("utf8")
179
+ this._pendingChunks = []
180
+ start = idx + 1
181
+ if (!line.trim()) continue
182
+ let msg
183
+ try {
184
+ msg = JSON.parse(line)
185
+ } catch (err) {
186
+ this.logger?.warn?.(
187
+ { err: err.message, line: line.slice(0, 500) },
188
+ "codex app-server: unparsable line (skipped)",
189
+ )
190
+ continue
191
+ }
192
+ this._dispatch(msg)
193
+ }
194
+ completed = true
195
+ } finally {
196
+ if (start < buf.length) {
197
+ this._pendingChunks.push(buf.subarray(start))
198
+ // 正常終了なら残りに改行は無い。例外で抜けたときだけ残りに完結行があり得る。
199
+ if (!completed) this._pendingNeedsRescan = true
162
200
  }
163
- this._dispatch(msg)
164
201
  }
165
202
  }
166
203