@cocorograph/hub-agent 0.6.26 → 0.6.28

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.26",
3
+ "version": "0.6.28",
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
@@ -204,6 +209,11 @@ class ClaudeStreamSession {
204
209
  /** 改修3: 直近 browser へ通知した queue 署名 (件数 + id 列)。変化時のみ emit する。
205
210
  * 空キューの署名 ("0:") で初期化し、空→空の冗長 emit を抑止する。 */
206
211
  this._lastEmittedQueueSig = "0:"
212
+ /** ultracode (0.6.28): 常駐 query へ現在適用済みの ultracode 状態。ターン単位の
213
+ * ワンショット適用を applyFlagSettings で reconcile する際の差分判定に使う。
214
+ * query を (再)起動すると flag settings は既定に戻るため、_runResidentQuery 冒頭で
215
+ * false に戻す。 */
216
+ this._ultracodeCurrent = false
207
217
 
208
218
  /** @type {Map<string, {resolve: (decision: object) => void}>} permission 応答待ち */
209
219
  this._permissionResolvers = new Map()
@@ -326,7 +336,7 @@ class ClaudeStreamSession {
326
336
  * 値が undefined のキーは「変更なし」として無視する (バッジ未送出時に既存値を消さない)。
327
337
  * model に空文字/null が来たら setModel(undefined) でデフォルトへ戻す。
328
338
  * maxThinkingTokens に 0/null が来たら setMaxThinkingTokens(null) でオフにする。 */
329
- applyRuntimeOptions({ model, permissionMode, maxThinkingTokens } = {}) {
339
+ applyRuntimeOptions({ model, permissionMode, maxThinkingTokens, effort } = {}) {
330
340
  const q = this._residentQuery
331
341
  // モデル
332
342
  if (model !== undefined) {
@@ -378,6 +388,34 @@ class ClaudeStreamSession {
378
388
  }
379
389
  }
380
390
  }
391
+ // effort (Opus 4.6+)。SDK には setEffort 相当のランタイム制御メソッドが無いため、
392
+ // 保持値の更新のみ行う。per-message セッションは次ターンの options 構築時に
393
+ // this.effort を読むため即反映される。常駐 query は次 spawn (resume 再起動) 時に
394
+ // 反映される (= 次回セッションから切替。ユーザー要件と一致)。
395
+ if (effort !== undefined) {
396
+ this.effort = effort || null
397
+ }
398
+ }
399
+
400
+ /** モデルが Opus 4.6+ (effort / adaptive thinking 対応) かどうか。
401
+ * budget 方式 (maxThinkingTokens) は Opus 4.7+ で廃止扱いのため、effort モデルでは
402
+ * effort + thinking:{type:'adaptive'} に切り替える。 */
403
+ _isEffortModel() {
404
+ return typeof this.model === "string" && /claude-opus-4-[678]/.test(this.model)
405
+ }
406
+
407
+ /** 思考関連オプション (effort / adaptive thinking / 旧 budget) を options へ適用する。
408
+ * per-message / 常駐 query の両方から呼ぶ共通ロジック (分岐の二重定義を避ける)。 */
409
+ _applyThinkingOptions(options) {
410
+ if (this._isEffortModel()) {
411
+ // Opus: adaptive thinking を明示 ON にし、effort で深さを指定する。
412
+ // budget 方式 (maxThinkingTokens) は使わない (Opus 4.7+ で非対応)。
413
+ options.thinking = { type: "adaptive" }
414
+ if (this.effort) options.effort = this.effort
415
+ } else if (this.maxThinkingTokens != null) {
416
+ // 非 effort モデル (Sonnet / Haiku 等) は従来の budget 方式を維持。
417
+ options.maxThinkingTokens = this.maxThinkingTokens
418
+ }
381
419
  }
382
420
 
383
421
  /** soft detach: browser 切断時にターンを中断せずセッションを生かしたまま detached に
@@ -412,11 +450,18 @@ class ClaudeStreamSession {
412
450
  * 既存ターン実行中 (busy) は破棄せず pending キューへ退避し、現ターン完了時に drain する
413
451
  * (改修3)。常駐 query 対象 (新規セッション) は InputQueue へ積む (改修2)。
414
452
  */
415
- async sendMessage(message) {
453
+ async sendMessage(message, opts = {}) {
416
454
  if (this._closed) return
417
455
  const prompt = extractPromptText(message)
418
456
  if (!prompt) return
419
457
 
458
+ // ultracode ワンショット (0.6.28): このターンのみ xhigh effort + 常時 dynamic-workflow
459
+ // オーケストレーションを有効化する。セッション既定としては持たず (トークン消費が
460
+ // 桁違いになるため)、ターン単位でトグルし、完了後は通常状態へ戻す。
461
+ // per-message では options.settings に乗せ、resident では applyFlagSettings で
462
+ // ターン前に ON / 次ターン前に OFF へ reconcile する (詳細は _reconcileResidentUltracode)。
463
+ const ultracode = opts.ultracode === true
464
+
420
465
  // 改修2+4: 常駐query対象セッション。
421
466
  if (this._residentEligible) {
422
467
  if (!this._inputQueue) this._inputQueue = new InputQueue()
@@ -425,7 +470,7 @@ class ClaudeStreamSession {
425
470
  // 次を push すると SDK streaming-input で割り込み扱いになり得るため (公式 interrupt 警告)。
426
471
  // pending は queue_state で UI (送信待ちチップ) に出す (per-message と同じ体験)。
427
472
  if (this._busy) {
428
- this._enqueuePending(prompt)
473
+ this._enqueuePending(prompt, ultracode)
429
474
  this.logger?.info(
430
475
  { stream_id: this.stream_id, queued: this._pendingMessages.length },
431
476
  "resident busy, message queued",
@@ -434,14 +479,18 @@ class ClaudeStreamSession {
434
479
  return
435
480
  }
436
481
  this._busy = true
437
- this._inputQueue.push(toSDKUserMessage(prompt))
438
482
  // 改修4 (A): 死亡ガード。常駐 query が未起動 or (エラー等で) 終了済み (_residentQuery=null)
439
483
  // なら (再)起動する。_runResidentQuery は起動時 options.resume=this.sessionId で文脈を
440
484
  // 復元するため、途中死からの復活でも過去コンテキストは失われない。
485
+ // ultracode のとき: query を先に起動 (空 InputQueue なので入力待ちでブロック) し、
486
+ // applyFlagSettings を await してから push することで、設定適用前にターンが
487
+ // 消費されるレースを防ぐ。
441
488
  if (!this._residentQuery) {
442
489
  this._residentStarted = true
443
490
  this._startResidentQuery()
444
491
  }
492
+ await this._reconcileResidentUltracode(ultracode)
493
+ this._inputQueue.push(toSDKUserMessage(prompt))
445
494
  return
446
495
  }
447
496
 
@@ -449,7 +498,7 @@ class ClaudeStreamSession {
449
498
  // 改修3: busy 中の送信は破棄せず pending キューへ退避し、現ターン完了時に drain する
450
499
  // (ターミナル流の「積む→待機→順次実行」)。常駐 query 化はしないので暴走リスクは増えない。
451
500
  if (this._busy) {
452
- this._enqueuePending(prompt)
501
+ this._enqueuePending(prompt, ultracode)
453
502
  this.logger?.info(
454
503
  { stream_id: this.stream_id, queued: this._pendingMessages.length },
455
504
  "claude busy, message queued",
@@ -457,12 +506,12 @@ class ClaudeStreamSession {
457
506
  this._emitQueueState()
458
507
  return
459
508
  }
460
- return this._runPerMessage(prompt)
509
+ return this._runPerMessage(prompt, { ultracode })
461
510
  }
462
511
 
463
512
  /** per-message 1 ターンを実行する (resume チェーン)。busy 中に届いた送信は sendMessage
464
513
  * が pending キューへ退避し、本メソッドの finally で drain する。 */
465
- async _runPerMessage(prompt) {
514
+ async _runPerMessage(prompt, opts = {}) {
466
515
  this._busy = true
467
516
  this._abortController = new AbortController()
468
517
  let aborted = false
@@ -475,9 +524,20 @@ class ClaudeStreamSession {
475
524
  }
476
525
  if (this.model) options.model = this.model
477
526
  if (this.permissionMode) options.permissionMode = this.permissionMode
478
- // Phase B: チャット SDK に効くオプション (拡張思考予算 / ツール往復上限)。
527
+ // Phase B: チャット SDK に効くオプション (ツール往復上限)。
479
528
  if (this.maxTurns != null) options.maxTurns = this.maxTurns
480
- if (this.maxThinkingTokens != null) options.maxThinkingTokens = this.maxThinkingTokens
529
+ // 思考オプション (effort / adaptive thinking / 旧 budget) はモデルに応じて切替。
530
+ this._applyThinkingOptions(options)
531
+ // ultracode ワンショット (0.6.28): per-message は 1 query = 1 ターンなので、
532
+ // このターンの options.settings (= --settings 相当 / flag settings 層) に乗せるだけで
533
+ // 自然に 1 ターン限定になる。次ターンは options を作り直すため通常状態に戻る。
534
+ if (opts.ultracode === true) {
535
+ options.settings = {
536
+ ...(options.settings || {}),
537
+ ultracode: true,
538
+ enableWorkflows: true,
539
+ }
540
+ }
481
541
  // 直前ターンまでの session_id があれば resume チェーン
482
542
  if (this.sessionId) options.resume = this.sessionId
483
543
 
@@ -579,18 +639,21 @@ class ClaudeStreamSession {
579
639
  }
580
640
  const next = this._pendingMessages.shift()
581
641
  this._emitQueueState([next.text])
582
- this._runPerMessage(next.text).catch((err) => {
583
- this.logger?.error(
584
- { stream_id: this.stream_id, err: err?.message },
585
- "drain runPerMessage threw",
586
- )
587
- })
642
+ this._runPerMessage(next.text, { ultracode: next.ultracode === true }).catch(
643
+ (err) => {
644
+ this.logger?.error(
645
+ { stream_id: this.stream_id, err: err?.message },
646
+ "drain runPerMessage threw",
647
+ )
648
+ },
649
+ )
588
650
  }
589
651
 
590
- /** キャンセル機能 (0.6.26): pending キューへ安定 id 付きで 1 件積む。 */
591
- _enqueuePending(prompt) {
652
+ /** キャンセル機能 (0.6.26): pending キューへ安定 id 付きで 1 件積む。
653
+ * ultracode (0.6.28): ワンショット ultracode フラグもエントリに保持し、drain 時に伝播する。 */
654
+ _enqueuePending(prompt, ultracode = false) {
592
655
  const id = `q${++this._queueSeq}`
593
- this._pendingMessages.push({ id, text: prompt })
656
+ this._pendingMessages.push({ id, text: prompt, ultracode: ultracode === true })
594
657
  return id
595
658
  }
596
659
 
@@ -651,9 +714,38 @@ class ClaudeStreamSession {
651
714
  })
652
715
  }
653
716
 
717
+ /** ultracode (0.6.28): 常駐 query の ultracode 状態を目標値へ寄せる (差分時のみ
718
+ * applyFlagSettings を発行)。push の前に await して、設定適用前にターンが消費される
719
+ * レースを防ぐ。query 未起動 / applyFlagSettings 非対応 SDK では no-op。 */
720
+ async _reconcileResidentUltracode(desired) {
721
+ const want = desired === true
722
+ if (want === this._ultracodeCurrent) return
723
+ const q = this._residentQuery
724
+ if (!q || typeof q.applyFlagSettings !== "function") return
725
+ try {
726
+ await q.applyFlagSettings(
727
+ want
728
+ ? { ultracode: true, enableWorkflows: true }
729
+ : { ultracode: false },
730
+ )
731
+ this._ultracodeCurrent = want
732
+ this.logger?.info(
733
+ { stream_id: this.stream_id, ultracode: want },
734
+ "resident ultracode reconciled",
735
+ )
736
+ } catch (err) {
737
+ this.logger?.warn(
738
+ { stream_id: this.stream_id, err: err?.message },
739
+ "applyFlagSettings ultracode failed",
740
+ )
741
+ }
742
+ }
743
+
654
744
  /** 改修4 (A): ターン完了時に pending の先頭 1 件を InputQueue へ流す (ターンのシリアライズ)。
655
- * queue_state を更新して送信待ちチップを drain させる (frontend がバブル昇格する)。 */
656
- _drainResidentPending() {
745
+ * queue_state を更新して送信待ちチップを drain させる (frontend がバブル昇格する)。
746
+ * ultracode (0.6.28): 次ターンの目標 ultracode 状態へ reconcile してから push する
747
+ * (await するため async 化。result ハンドラからは fire-and-forget で呼ばれる)。 */
748
+ async _drainResidentPending() {
657
749
  if (this._closed) return
658
750
  if (this._pendingMessages.length === 0) {
659
751
  this._emitQueueState()
@@ -661,6 +753,7 @@ class ClaudeStreamSession {
661
753
  }
662
754
  const next = this._pendingMessages.shift()
663
755
  this._busy = true
756
+ await this._reconcileResidentUltracode(next.ultracode === true)
664
757
  this._inputQueue.push(toSDKUserMessage(next.text))
665
758
  this._emitQueueState([next.text])
666
759
  }
@@ -683,7 +776,11 @@ class ClaudeStreamSession {
683
776
  if (this.model) options.model = this.model
684
777
  if (this.permissionMode) options.permissionMode = this.permissionMode
685
778
  if (this.maxTurns != null) options.maxTurns = this.maxTurns
686
- if (this.maxThinkingTokens != null) options.maxThinkingTokens = this.maxThinkingTokens
779
+ this._applyThinkingOptions(options)
780
+ // ultracode (0.6.28): 新規 query は flag settings 既定 (ultracode=off) で始まる。
781
+ // 適用済み状態の追跡を false にリセットし、次ターンの reconcile が正しく差分判定できる
782
+ // ようにする (異常終了→resume 再起動時にも確実にリセット)。
783
+ this._ultracodeCurrent = false
687
784
  // 改修4: 起動時に sessionId (= resumeSessionId) があれば resume チェーンで文脈を引き継ぐ。
688
785
  // query 起動時点の値のみ有効 (起動後に確定/変化する session_id は同一 query 内で継続される)。
689
786
  if (this.sessionId) options.resume = this.sessionId
@@ -714,7 +811,14 @@ class ClaudeStreamSession {
714
811
  denyPending("turn ended")
715
812
  this._busy = false
716
813
  // 改修4 (A): シリアライズした pending があれば次の 1 件を InputQueue へ流す。
717
- this._drainResidentPending()
814
+ // ultracode (0.6.28): _drainResidentPending は applyFlagSettings を await するため
815
+ // async。result ハンドラ (for await ループ内) からは fire-and-forget で呼ぶ。
816
+ this._drainResidentPending().catch((err) =>
817
+ this.logger?.warn(
818
+ { stream_id: this.stream_id, err: err?.message },
819
+ "drainResidentPending threw",
820
+ ),
821
+ )
718
822
  }
719
823
  try {
720
824
  this.onEvent?.(msg)
@@ -871,6 +975,7 @@ export class ClaudeStreamBridge extends EventEmitter {
871
975
  * permissionMode?: string|null,
872
976
  * maxTurns?: number|null,
873
977
  * maxThinkingTokens?: number|null,
978
+ * effort?: string|null,
874
979
  * resumeSessionId?: string|null,
875
980
  * }} args
876
981
  * @returns {{ stream_id: string, resuming: boolean }}
@@ -882,6 +987,7 @@ export class ClaudeStreamBridge extends EventEmitter {
882
987
  permissionMode,
883
988
  maxTurns,
884
989
  maxThinkingTokens,
990
+ effort,
885
991
  resumeSessionId,
886
992
  resident,
887
993
  }) {
@@ -910,6 +1016,7 @@ export class ClaudeStreamBridge extends EventEmitter {
910
1016
  permissionMode,
911
1017
  maxThinkingTokens:
912
1018
  typeof maxThinkingTokens === "number" ? maxThinkingTokens : null,
1019
+ effort,
913
1020
  })
914
1021
  this.sessions.set(stream_id, live)
915
1022
  this.logger?.info(
@@ -920,6 +1027,7 @@ export class ClaudeStreamBridge extends EventEmitter {
920
1027
  model: live.model,
921
1028
  permissionMode: live.permissionMode,
922
1029
  maxThinkingTokens: live.maxThinkingTokens,
1030
+ effort: live.effort,
923
1031
  },
924
1032
  "claude stream reattached to live session",
925
1033
  )
@@ -934,6 +1042,7 @@ export class ClaudeStreamBridge extends EventEmitter {
934
1042
  maxTurns: typeof maxTurns === "number" ? maxTurns : null,
935
1043
  maxThinkingTokens:
936
1044
  typeof maxThinkingTokens === "number" ? maxThinkingTokens : null,
1045
+ effort: effort || null,
937
1046
  resumeSessionId: resumeSessionId || null,
938
1047
  resident,
939
1048
  sdk: this.sdk,
@@ -991,14 +1100,15 @@ export class ClaudeStreamBridge extends EventEmitter {
991
1100
  }
992
1101
 
993
1102
  /** browser → claude の user メッセージ。1 件 = 1 query (resume チェーン)。 */
994
- input({ stream_id, message }) {
1103
+ input({ stream_id, message, ultracode }) {
995
1104
  const s = this.sessions.get(stream_id)
996
1105
  if (!s) {
997
1106
  this.logger?.warn({ stream_id }, "claude.input but stream missing")
998
1107
  return false
999
1108
  }
1000
1109
  // 非同期でターン実行 (完了は result イベント + onEvent 経由で browser に届く)
1001
- s.sendMessage(message).catch((err) => {
1110
+ // ultracode (0.6.28): このメッセージのみ ultracode ワンショットを適用するフラグ。
1111
+ s.sendMessage(message, { ultracode: ultracode === true }).catch((err) => {
1002
1112
  this.logger?.error(
1003
1113
  { stream_id, err: err?.message },
1004
1114
  "claude sendMessage threw unexpectedly",
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({
@@ -778,6 +781,9 @@ async function dispatch(msg, ctx) {
778
781
  ctx.claudeBridge.input({
779
782
  stream_id: msg.stream_id,
780
783
  message: msg.message,
784
+ // ultracode (0.6.28): browser がこのメッセージ単位で送るワンショット指定。
785
+ // true のときだけそのターンを xhigh effort + dynamic-workflow で実行する。
786
+ ultracode: msg.ultracode === true,
781
787
  })
782
788
  return
783
789
  case "claude.upload": {