@cocorograph/hub-agent 0.6.78 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.6.78",
3
+ "version": "0.6.81",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -237,15 +237,33 @@ function Set-Systemd($distro) {
237
237
  }
238
238
  }
239
239
 
240
- # Register a logon task that wakes the distro (its systemd starts hub-agent).
240
+ # Register a logon task that keeps the distro ALIVE (so its systemd --user
241
+ # hub-agent stays online).
242
+ #
243
+ # 旧実装は `wsl ... /bin/true` で distro を一瞬起こすだけだった。これだと WSL2 の
244
+ # アイドルシャットダウンが数十秒で distro ごと畳んでしまい (agent.log に
245
+ # `SIGTERM shutting down` が出て `wsl -l -v` が Stopped になる)、hub-agent も停止して
246
+ # オフラインに戻る。systemd --user + linger は「distro が起動している間」しか効かない
247
+ # ため、distro 自体を起こし続ける常駐プロセスが必要。
248
+ #
249
+ # 対策: ログオン中ずっと `sleep infinity` を WSL 内で走らせて distro を起動状態に保つ。
250
+ # 併せて hub-agent.service を start しておく (enable + linger 済みなら冪等)。
241
251
  function Register-BootTask($distro, $user) {
242
- Write-Step "Registering autostart on logon (Task Scheduler)"
243
- $action = New-ScheduledTaskAction -Execute "wsl.exe" -Argument "-d $distro -u $user -- /bin/true"
252
+ Write-Step "Registering autostart on logon (Task Scheduler, keep-alive)"
253
+ # distro を起動し続ける keep-alive。systemctl start は冪等 (既に active なら no-op)。
254
+ $keepalive = "systemctl --user start hub-agent.service 2>/dev/null; exec sleep infinity"
255
+ $action = New-ScheduledTaskAction -Execute "wsl.exe" `
256
+ -Argument "-d $distro -u $user -- bash -lc `"$keepalive`""
244
257
  $trigger = New-ScheduledTaskTrigger -AtLogOn
245
- $set = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
258
+ # ExecutionTimeLimit=0 で無制限 (既定 72h で停止されると distro が落ちる)。
259
+ # keep-alive が万一終了したら 1 分後に最大 3 回まで再起動。多重起動は抑止。
260
+ $set = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
261
+ -StartWhenAvailable -MultipleInstances IgnoreNew `
262
+ -RestartInterval (New-TimeSpan -Minutes 1) -RestartCount 3 `
263
+ -ExecutionTimeLimit (New-TimeSpan -Seconds 0)
246
264
  Register-ScheduledTask -TaskName $TASK_NAME -Action $action -Trigger $trigger -Settings $set `
247
265
  -RunLevel Limited -Force | Out-Null
248
- Write-Ok "Logon task '$TASK_NAME' registered"
266
+ Write-Ok "Logon task '$TASK_NAME' registered (keeps WSL distro alive)"
249
267
  }
250
268
 
251
269
  # Run the existing install.sh inside WSL.
@@ -315,6 +333,16 @@ function Main {
315
333
  # --- autostart ---
316
334
  Register-BootTask $distro $user
317
335
 
336
+ # セットアップ完了時点でオンライン化まで完結させる: keep-alive タスクを今すぐ起動し、
337
+ # distro を起動状態に保つ (再ログオンを待たずに hub-agent が常駐する)。
338
+ Write-Step "Starting keep-alive now (online without re-logon)"
339
+ try {
340
+ Start-ScheduledTask -TaskName $TASK_NAME
341
+ Write-Ok "Keep-alive started; hub-agent should come online shortly"
342
+ } catch {
343
+ Write-Warn "Start-ScheduledTask failed ($($_.Exception.Message)); ログオンし直すと起動します"
344
+ }
345
+
318
346
  Write-Host ""
319
347
  Write-Ok "Setup complete. Check the online status in the Hub UI:"
320
348
  Write-Host " https://hub.cocorograph.com/user/cockpit/agents" -ForegroundColor Cyan
package/src/main.mjs CHANGED
@@ -25,6 +25,7 @@ import { WsClient } from "./ws-client.mjs"
25
25
  import { PtyBridge } from "./pty-bridge.mjs"
26
26
  import { ClaudeStreamBridge } from "./claude-stream-bridge.mjs"
27
27
  import { UploadManager } from "./claude-upload.mjs"
28
+ import { requestSelfUninstall } from "./service-install.mjs"
28
29
  import {
29
30
  decideSessionRotation,
30
31
  fetchSessionHistory,
@@ -656,6 +657,8 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
656
657
  }
657
658
  process.on("SIGINT", () => shutdown("SIGINT"))
658
659
  process.on("SIGTERM", () => shutdown("SIGTERM"))
660
+ // dispatch から呼べるように shutdown を ctx に載せる (agent.shutdown 用)。
661
+ ctx.requestShutdown = shutdown
659
662
 
660
663
  // 0.6.2 fix: 例外で silent 終了しないよう最後のセーフティネット。
661
664
  // Node 24 で unhandledRejection は default で process を kill する仕様のため、
@@ -1136,6 +1139,21 @@ async function dispatch(msg, ctx) {
1136
1139
  case "error":
1137
1140
  ctx.logger.warn({ msg }, "hub error")
1138
1141
  return
1142
+ case "agent.shutdown": {
1143
+ // Cockpit の「エージェント削除 (ゴミ箱)」由来。この端末をエージェントとして
1144
+ // 撤去する: サービス停止・無効化・autostart 撤去のみ。ユーザーデータ
1145
+ // (~/.hub / ~/.claude / プロジェクト) には触れない。
1146
+ ctx.logger.info({ msg }, "agent.shutdown received — self-uninstalling")
1147
+ try {
1148
+ // cgroup の外で動く detached プロセスにクリーンアップを委譲してから exit。
1149
+ requestSelfUninstall()
1150
+ } catch (err) {
1151
+ ctx.logger.warn({ err: err?.message }, "requestSelfUninstall failed")
1152
+ }
1153
+ // 本体は即停止 (detached cleanup が disable/stop を完走させる)。
1154
+ await ctx.requestShutdown?.("agent.shutdown")
1155
+ return
1156
+ }
1139
1157
  case "pty.attach": {
1140
1158
  const stream_id = msg.stream_id
1141
1159
  try {
@@ -19,6 +19,9 @@ import { readConfig } from "./config.mjs"
19
19
 
20
20
  const PLIST_LABEL = "co.cocorograph.hub-agent"
21
21
  const SYSTEMD_UNIT_NAME = "hub-agent.service"
22
+ // Windows(WSL) のログオン keep-alive タスク名 (install.ps1 の $TASK_NAME と一致)。
23
+ // WSL からは Windows 連携 (interop) で schtasks.exe を叩いて削除できる。
24
+ const WSL_BOOT_TASK_NAME = "HubAgentWSLBoot"
22
25
 
23
26
  function repoTemplatesDir() {
24
27
  // src/service-install.mjs → ../templates
@@ -323,7 +326,14 @@ export async function installService({ bin } = {}) {
323
326
  // 無い間 (= WSL2 のログオンタスク経由の boot や headless) に常駐しない。
324
327
  const linger = enableLinger()
325
328
  run("systemctl", ["--user", "daemon-reload"])
326
- run("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT_NAME])
329
+ run("systemctl", ["--user", "enable", SYSTEMD_UNIT_NAME])
330
+ // `enable --now` は既に起動中のサービスを再起動しない (--now は停止中なら start、
331
+ // 起動中なら no-op)。distro を起動し続ける keep-alive と組み合わさると、再 enroll で
332
+ // agent.json のトークンを書き換えても古いプロセスが旧トークンのまま生き続け、
333
+ // (同一ホスト重複解消で旧 agent 行が消えるため) WS 403 でオフラインになる。
334
+ // restart は停止中なら起動・起動中なら再起動するので、install-service のたびに
335
+ // 必ず最新の agent.json でプロセスを入れ替える。
336
+ run("systemctl", ["--user", "restart", SYSTEMD_UNIT_NAME])
327
337
  return {
328
338
  platform: "linux",
329
339
  path: dest,
@@ -409,6 +419,75 @@ export async function uninstallService() {
409
419
  throw new Error(`unsupported platform: ${process.platform}`)
410
420
  }
411
421
 
422
+ /**
423
+ * Cockpit の「エージェント削除 (ゴミ箱)」を受けて、この端末を **エージェントとして
424
+ * 撤去** する。サービス停止・無効化・autostart 撤去のみを行い、ユーザーデータ
425
+ * (`~/.hub` / `~/.claude` / プロジェクト) には一切触れない。
426
+ *
427
+ * 重要: hub-agent.service は `Restart=always`。自プロセスから `systemctl --user
428
+ * stop` すると自分の cgroup ごと殺され、後続のクリーンアップが完走しない。そこで
429
+ * **サービスの cgroup の外** で動く detached プロセスにクリーンアップを委譲し、
430
+ * 本体はすぐ exit する (呼び出し側が exit する)。
431
+ *
432
+ * 撤去内容:
433
+ * - systemd user unit: disable --now + unit ファイル削除 + daemon-reload
434
+ * - linger: loginctl disable-linger
435
+ * - Windows(WSL): schtasks.exe で keep-alive タスク削除 (interop。非WSLでは no-op)
436
+ *
437
+ * @param {{spawnSync?: typeof spawnSync}} [opts] テスト用に spawn を注入可能。
438
+ * @returns {boolean} クリーンアップ用 detached プロセスの起動に成功したら true。
439
+ */
440
+ export function requestSelfUninstall(opts = {}) {
441
+ const spawn = opts.spawnSync ?? spawnSync
442
+
443
+ if (process.platform === "linux") {
444
+ const user = currentUsername()
445
+ const unit = linuxUnitPath()
446
+ // 1 秒待ってから (本体 exit を待つ) 撤去。各手順は失敗しても続行 (|| true)。
447
+ const script = [
448
+ "sleep 1",
449
+ `systemctl --user disable --now ${SYSTEMD_UNIT_NAME} 2>/dev/null || true`,
450
+ `rm -f '${unit}' 2>/dev/null || true`,
451
+ "systemctl --user daemon-reload 2>/dev/null || true",
452
+ `loginctl disable-linger ${user} 2>/dev/null || true`,
453
+ // WSL のみ有効 (Windows interop)。schtasks.exe が無い環境では黙って失敗。
454
+ `schtasks.exe /delete /tn ${WSL_BOOT_TASK_NAME} /f >/dev/null 2>&1 || true`,
455
+ ].join("; ")
456
+
457
+ // systemd-run --user --scope で別 scope(=別 cgroup) に逃がす。これにより
458
+ // hub-agent.service を stop しても掃除プロセスは生き残って完走する。
459
+ const r = spawn(
460
+ "systemd-run",
461
+ ["--user", "--scope", "--collect", "--quiet", "bash", "-lc", script],
462
+ { stdio: "ignore", detached: true },
463
+ )
464
+ if (!r.error && r.status === 0) return true
465
+ // systemd-run が無い/失敗時は setsid で cgroup から外して代替。
466
+ const r2 = spawn("setsid", ["bash", "-lc", script], {
467
+ stdio: "ignore",
468
+ detached: true,
469
+ })
470
+ return !r2.error
471
+ }
472
+
473
+ if (process.platform === "darwin") {
474
+ const uid = ensureUnixUid()
475
+ const dest = macPlistPath()
476
+ // launchctl bootout は job の全プロセスを殺すため、setsid で job から切り離した
477
+ // detached プロセスに委譲する。
478
+ const script =
479
+ `sleep 1; launchctl bootout gui/${uid} '${dest}' 2>/dev/null || true; ` +
480
+ `rm -f '${dest}' 2>/dev/null || true`
481
+ const r = spawn("bash", ["-lc", `setsid bash -lc "${script}" >/dev/null 2>&1 &`], {
482
+ stdio: "ignore",
483
+ detached: true,
484
+ })
485
+ return !r.error
486
+ }
487
+
488
+ return false
489
+ }
490
+
412
491
  export const _internal = {
413
492
  expandTemplate,
414
493
  detectHubAgentBin,
@@ -420,4 +499,5 @@ export const _internal = {
420
499
  bootstrapWithRetry,
421
500
  currentUsername,
422
501
  enableLinger,
502
+ requestSelfUninstall,
423
503
  }