@cocorograph/hub-agent 0.6.19 → 0.6.20
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
|
@@ -389,12 +389,17 @@ class ClaudeStreamSession {
|
|
|
389
389
|
// 走行中ターンは完走を待ち TTL 後に再チェック (走行中は絶対に撤去しない)
|
|
390
390
|
if (this._busy) {
|
|
391
391
|
this._idleTimer = setTimeout(tick, ttlMs)
|
|
392
|
+
// idle TTL は最大 7 日。タイマー単独でイベントループを生かさない (unref)。
|
|
393
|
+
// 本番デーモンは WS 接続等で常駐するので reap は問題なく発火し、テスト/CLI
|
|
394
|
+
// など他に handle が無いプロセスは即座に終了できる。
|
|
395
|
+
this._idleTimer.unref?.()
|
|
392
396
|
return
|
|
393
397
|
}
|
|
394
398
|
this._idleTimer = null
|
|
395
399
|
onTimeout?.()
|
|
396
400
|
}
|
|
397
401
|
this._idleTimer = setTimeout(tick, ttlMs)
|
|
402
|
+
this._idleTimer.unref?.()
|
|
398
403
|
}
|
|
399
404
|
|
|
400
405
|
/**
|
package/src/service-install.mjs
CHANGED
|
@@ -128,6 +128,67 @@ function run(cmd, args, opts = {}) {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* macOS の `launchctl bootout` は非同期で、graceful shutdown を持つ hub-agent は
|
|
135
|
+
* 実際に launchd の登録から消えるまでに時間がかかる。直後に `bootstrap` を叩くと
|
|
136
|
+
* 旧サービスがまだ抜け切っておらず `Bootstrap failed: 5: Input/output error` 等で
|
|
137
|
+
* 失敗し、plist はディスクにあるのにロードされない (= KeepAlive 不発 → offline)
|
|
138
|
+
* 状態に陥る。本関数は対象 service が launchd の登録から消えるまで polling して待つ。
|
|
139
|
+
* (2026-05-30: install-service の bootout→bootstrap レースで offline 化する事故の修正)
|
|
140
|
+
*
|
|
141
|
+
* @param {number} uid
|
|
142
|
+
* @param {{ timeoutMs?: number, intervalMs?: number }} [opts]
|
|
143
|
+
* @returns {Promise<boolean>} 消えたら true / timeout なら false
|
|
144
|
+
*/
|
|
145
|
+
async function waitUntilBootedOut(uid, opts = {}) {
|
|
146
|
+
const timeoutMs = opts.timeoutMs ?? 5000
|
|
147
|
+
const intervalMs = opts.intervalMs ?? 200
|
|
148
|
+
const spawn = opts.spawn ?? spawnSync
|
|
149
|
+
const sleepFn = opts.sleep ?? sleep
|
|
150
|
+
const now = opts.now ?? (() => Date.now())
|
|
151
|
+
const deadline = now() + timeoutMs
|
|
152
|
+
while (now() < deadline) {
|
|
153
|
+
const r = spawn("launchctl", ["print", `gui/${uid}/${PLIST_LABEL}`], {
|
|
154
|
+
stdio: "ignore",
|
|
155
|
+
})
|
|
156
|
+
// 非 0 = 登録に存在しない = bootout 完了
|
|
157
|
+
if (r.status !== 0) return true
|
|
158
|
+
await sleepFn(intervalMs)
|
|
159
|
+
}
|
|
160
|
+
return false
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* bootout 後の bootstrap を、過渡的失敗 (旧サービスの抜け残り等) に対して
|
|
165
|
+
* リトライする。`run` と違い最終的に失敗したときだけ throw する。
|
|
166
|
+
*
|
|
167
|
+
* @param {number} uid
|
|
168
|
+
* @param {string} dest plist パス
|
|
169
|
+
* @param {{ attempts?: number, intervalMs?: number }} [opts]
|
|
170
|
+
*/
|
|
171
|
+
async function bootstrapWithRetry(uid, dest, opts = {}) {
|
|
172
|
+
const attempts = opts.attempts ?? 4
|
|
173
|
+
const intervalMs = opts.intervalMs ?? 400
|
|
174
|
+
const spawn = opts.spawn ?? spawnSync
|
|
175
|
+
const sleepFn = opts.sleep ?? sleep
|
|
176
|
+
let lastStatus = null
|
|
177
|
+
for (let i = 0; i < attempts; i++) {
|
|
178
|
+
const r = spawn("launchctl", ["bootstrap", `gui/${uid}`, dest], {
|
|
179
|
+
stdio: "inherit",
|
|
180
|
+
})
|
|
181
|
+
if (r.status === 0) return
|
|
182
|
+
// 既にロード済み (status 37 / "service already loaded") は成功とみなす
|
|
183
|
+
if (r.status === 37) return
|
|
184
|
+
lastStatus = r.status
|
|
185
|
+
await sleepFn(intervalMs)
|
|
186
|
+
}
|
|
187
|
+
throw new Error(
|
|
188
|
+
`launchctl bootstrap gui/${uid} ${dest} failed after ${attempts} attempts (last exit ${lastStatus})`
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
131
192
|
/**
|
|
132
193
|
* `which hub-agent` の結果を、launchd / systemd から長期間 exec 可能な
|
|
133
194
|
* 安定パスに正規化する。pure 関数。`opts` で fs を差し替えてテスト可能。
|
|
@@ -200,10 +261,17 @@ export async function installService({ bin } = {}) {
|
|
|
200
261
|
await fs.writeFile(dest, expanded, { mode: 0o644 })
|
|
201
262
|
|
|
202
263
|
const uid = ensureUnixUid()
|
|
203
|
-
// 既存ロード解除 → bootstrap → kickstart
|
|
264
|
+
// 既存ロード解除 → (登録が消えるまで待機) → bootstrap (リトライ) → kickstart。
|
|
265
|
+
// bootout は非同期なので待たずに bootstrap するとレースで失敗し offline 化する
|
|
266
|
+
// (2026-05-30 の事故)。waitUntilBootedOut で確実に抜けてから bootstrap する。
|
|
204
267
|
spawnSync("launchctl", ["bootout", `gui/${uid}`, dest], { stdio: "ignore" })
|
|
205
|
-
|
|
206
|
-
|
|
268
|
+
await waitUntilBootedOut(uid)
|
|
269
|
+
await bootstrapWithRetry(uid, dest)
|
|
270
|
+
// kickstart は「既に走っている daemon を再起動」する用途。bootstrap + RunAtLoad で
|
|
271
|
+
// 既に起動済みのことがあり、その場合 kickstart 失敗は致命的でないので throw しない。
|
|
272
|
+
spawnSync("launchctl", ["kickstart", "-k", `gui/${uid}/${PLIST_LABEL}`], {
|
|
273
|
+
stdio: "ignore",
|
|
274
|
+
})
|
|
207
275
|
return { platform: "darwin", path: dest, label: PLIST_LABEL, bin: hubAgentBin }
|
|
208
276
|
}
|
|
209
277
|
|
|
@@ -300,4 +368,6 @@ export const _internal = {
|
|
|
300
368
|
macPlistPath,
|
|
301
369
|
linuxUnitPath,
|
|
302
370
|
repoTemplatesDir,
|
|
371
|
+
waitUntilBootedOut,
|
|
372
|
+
bootstrapWithRetry,
|
|
303
373
|
}
|