@cocorograph/hub-agent 0.6.25 → 0.6.27

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.25",
3
+ "version": "0.6.27",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -32,7 +32,7 @@
32
32
  "LICENSE"
33
33
  ],
34
34
  "dependencies": {
35
- "@anthropic-ai/claude-agent-sdk": "^0.3.152",
35
+ "@anthropic-ai/claude-agent-sdk": "^0.3.158",
36
36
  "commander": "^12.1.0",
37
37
  "node-pty": "^1.0.0",
38
38
  "pino": "^9.0.0",
@@ -137,6 +137,7 @@ class ClaudeStreamSession {
137
137
  permissionMode,
138
138
  maxTurns,
139
139
  maxThinkingTokens,
140
+ effort,
140
141
  resumeSessionId,
141
142
  resident,
142
143
  sdk,
@@ -158,6 +159,10 @@ class ClaudeStreamSession {
158
159
  this.maxTurns = typeof maxTurns === "number" ? maxTurns : null
159
160
  this.maxThinkingTokens =
160
161
  typeof maxThinkingTokens === "number" ? maxThinkingTokens : null
162
+ // Opus 4.6+ の effort パラメータ (low/medium/high/xhigh/max)。adaptive thinking と
163
+ // 併用して思考の深さを制御する。Opus では budget 方式 (maxThinkingTokens) は廃止
164
+ // 扱いのため、effort モデルでは effort + thinking:{type:'adaptive'} を使う。
165
+ this.effort = effort || null
161
166
  this.sdk = sdk
162
167
  this.logger = logger
163
168
  this.onEvent = onEvent
@@ -195,10 +200,15 @@ class ClaudeStreamSession {
195
200
 
196
201
  /** 改修3: per-message セッションで busy 中に届いた送信を退避する pending キュー。
197
202
  * 常駐 query 化 (改修2) とは別レイヤー。resume チェーンは維持したまま、現ターン
198
- * 完了時 (finally) に先頭から drain して次ターンを自動発火する。 */
203
+ * 完了時 (finally) に先頭から drain して次ターンを自動発火する。
204
+ * キャンセル機能 (0.6.26): 各項目に安定 id を振り、browser から id 指定で
205
+ * 個別削除できるようにする。要素は { id: string, text: string }。 */
199
206
  this._pendingMessages = []
200
- /** 改修3: 直近 browser へ通知した pending 件数 (変化時のみ queue_state を emit する)。 */
201
- this._lastEmittedQueueCount = 0
207
+ /** キャンセル機能: pending 項目へ安定 id を振るための連番カウンタ。 */
208
+ this._queueSeq = 0
209
+ /** 改修3: 直近 browser へ通知した queue 署名 (件数 + id 列)。変化時のみ emit する。
210
+ * 空キューの署名 ("0:") で初期化し、空→空の冗長 emit を抑止する。 */
211
+ this._lastEmittedQueueSig = "0:"
202
212
 
203
213
  /** @type {Map<string, {resolve: (decision: object) => void}>} permission 応答待ち */
204
214
  this._permissionResolvers = new Map()
@@ -321,7 +331,7 @@ class ClaudeStreamSession {
321
331
  * 値が undefined のキーは「変更なし」として無視する (バッジ未送出時に既存値を消さない)。
322
332
  * model に空文字/null が来たら setModel(undefined) でデフォルトへ戻す。
323
333
  * maxThinkingTokens に 0/null が来たら setMaxThinkingTokens(null) でオフにする。 */
324
- applyRuntimeOptions({ model, permissionMode, maxThinkingTokens } = {}) {
334
+ applyRuntimeOptions({ model, permissionMode, maxThinkingTokens, effort } = {}) {
325
335
  const q = this._residentQuery
326
336
  // モデル
327
337
  if (model !== undefined) {
@@ -373,6 +383,34 @@ class ClaudeStreamSession {
373
383
  }
374
384
  }
375
385
  }
386
+ // effort (Opus 4.6+)。SDK には setEffort 相当のランタイム制御メソッドが無いため、
387
+ // 保持値の更新のみ行う。per-message セッションは次ターンの options 構築時に
388
+ // this.effort を読むため即反映される。常駐 query は次 spawn (resume 再起動) 時に
389
+ // 反映される (= 次回セッションから切替。ユーザー要件と一致)。
390
+ if (effort !== undefined) {
391
+ this.effort = effort || null
392
+ }
393
+ }
394
+
395
+ /** モデルが Opus 4.6+ (effort / adaptive thinking 対応) かどうか。
396
+ * budget 方式 (maxThinkingTokens) は Opus 4.7+ で廃止扱いのため、effort モデルでは
397
+ * effort + thinking:{type:'adaptive'} に切り替える。 */
398
+ _isEffortModel() {
399
+ return typeof this.model === "string" && /claude-opus-4-[678]/.test(this.model)
400
+ }
401
+
402
+ /** 思考関連オプション (effort / adaptive thinking / 旧 budget) を options へ適用する。
403
+ * per-message / 常駐 query の両方から呼ぶ共通ロジック (分岐の二重定義を避ける)。 */
404
+ _applyThinkingOptions(options) {
405
+ if (this._isEffortModel()) {
406
+ // Opus: adaptive thinking を明示 ON にし、effort で深さを指定する。
407
+ // budget 方式 (maxThinkingTokens) は使わない (Opus 4.7+ で非対応)。
408
+ options.thinking = { type: "adaptive" }
409
+ if (this.effort) options.effort = this.effort
410
+ } else if (this.maxThinkingTokens != null) {
411
+ // 非 effort モデル (Sonnet / Haiku 等) は従来の budget 方式を維持。
412
+ options.maxThinkingTokens = this.maxThinkingTokens
413
+ }
376
414
  }
377
415
 
378
416
  /** soft detach: browser 切断時にターンを中断せずセッションを生かしたまま detached に
@@ -420,7 +458,7 @@ class ClaudeStreamSession {
420
458
  // 次を push すると SDK streaming-input で割り込み扱いになり得るため (公式 interrupt 警告)。
421
459
  // pending は queue_state で UI (送信待ちチップ) に出す (per-message と同じ体験)。
422
460
  if (this._busy) {
423
- this._pendingMessages.push(prompt)
461
+ this._enqueuePending(prompt)
424
462
  this.logger?.info(
425
463
  { stream_id: this.stream_id, queued: this._pendingMessages.length },
426
464
  "resident busy, message queued",
@@ -444,7 +482,7 @@ class ClaudeStreamSession {
444
482
  // 改修3: busy 中の送信は破棄せず pending キューへ退避し、現ターン完了時に drain する
445
483
  // (ターミナル流の「積む→待機→順次実行」)。常駐 query 化はしないので暴走リスクは増えない。
446
484
  if (this._busy) {
447
- this._pendingMessages.push(prompt)
485
+ this._enqueuePending(prompt)
448
486
  this.logger?.info(
449
487
  { stream_id: this.stream_id, queued: this._pendingMessages.length },
450
488
  "claude busy, message queued",
@@ -470,9 +508,10 @@ class ClaudeStreamSession {
470
508
  }
471
509
  if (this.model) options.model = this.model
472
510
  if (this.permissionMode) options.permissionMode = this.permissionMode
473
- // Phase B: チャット SDK に効くオプション (拡張思考予算 / ツール往復上限)。
511
+ // Phase B: チャット SDK に効くオプション (ツール往復上限)。
474
512
  if (this.maxTurns != null) options.maxTurns = this.maxTurns
475
- if (this.maxThinkingTokens != null) options.maxThinkingTokens = this.maxThinkingTokens
513
+ // 思考オプション (effort / adaptive thinking / 旧 budget) はモデルに応じて切替。
514
+ this._applyThinkingOptions(options)
476
515
  // 直前ターンまでの session_id があれば resume チェーン
477
516
  if (this.sessionId) options.resume = this.sessionId
478
517
 
@@ -573,8 +612,8 @@ class ClaudeStreamSession {
573
612
  return
574
613
  }
575
614
  const next = this._pendingMessages.shift()
576
- this._emitQueueState()
577
- this._runPerMessage(next).catch((err) => {
615
+ this._emitQueueState([next.text])
616
+ this._runPerMessage(next.text).catch((err) => {
578
617
  this.logger?.error(
579
618
  { stream_id: this.stream_id, err: err?.message },
580
619
  "drain runPerMessage threw",
@@ -582,20 +621,53 @@ class ClaudeStreamSession {
582
621
  })
583
622
  }
584
623
 
624
+ /** キャンセル機能 (0.6.26): pending キューへ安定 id 付きで 1 件積む。 */
625
+ _enqueuePending(prompt) {
626
+ const id = `q${++this._queueSeq}`
627
+ this._pendingMessages.push({ id, text: prompt })
628
+ return id
629
+ }
630
+
631
+ /** キャンセル機能 (0.6.26): browser から id 指定で送信待ちメッセージを 1 件取り消す。
632
+ * 実行開始済み (既に drain された) メッセージは pending に残っていないので no-op になる。
633
+ * 削除に成功したら queue_state を再 emit して送信待ちチップを更新する。 */
634
+ cancelQueued(id) {
635
+ const before = this._pendingMessages.length
636
+ this._pendingMessages = this._pendingMessages.filter((m) => m.id !== id)
637
+ const removed = before !== this._pendingMessages.length
638
+ if (removed) {
639
+ this.logger?.info(
640
+ { stream_id: this.stream_id, id, queued: this._pendingMessages.length },
641
+ "queued message canceled",
642
+ )
643
+ this._emitQueueState()
644
+ }
645
+ return removed
646
+ }
647
+
585
648
  /** 改修3: pending キューの現状を browser へ通知する (送信待ちチップ表示用)。
586
- * onEvent 経由で claude.event(event.type="queue_state") として届く。 */
587
- _emitQueueState() {
649
+ * onEvent 経由で claude.event(event.type="queue_state") として届く。
650
+ * @param {string[]} [started] このタイミングで pending から取り出して実行開始した
651
+ * メッセージ本文。drain 由来の emit でのみ渡す。frontend はこれを user バブルへ
652
+ * 昇格させる。キャンセル / 追加由来の emit では空 (昇格させない)。これにより
653
+ * 「先頭の pending をキャンセルした」のを「実行開始した」と誤認しなくなる (0.6.26)。 */
654
+ _emitQueueState(started = []) {
588
655
  const count = this._pendingMessages.length
589
- // 件数が変わらないなら通知しない (空→空の冗長 emit を抑止)。
590
- if (count === this._lastEmittedQueueCount) return
591
- this._lastEmittedQueueCount = count
656
+ // 署名 = 件数 + id 列。件数が同じでもキャンセルで中身が変われば通知する。
657
+ const sig = `${count}:${this._pendingMessages.map((m) => m.id).join(",")}`
658
+ // started があるときは drain なので、sig 変化が無くても (理論上起きないが) 通知する。
659
+ if (started.length === 0 && sig === this._lastEmittedQueueSig) return
660
+ this._lastEmittedQueueSig = sig
592
661
  try {
593
662
  // messages は全文を載せる。frontend は実行開始 (drain) 時にこれを user バブルへ
594
663
  // 昇格させるため、ここで切り詰めると本文が欠ける。チップの省略表示は CSS 側で行う。
664
+ // items は id 付きでキャンセルボタンの対象特定に使う (0.6.26)。
595
665
  this.onEvent?.({
596
666
  type: "queue_state",
597
667
  pending: count,
598
- messages: [...this._pendingMessages],
668
+ messages: this._pendingMessages.map((m) => m.text),
669
+ items: this._pendingMessages.map((m) => ({ id: m.id, text: m.text })),
670
+ started,
599
671
  })
600
672
  } catch {
601
673
  /* ignore */
@@ -623,8 +695,8 @@ class ClaudeStreamSession {
623
695
  }
624
696
  const next = this._pendingMessages.shift()
625
697
  this._busy = true
626
- this._inputQueue.push(toSDKUserMessage(next))
627
- this._emitQueueState()
698
+ this._inputQueue.push(toSDKUserMessage(next.text))
699
+ this._emitQueueState([next.text])
628
700
  }
629
701
 
630
702
  /** 改修2+4: 常駐 query を 1 回だけ起動し、streaming input キューから複数ターンを処理する。
@@ -645,7 +717,7 @@ class ClaudeStreamSession {
645
717
  if (this.model) options.model = this.model
646
718
  if (this.permissionMode) options.permissionMode = this.permissionMode
647
719
  if (this.maxTurns != null) options.maxTurns = this.maxTurns
648
- if (this.maxThinkingTokens != null) options.maxThinkingTokens = this.maxThinkingTokens
720
+ this._applyThinkingOptions(options)
649
721
  // 改修4: 起動時に sessionId (= resumeSessionId) があれば resume チェーンで文脈を引き継ぐ。
650
722
  // query 起動時点の値のみ有効 (起動後に確定/変化する session_id は同一 query 内で継続される)。
651
723
  if (this.sessionId) options.resume = this.sessionId
@@ -712,8 +784,8 @@ class ClaudeStreamSession {
712
784
  const next = this._pendingMessages.shift()
713
785
  this._busy = true
714
786
  this._residentStarted = true
715
- this._inputQueue.push(toSDKUserMessage(next))
716
- this._emitQueueState()
787
+ this._inputQueue.push(toSDKUserMessage(next.text))
788
+ this._emitQueueState([next.text])
717
789
  this._startResidentQuery()
718
790
  }
719
791
  }
@@ -833,6 +905,7 @@ export class ClaudeStreamBridge extends EventEmitter {
833
905
  * permissionMode?: string|null,
834
906
  * maxTurns?: number|null,
835
907
  * maxThinkingTokens?: number|null,
908
+ * effort?: string|null,
836
909
  * resumeSessionId?: string|null,
837
910
  * }} args
838
911
  * @returns {{ stream_id: string, resuming: boolean }}
@@ -844,6 +917,7 @@ export class ClaudeStreamBridge extends EventEmitter {
844
917
  permissionMode,
845
918
  maxTurns,
846
919
  maxThinkingTokens,
920
+ effort,
847
921
  resumeSessionId,
848
922
  resident,
849
923
  }) {
@@ -872,6 +946,7 @@ export class ClaudeStreamBridge extends EventEmitter {
872
946
  permissionMode,
873
947
  maxThinkingTokens:
874
948
  typeof maxThinkingTokens === "number" ? maxThinkingTokens : null,
949
+ effort,
875
950
  })
876
951
  this.sessions.set(stream_id, live)
877
952
  this.logger?.info(
@@ -882,6 +957,7 @@ export class ClaudeStreamBridge extends EventEmitter {
882
957
  model: live.model,
883
958
  permissionMode: live.permissionMode,
884
959
  maxThinkingTokens: live.maxThinkingTokens,
960
+ effort: live.effort,
885
961
  },
886
962
  "claude stream reattached to live session",
887
963
  )
@@ -896,6 +972,7 @@ export class ClaudeStreamBridge extends EventEmitter {
896
972
  maxTurns: typeof maxTurns === "number" ? maxTurns : null,
897
973
  maxThinkingTokens:
898
974
  typeof maxThinkingTokens === "number" ? maxThinkingTokens : null,
975
+ effort: effort || null,
899
976
  resumeSessionId: resumeSessionId || null,
900
977
  resident,
901
978
  sdk: this.sdk,
@@ -990,6 +1067,17 @@ export class ClaudeStreamBridge extends EventEmitter {
990
1067
  return true
991
1068
  }
992
1069
 
1070
+ /** キャンセル機能 (0.6.26): browser → 送信待ち (pending) メッセージを id 指定で取り消す。
1071
+ * 実行中ターンには影響しない (中断は interrupt を使う)。 */
1072
+ cancelQueued({ stream_id, id }) {
1073
+ const s = this.sessions.get(stream_id)
1074
+ if (!s) {
1075
+ this.logger?.warn({ stream_id, id }, "claude.queue.cancel but stream missing")
1076
+ return false
1077
+ }
1078
+ return s.cancelQueued(id)
1079
+ }
1080
+
993
1081
  /**
994
1082
  * セッション停止 (graceful)。実行中ターンは中断せず完走させ、完走後に
995
1083
  * onReap で Map から撤去する。アイドルなら即時撤去。
package/src/main.mjs CHANGED
@@ -756,6 +756,9 @@ async function dispatch(msg, ctx) {
756
756
  typeof msg.max_thinking_tokens === "number"
757
757
  ? msg.max_thinking_tokens
758
758
  : null,
759
+ // effort (Opus 4.6+): browser がチャット既定 (agent 設定由来) を送る。
760
+ // 空文字/未指定なら null (SDK / Claude Code 既定 = Opus は high)。
761
+ effort: msg.effort || ctx.config?.claude_effort || null,
759
762
  resumeSessionId: msg.resume_session_id || null,
760
763
  })
761
764
  ctx.client.send({
@@ -844,6 +847,11 @@ async function dispatch(msg, ctx) {
844
847
  if (!ctx.claudeBridge) return
845
848
  ctx.claudeBridge.interrupt({ stream_id: msg.stream_id })
846
849
  return
850
+ case "claude.queue.cancel":
851
+ // 送信待ち (pending) メッセージを id 指定で取り消す (0.6.26)。
852
+ if (!ctx.claudeBridge) return
853
+ ctx.claudeBridge.cancelQueued({ stream_id: msg.stream_id, id: msg.id })
854
+ return
847
855
  case "claude.detach":
848
856
  if (!ctx.claudeBridge) return
849
857
  ctx.claudeBridge.detach({ stream_id: msg.stream_id })