@cocorograph/hub-agent 0.8.27 → 0.8.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.8.27",
3
+ "version": "0.8.28",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -3000,6 +3000,29 @@ export class ClaudeStreamBridge extends EventEmitter {
3000
3000
  )
3001
3001
  }
3002
3002
 
3003
+ /**
3004
+ * cwd と session_id で一意に確定したライブセッションの claude.event 経路へ内部イベントを流す。
3005
+ * sibling-inject の delegate-event 専用で、本文 FIFO や sendMessage は経由しない。
3006
+ */
3007
+ emitToStream(cwd, sessionId, event) {
3008
+ if (!cwd || !sessionId) return { status: "not_found" }
3009
+ const matches = this._liveCandidatesForCwd(cwd, true).filter(
3010
+ ({ session }) => session.sessionId === sessionId,
3011
+ )
3012
+ if (matches.length === 0) return { status: "not_found" }
3013
+ if (matches.length > 1) {
3014
+ return {
3015
+ status: "ambiguous",
3016
+ candidates: matches.map(({ session, attached }) =>
3017
+ this._describeCandidate(session, attached),
3018
+ ),
3019
+ }
3020
+ }
3021
+ const { session, attached } = matches[0]
3022
+ session._emit(event)
3023
+ return { status: "delivered", session: this._describeCandidate(session, attached) }
3024
+ }
3025
+
3003
3026
  /**
3004
3027
  * cwd 内の live セッション候補を `{session, attached}` の配列で返す共通スキャン。
3005
3028
  * this.sessions (attached=browser 接続中) と _liveBySession (attached=false=soft-detached の warm)
@@ -332,7 +332,21 @@ export class SiblingInjectBridge {
332
332
  // (T06271 レビュー major①): 旧 hub-agent は `!text` で invalid_request を返すため、
333
333
  // 未知フィールドを無視して sendToCwd 推測注入する事故が **単一リクエストで原理的に
334
334
  // 起きない** (probe→注入の TOCTOU を排除)。
335
- const { op, cwd, target_cli_kind, text, message, token, target_session_id, source } = body || {}
335
+ const {
336
+ op,
337
+ cwd,
338
+ target_cli_kind,
339
+ text,
340
+ message,
341
+ token,
342
+ target_session_id,
343
+ source,
344
+ delegation_id,
345
+ phase,
346
+ agent,
347
+ description,
348
+ timestamp,
349
+ } = body || {}
336
350
  // 連携カード種別 (Phase 2, 2026-07-23): 注入を受け手画面で専用カード表示させるためのタグ。
337
351
  // 既知値 ('peer'|'orchestrator') のみ通し、未知値/未指定は undefined に落とす (未知の source を
338
352
  // frontend へ流して分岐を汚さない fail-closed)。bridge が user echo にこれを付ける。
@@ -340,9 +354,22 @@ export class SiblingInjectBridge {
340
354
  const isListAll = op === "list-all"
341
355
  const isList = op === "list"
342
356
  const isExplicit = op === "inject"
357
+ const isDelegateEvent = op === "delegate-event"
343
358
  let result
344
359
  const badKind = target_cli_kind !== "claude" && target_cli_kind !== "codex"
345
- const badBody = isList
360
+ const delegatePhases = new Set(["started", "completed", "failed", "stopped"])
361
+ const badDelegateBody =
362
+ typeof delegation_id !== "string" ||
363
+ !delegation_id ||
364
+ !delegatePhases.has(phase) ||
365
+ (phase === "started" && (typeof agent !== "string" || !agent)) ||
366
+ (agent !== undefined && typeof agent !== "string") ||
367
+ (description !== undefined && typeof description !== "string") ||
368
+ (timestamp !== undefined &&
369
+ (typeof timestamp !== "string" || Number.isNaN(Date.parse(timestamp))))
370
+ const badBody = isDelegateEvent
371
+ ? badDelegateBody
372
+ : isList
346
373
  ? false
347
374
  : isExplicit
348
375
  ? !target_session_id || !message // 明示は id と message 必須 (空指定 fail-open を防ぐ)
@@ -361,7 +388,7 @@ export class SiblingInjectBridge {
361
388
  // 判定は live-sessions.mjs に一本化する (tmux.list_sessions の warm と同じ関数)。
362
389
  result = { ok: true, live_sessions: describeLiveSessionsUnion(this._bridges()) }
363
390
  }
364
- } else if (!cwd || badKind || badBody) {
391
+ } else if (!cwd || (!isDelegateEvent && badKind) || badBody) {
365
392
  result = { ok: false, error: "invalid_request" }
366
393
  } else if (!verifySiblingTokenAny(token)) {
367
394
  // 2026-07-23 統一再設計 1-2: 認可を「ターゲット cwd 所有」→「いずれかの cwd の有効トークン
@@ -384,7 +411,34 @@ export class SiblingInjectBridge {
384
411
  }
385
412
  }
386
413
  const bridge = target_cli_kind === "codex" ? this.codexBridge : this.claudeBridge
387
- if (isList) {
414
+ if (isDelegateEvent) {
415
+ // 委譲カードは別 Claude 会話へ誤配送すると誤解を生むため、cwd 内の 1 本へ推測配送しない。
416
+ // target_session_id が無い場合も、指定先が一意に見つからない場合も同じ fail-closed 契約にする。
417
+ if (!target_session_id) {
418
+ result = { ok: false, error: AMBIGUOUS_TARGET_ERROR }
419
+ } else {
420
+ const event = {
421
+ type: "system",
422
+ subtype: "codex_delegate",
423
+ delegation_id,
424
+ phase,
425
+ ...(agent !== undefined ? { agent } : {}),
426
+ ...(description !== undefined ? { description } : {}),
427
+ timestamp: timestamp || new Date().toISOString(),
428
+ }
429
+ const r = this.claudeBridge?.emitToStream
430
+ ? this.claudeBridge.emitToStream(cwd, target_session_id, event)
431
+ : { status: "not_found" }
432
+ result =
433
+ r.status === "delivered"
434
+ ? { ok: true, delivered_to: r.session }
435
+ : {
436
+ ok: false,
437
+ error: AMBIGUOUS_TARGET_ERROR,
438
+ ...(r.candidates ? { candidates: r.candidates } : {}),
439
+ }
440
+ }
441
+ } else if (isList) {
388
442
  // 宛先候補の列挙 (cockpit_orch.py --list 用)。この cwd の同種セッションを返す。
389
443
  // ⚠️ tmux.list_sessions の `warm` と同じ関数を通す (二重定義にすると「グレー表示
390
444
  // なのに宛先一覧に出ない」食い違いが再発する)。