@cocorograph/hub-agent 0.6.5 → 0.6.6

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.5",
3
+ "version": "0.6.6",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -55,6 +55,7 @@ class ClaudeStreamSession {
55
55
  onPermission,
56
56
  onExit,
57
57
  onError,
58
+ onReap,
58
59
  }) {
59
60
  this.stream_id = stream_id
60
61
  this.cwd = cwd
@@ -66,6 +67,8 @@ class ClaudeStreamSession {
66
67
  this.onPermission = onPermission
67
68
  this.onExit = onExit
68
69
  this.onError = onError
70
+ /** ターン完走後に遅延クローズする際、manager にセッション撤去を依頼するコールバック */
71
+ this.onReap = onReap
69
72
 
70
73
  /** 次の query() で resume に使う session_id。各ターンの system/init で更新。 */
71
74
  this.sessionId = resumeSessionId || null
@@ -76,6 +79,10 @@ class ClaudeStreamSession {
76
79
  this._busy = false
77
80
  /** detach 済みフラグ (新規ターン受付停止) */
78
81
  this._closed = false
82
+ /** 実行中ターンの完走を待ってからクローズする予約フラグ (graceful detach 用)。
83
+ * 端末スリープ / ネット断で browser が切れても、明示的な中断 (interrupt) が
84
+ * 無い限りターンを落とさず最後まで走らせるために使う。 */
85
+ this._reapAfterTurn = false
79
86
  /** 現在ターンの AbortController (interrupt 用) */
80
87
  this._abortController = null
81
88
 
@@ -243,6 +250,19 @@ class ClaudeStreamSession {
243
250
  if (aborted) {
244
251
  this.logger?.info({ stream_id: this.stream_id }, "claude turn aborted")
245
252
  }
253
+ // graceful detach: browser が切れている間にターンが完走したら、ここで遅延
254
+ // クローズする。manager 側で sessions Map から撤去 + exit を emit する。
255
+ if (this._reapAfterTurn && !this._closed) {
256
+ this.logger?.info(
257
+ { stream_id: this.stream_id },
258
+ "claude turn finished after detach, reaping session",
259
+ )
260
+ try {
261
+ this.close()
262
+ } finally {
263
+ this.onReap?.()
264
+ }
265
+ }
246
266
  }
247
267
  }
248
268
 
@@ -257,6 +277,20 @@ class ClaudeStreamSession {
257
277
  }
258
278
  }
259
279
 
280
+ /**
281
+ * graceful detach: 実行中ターンがあれば中断せず完走を待つ (完走後に finally で
282
+ * 自動クローズ + onReap)。アイドルなら即クローズする。
283
+ * @returns {boolean} 即時クローズしたら true / 完走待ちにしたら false
284
+ */
285
+ gracefulClose() {
286
+ if (this._busy) {
287
+ this._reapAfterTurn = true
288
+ return false
289
+ }
290
+ this.close()
291
+ return true
292
+ }
293
+
260
294
  /** セッション終了。新規ターンを止め、実行中なら中断し、watcher も停止する。 */
261
295
  close() {
262
296
  this._closed = true
@@ -332,6 +366,18 @@ export class ClaudeStreamBridge extends EventEmitter {
332
366
  onError: (err) => {
333
367
  this.emit("error", { stream_id, error: err?.message || String(err) })
334
368
  },
369
+ onReap: () => {
370
+ // ターン完走後の遅延クローズ。Map から撤去し exit を emit する。
371
+ if (this.sessions.get(stream_id) === session) {
372
+ this.sessions.delete(stream_id)
373
+ }
374
+ this.emit("exit", {
375
+ stream_id,
376
+ code: 0,
377
+ reason: "detached-after-turn",
378
+ session_id: session.sessionId,
379
+ })
380
+ },
335
381
  })
336
382
  this.sessions.set(stream_id, session)
337
383
  this.logger?.info(
@@ -381,20 +427,31 @@ export class ClaudeStreamBridge extends EventEmitter {
381
427
  return true
382
428
  }
383
429
 
384
- /** セッション停止。Map から即時削除し、実行中ターンを中断する。 */
430
+ /**
431
+ * セッション停止 (graceful)。実行中ターンは中断せず完走させ、完走後に
432
+ * onReap で Map から撤去する。アイドルなら即時撤去。
433
+ * 端末スリープ / ネット断による browser 切断でもターンを落とさないための入口。
434
+ * 明示的に止めたい場合は interrupt() を使う。
435
+ */
385
436
  detach({ stream_id }) {
386
437
  const s = this.sessions.get(stream_id)
387
438
  if (!s) return false
388
- s.close()
389
- this.sessions.delete(stream_id)
390
- this.emit("exit", { stream_id, code: 0, reason: "detached", session_id: s.sessionId })
439
+ const reaped = s.gracefulClose()
440
+ if (reaped) {
441
+ this.sessions.delete(stream_id)
442
+ this.emit("exit", { stream_id, code: 0, reason: "detached", session_id: s.sessionId })
443
+ }
391
444
  return true
392
445
  }
393
446
 
394
- /** 全セッションを停止 (agent shutdown )。 */
447
+ /** 全セッションを強制停止 (agent shutdown 用。実行中ターンも中断する)。 */
395
448
  shutdown() {
396
449
  for (const stream_id of Array.from(this.sessions.keys())) {
397
- this.detach({ stream_id })
450
+ const s = this.sessions.get(stream_id)
451
+ if (!s) continue
452
+ s.close()
453
+ this.sessions.delete(stream_id)
454
+ this.emit("exit", { stream_id, code: 0, reason: "shutdown", session_id: s.sessionId })
398
455
  }
399
456
  }
400
457