@cocorograph/hub-agent 0.7.100 → 0.7.102
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
|
@@ -314,7 +314,9 @@ const STALE_IDLE_GUARD_MS = 5000
|
|
|
314
314
|
/**
|
|
315
315
|
* 1 stream に対応する Claude セッション (per-message query モデル)。
|
|
316
316
|
*/
|
|
317
|
-
|
|
317
|
+
// export: 単体テストから直接インスタンス化して純ロジック (連携カード source タグの
|
|
318
|
+
// record/remove/tag 等) を検証するため。実運用の生成は ClaudeStreamBridge が行う。
|
|
319
|
+
export class ClaudeStreamSession {
|
|
318
320
|
constructor({
|
|
319
321
|
stream_id,
|
|
320
322
|
cwd,
|
|
@@ -431,6 +433,14 @@ class ClaudeStreamSession {
|
|
|
431
433
|
/** 改修3: 直近 browser へ通知した queue 署名 (件数 + id 列)。変化時のみ emit する。
|
|
432
434
|
* 空キューの署名 ("0:") で初期化し、空→空の冗長 emit を抑止する。 */
|
|
433
435
|
this._lastEmittedQueueSig = "0:"
|
|
436
|
+
|
|
437
|
+
/** 連携カード種別タグ (Phase 2, 2026-07-23): ask-sibling/orchestrate 等の **別セッションからの
|
|
438
|
+
* 注入** を、受け手画面で「青い自分バブル」ではなく専用の連携依頼カードで表示させるため、
|
|
439
|
+
* 注入本文 (text) を相関キーに source ('peer'|'orchestrator') を stash する FIFO。
|
|
440
|
+
* sendToCwd/sendToStream が注入直前に記録し、直後に SDK が返す user echo (type:"user") の
|
|
441
|
+
* 本文一致で _emit が消費してタグ付けする。人間の実入力や tool_result は本 set に無いので
|
|
442
|
+
* タグされない (誤タグ防止)。echo が来ない稀ケースの無限成長は上限で古い順に切る。 */
|
|
443
|
+
this._pendingInjectSources = []
|
|
434
444
|
/** ultracode (0.6.28): 常駐 query へ現在適用済みの ultracode 状態。ターン単位の
|
|
435
445
|
* ワンショット適用を applyFlagSettings で reconcile する際の差分判定に使う。
|
|
436
446
|
* query を (再)起動すると flag settings は既定に戻るため、_runResidentQuery 冒頭で
|
|
@@ -519,6 +529,64 @@ class ClaudeStreamSession {
|
|
|
519
529
|
|
|
520
530
|
/** マスク済みイベントを onEvent へ流す共通経路。秘匿情報 / 非公開スキルプロンプト本文を
|
|
521
531
|
* 表示・WS 配信前に伏せる。マスク失敗時は素のイベントを流す (表示が止まるより素通し優先)。 */
|
|
532
|
+
/** 連携カード種別 (Phase 2): 注入本文と source を FIFO に控える。sendToCwd/sendToStream が
|
|
533
|
+
* session.sendMessage() を呼ぶ直前に記録し、直後の user echo を _tagInjectSource が拾う。
|
|
534
|
+
*
|
|
535
|
+
* ⚠️ 相関は「注入本文の完全一致」で行う (cross-review round1 major#1)。ID をメタデータで
|
|
536
|
+
* 伝播する方が堅牢だが、**Claude Agent SDK の user echo には隠しメタを載せる経路が無い**
|
|
537
|
+
* (echo は本文そのもの)。cockpit_orch.py が注入本文末尾に付ける返信 footer で本文の一意性が
|
|
538
|
+
* 高く、人間が同一本文を打つ衝突は実質起きない。残る衝突リスクは①送信失敗時の stale entry
|
|
539
|
+
* → _removeInjectSource で掃除、②未 echo の無限成長 → 32 件上限、で最小化する。衝突時も
|
|
540
|
+
* 誤タグ (青バブルが稀にカード化) に留まりクラッシュしない。
|
|
541
|
+
* @param {string} text 注入本文 (SDK が echo する user メッセージ本文と一致する)
|
|
542
|
+
* @param {'peer'|'orchestrator'|undefined} source
|
|
543
|
+
* @returns {object|null} この entry の固有 handle (identity)。配送失敗時に _removeInjectSource へ
|
|
544
|
+
* 渡して**その entry だけ**を除去する。無効な source/text では null。 */
|
|
545
|
+
_recordInjectSource(text, source) {
|
|
546
|
+
if ((source !== "peer" && source !== "orchestrator") || !text) return null
|
|
547
|
+
const entry = { text, source }
|
|
548
|
+
this._pendingInjectSources.push(entry)
|
|
549
|
+
// echo が来ない稀ケース (注入直後にセッション消失等) の無限成長を防ぐ。連携注入は低頻度
|
|
550
|
+
// なので 32 件で十分 (古い順に切る)。
|
|
551
|
+
if (this._pendingInjectSources.length > 32) this._pendingInjectSources.shift()
|
|
552
|
+
return entry
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** 連携カード種別 (Phase 2, cross-review round1 major#1 / round2 major): 注入 (sendMessage) が
|
|
556
|
+
* 配送失敗したときに、record が返した **その entry だけ**を identity (indexOf) で取り消す。
|
|
557
|
+
* ⚠️ text で消してはいけない (round2 指摘): 同一本文を並行注入し後発だけ失敗すると、text 先頭
|
|
558
|
+
* 一致は先発の成功 entry を誤って消し、先発 echo が後発 source で誤タグされる。handle 同定なら
|
|
559
|
+
* 失敗した当該 entry のみ除去される。既に消費/cap 済みなら indexOf=-1 で no-op。 */
|
|
560
|
+
_removeInjectSource(handle) {
|
|
561
|
+
if (!handle) return
|
|
562
|
+
const idx = this._pendingInjectSources.indexOf(handle)
|
|
563
|
+
if (idx !== -1) this._pendingInjectSources.splice(idx, 1)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** user echo イベントの本文が控えた注入本文と一致すれば source タグを付けて返す (それ以外は
|
|
567
|
+
* 無変更)。frontend はこの source があれば専用の連携依頼カードへ分岐する。共有/at-rest の
|
|
568
|
+
* イベントを壊さないよう shallow-clone に付ける (top-level に source を足すだけ)。 */
|
|
569
|
+
_tagInjectSource(event) {
|
|
570
|
+
if (!this._pendingInjectSources.length) return event
|
|
571
|
+
if (!event || event.type !== "user" || !event.message) return event
|
|
572
|
+
const content = event.message.content
|
|
573
|
+
const text =
|
|
574
|
+
typeof content === "string"
|
|
575
|
+
? content
|
|
576
|
+
: Array.isArray(content)
|
|
577
|
+
? content
|
|
578
|
+
.filter((b) => b && b.type === "text" && typeof b.text === "string")
|
|
579
|
+
.map((b) => b.text)
|
|
580
|
+
.join("\n")
|
|
581
|
+
: ""
|
|
582
|
+
if (!text) return event
|
|
583
|
+
const idx = this._pendingInjectSources.findIndex((p) => p.text === text)
|
|
584
|
+
if (idx === -1) return event
|
|
585
|
+
const { source } = this._pendingInjectSources[idx]
|
|
586
|
+
this._pendingInjectSources.splice(idx, 1)
|
|
587
|
+
return { ...event, source }
|
|
588
|
+
}
|
|
589
|
+
|
|
522
590
|
_emit(event) {
|
|
523
591
|
if (!this.onEvent) return
|
|
524
592
|
let out = event
|
|
@@ -533,6 +601,7 @@ class ClaudeStreamSession {
|
|
|
533
601
|
out = event
|
|
534
602
|
}
|
|
535
603
|
}
|
|
604
|
+
out = this._tagInjectSource(out)
|
|
536
605
|
this.onEvent(out)
|
|
537
606
|
}
|
|
538
607
|
|
|
@@ -2610,7 +2679,7 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
2610
2679
|
* @param {string} text
|
|
2611
2680
|
* @returns {{status:"delivered", session:object} | {status:"not_found"} | {status:"ambiguous", candidates:object[]}}
|
|
2612
2681
|
*/
|
|
2613
|
-
sendToCwd(cwd, text) {
|
|
2682
|
+
sendToCwd(cwd, text, source) {
|
|
2614
2683
|
if (!cwd || !text) return { status: "not_found" }
|
|
2615
2684
|
// sessionId 未確定の fresh も配送対象にするため requireSessionId=false。
|
|
2616
2685
|
const cands = this._liveCandidatesForCwd(cwd, false)
|
|
@@ -2625,7 +2694,12 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
2625
2694
|
}
|
|
2626
2695
|
}
|
|
2627
2696
|
const { session, attached: att } = tier[0]
|
|
2697
|
+
// 連携カード種別 (Phase 2): 配送先が確定した後に記録する (ambiguous/not_found では記録しない)。
|
|
2698
|
+
// handle を控え、失敗時にその entry だけを除去する (round2: text 除去は並行同一本文で誤消費)。
|
|
2699
|
+
const injectHandle = session._recordInjectSource?.(text, source)
|
|
2628
2700
|
session.sendMessage({ role: "user", content: text }).catch((err) => {
|
|
2701
|
+
// Phase 2: 配送失敗なら echo は来ないので stale な source entry を掃除する (誤タグ防止)。
|
|
2702
|
+
session._removeInjectSource?.(injectHandle)
|
|
2629
2703
|
this.logger?.error(
|
|
2630
2704
|
{ cwd, err: err?.message },
|
|
2631
2705
|
"sendToCwd: sendMessage threw unexpectedly",
|
|
@@ -2657,7 +2731,7 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
2657
2731
|
* @param {string} text
|
|
2658
2732
|
* @returns {{status: "delivered", session: object} | {status: "not_found"} | {status: "ambiguous", candidates: object[]}}
|
|
2659
2733
|
*/
|
|
2660
|
-
sendToStream(cwd, sessionId, text) {
|
|
2734
|
+
sendToStream(cwd, sessionId, text, source) {
|
|
2661
2735
|
if (!cwd || !sessionId || !text) return { status: "not_found" }
|
|
2662
2736
|
// union 走査 (2026-07-23, 統一再設計 1-1): warm (soft-detached) セッションも session_id
|
|
2663
2737
|
// ピンの対象にする。従来は this.sessions のみで、detach 済みだが生きているセッションを
|
|
@@ -2674,7 +2748,11 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
2674
2748
|
}
|
|
2675
2749
|
}
|
|
2676
2750
|
const { session, attached } = matches[0]
|
|
2751
|
+
// 連携カード種別 (Phase 2): 宛先確定後に記録 (not_found/ambiguous では記録しない)。
|
|
2752
|
+
const injectHandle = session._recordInjectSource?.(text, source)
|
|
2677
2753
|
session.sendMessage({ role: "user", content: text }).catch((err) => {
|
|
2754
|
+
// Phase 2: 配送失敗なら echo は来ないので stale な source entry を掃除する (誤タグ防止)。
|
|
2755
|
+
session._removeInjectSource?.(injectHandle)
|
|
2678
2756
|
this.logger?.error(
|
|
2679
2757
|
{ session_id: sessionId, err: err?.message },
|
|
2680
2758
|
"sendToStream: sendMessage threw unexpectedly",
|
|
@@ -65,8 +65,40 @@ export class CodexAppServerClient extends EventEmitter {
|
|
|
65
65
|
// サブコマンド (`app-server`) より前に置く必要がある(`codex app-server --help`
|
|
66
66
|
// にはこのフラグが出ない = サブコマンド固有オプションではないため)。
|
|
67
67
|
const bypassHookTrust = process.env.HUB_CODEX_BYPASS_HOOK_TRUST !== "0"
|
|
68
|
+
// sibling 連携 env 注入 (2026-07-23): app-server プロセス env には siblingEnvFor が
|
|
69
|
+
// COCKPIT_SIBLING_TOKEN/CWD/SELF_KIND を載せている (codex-stream-bridge.mjs)。だが codex の
|
|
70
|
+
// **code-mode exec (tools.exec_command)** がモデル生成コマンドを実行する際、これらは
|
|
71
|
+
// シェルに渡らない (2026-07-23 実測: rollout で `env|grep COCKPIT_SIBLING` が空・SELF=[])。
|
|
72
|
+
// classic `codex exec` は inherit=core でも通すが、app-server チャットが使う code-mode 経路は
|
|
73
|
+
// 継承ベースが異なり COCKPIT_* を落とす。結果 codex-as-reviewer の返信 `cockpit_orch.py send`
|
|
74
|
+
// が「SELF_KIND 環境変数が無い」で fail-closed になり cross-review の返信が不達になっていた。
|
|
75
|
+
//
|
|
76
|
+
// 修正: `shell_environment_policy.set.<VAR>=<値>` で 3 変数を **無条件注入** する
|
|
77
|
+
// (inherit/exclude フィルタや code-mode の env 剥がしに関係なく command env に入る。
|
|
78
|
+
// 2026-07-23 実測で **code-mode exec (custom_tool_call/exec_command) 経路でも** 注入値が
|
|
79
|
+
// コマンドシェルに出現することを確認済み = 実際に壊れていた経路で有効)。値は this.env (= siblingEnvFor が
|
|
80
|
+
// 生成した本セッションの実値) から取るので静的化されず正しい per-session token/cwd になる。
|
|
81
|
+
// inherit=all を選ばない理由: code-mode に効く保証が無い (core 継承下で全消しを実測) ため。
|
|
82
|
+
// token が argv (ps) に載るが、既に app-server/code-mode-host の env に同値があり同一ユーザー
|
|
83
|
+
// 可視・cwd スコープ・短命なので露出クラスは増えない。HUB_CODEX_SIBLING_ENV_PASSTHROUGH=0 で opt-out。
|
|
84
|
+
const siblingEnvPassthrough = process.env.HUB_CODEX_SIBLING_ENV_PASSTHROUGH !== "0"
|
|
85
|
+
const spawnEnv = this.env || process.env
|
|
86
|
+
const siblingSetArgs = []
|
|
87
|
+
if (siblingEnvPassthrough) {
|
|
88
|
+
for (const key of [
|
|
89
|
+
"COCKPIT_SIBLING_SELF_KIND",
|
|
90
|
+
"COCKPIT_SIBLING_CWD",
|
|
91
|
+
"COCKPIT_SIBLING_TOKEN",
|
|
92
|
+
]) {
|
|
93
|
+
const val = spawnEnv[key]
|
|
94
|
+
// TOML 文字列としてクォート (パスの空白/特殊文字対策)。値に " は含まれない
|
|
95
|
+
// (hex token / パス / "codex")。1 つの argv 要素として渡すのでシェル再解釈は無い。
|
|
96
|
+
if (val) siblingSetArgs.push("-c", `shell_environment_policy.set.${key}="${val}"`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
68
99
|
const args = [
|
|
69
100
|
...(bypassHookTrust ? ["--dangerously-bypass-hook-trust"] : []),
|
|
101
|
+
...siblingSetArgs,
|
|
70
102
|
"app-server",
|
|
71
103
|
"--stdio",
|
|
72
104
|
]
|
|
@@ -497,6 +497,12 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
497
497
|
// major#2: ライブ追記でも cold flatten と同じ「item 数+概算 byte」二重上限を課すため、
|
|
498
498
|
// 蓄積済み履歴の概算 byte 合計を保持する (再計算を避け O(1) で trim 判定)。
|
|
499
499
|
historyBytes: 0,
|
|
500
|
+
// 連携カード種別タグ (Phase 2, 2026-07-23): ask-sibling/orchestrate 等の別セッションからの
|
|
501
|
+
// 注入本文 (text) → source ('peer'|'orchestrator') を控える FIFO。sendToCwd/sendToStream が
|
|
502
|
+
// 注入直前に積み、直後に app-server が返す userMessage の item/completed で本文一致により
|
|
503
|
+
// _onNotification が消費して params.item.source を付ける (frontend が専用カードへ分岐)。
|
|
504
|
+
/** @type {Array<{text: string, source: 'peer'|'orchestrator'}>} */
|
|
505
|
+
pendingInjectSources: [],
|
|
500
506
|
}
|
|
501
507
|
session.ready = new Promise((resolve) => {
|
|
502
508
|
settleReady = resolve
|
|
@@ -849,6 +855,62 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
849
855
|
return Array.from(new Set(this.sessions.values())).filter((s) => !s.dead)
|
|
850
856
|
}
|
|
851
857
|
|
|
858
|
+
/** 連携カード種別 (Phase 2): 注入本文 → source を session の FIFO に控える。sendToCwd/
|
|
859
|
+
* sendToStream が input() を呼ぶ直前に記録し、直後の userMessage echo を
|
|
860
|
+
* _tagInjectSourceOnItem が拾う。
|
|
861
|
+
* @param {object} session
|
|
862
|
+
* @param {string} text
|
|
863
|
+
* @param {'peer'|'orchestrator'|undefined} source */
|
|
864
|
+
_recordInjectSource(session, text, source) {
|
|
865
|
+
if (!session || (source !== "peer" && source !== "orchestrator") || !text) return null
|
|
866
|
+
const list = session.pendingInjectSources || (session.pendingInjectSources = [])
|
|
867
|
+
const entry = { text, source }
|
|
868
|
+
list.push(entry)
|
|
869
|
+
// echo が来ない稀ケースの無限成長を防ぐ (連携注入は低頻度、32 件で十分)。
|
|
870
|
+
if (list.length > 32) list.shift()
|
|
871
|
+
return entry
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/** 連携カード種別 (Phase 2, cross-review round1 major#1 / round2 major): 注入 (input) が配送失敗
|
|
875
|
+
* したときに、record が返した **その entry だけ**を identity (indexOf) で取り消す。
|
|
876
|
+
* ⚠️ text で消してはいけない (round2 指摘): 同一本文を並行注入し後発だけ失敗すると、text 先頭
|
|
877
|
+
* 一致は先発の成功 entry を誤って消し、先発 echo が後発 source で誤タグされる。handle 同定なら
|
|
878
|
+
* 失敗した当該 entry のみ除去される。既に消費/cap 済みなら indexOf=-1 で no-op。 */
|
|
879
|
+
_removeInjectSource(session, handle) {
|
|
880
|
+
const list = session?.pendingInjectSources
|
|
881
|
+
if (!list || !handle) return
|
|
882
|
+
const idx = list.indexOf(handle)
|
|
883
|
+
if (idx !== -1) list.splice(idx, 1)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/** userMessage の echo (item/completed) が控えた注入本文と一致すれば、params を **クローン**して
|
|
887
|
+
* item.source を付けたものを返す (元の app-server オブジェクトは変更しない)。一致しなければ null。
|
|
888
|
+
* frontend の userMessageItemToEvent がこの item.source を読み専用カードへ分岐する。 */
|
|
889
|
+
_tagInjectSourceOnItem(session, msg) {
|
|
890
|
+
const list = session?.pendingInjectSources
|
|
891
|
+
if (!list || !list.length) return null
|
|
892
|
+
if (msg?.method !== "item/completed") return null
|
|
893
|
+
const item = msg.params?.item
|
|
894
|
+
if (!item || item.type !== "userMessage") return null
|
|
895
|
+
const content = item.content
|
|
896
|
+
const text =
|
|
897
|
+
typeof content === "string"
|
|
898
|
+
? content
|
|
899
|
+
: Array.isArray(content)
|
|
900
|
+
? content
|
|
901
|
+
.map((b) =>
|
|
902
|
+
b && typeof b === "object" && typeof b.text === "string" ? b.text : "",
|
|
903
|
+
)
|
|
904
|
+
.join("")
|
|
905
|
+
: ""
|
|
906
|
+
if (!text) return null
|
|
907
|
+
const idx = list.findIndex((p) => p.text === text)
|
|
908
|
+
if (idx === -1) return null
|
|
909
|
+
const { source } = list[idx]
|
|
910
|
+
list.splice(idx, 1)
|
|
911
|
+
return { ...msg.params, item: { ...item, source } }
|
|
912
|
+
}
|
|
913
|
+
|
|
852
914
|
/** 案B: この session を指す全購読 stream_id キーを sessions Map から外す(reap / exit 共通)。 */
|
|
853
915
|
_dropAllStreamKeys(session) {
|
|
854
916
|
const ids = session.streamIds ? Array.from(session.streamIds) : []
|
|
@@ -865,6 +927,15 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
865
927
|
// 正常に走っているターンは reaper に回収されず、通知が途絶えたスタックターンだけが
|
|
866
928
|
// 放棄セッションとして回収される。
|
|
867
929
|
this._touch(session)
|
|
930
|
+
// 連携カード種別 (Phase 2, cross-review round1 major#2): タグ判定を switch より前に行い、
|
|
931
|
+
// **history にもタグ済み item を積む**。旧実装は未タグの msg.params.item を history へ入れてから
|
|
932
|
+
// ライブ emit だけタグしていたため、cwd reattach で historyItems が返ると連携カードが通常の青い
|
|
933
|
+
// 自分バブルへ戻っていた。同一プロセス内 reattach で表示が維持されるよう、以降 effectiveParams
|
|
934
|
+
// (タグ済み or 素) を history と emit の両方で使う。一致しなければ null → 素の params のまま。
|
|
935
|
+
// ※ hub-agent 再起動 (cold resume) では historyItems が揮発するため青バブルへ戻る (claude の
|
|
936
|
+
// jsonl at-rest と同じ許容範囲。source 永続マップは TTL/ID 再利用の複雑さに見合わないため不採用)。
|
|
937
|
+
const taggedParams = this._tagInjectSourceOnItem(session, msg)
|
|
938
|
+
const effectiveParams = taggedParams || msg.params
|
|
868
939
|
switch (msg.method) {
|
|
869
940
|
case "turn/started":
|
|
870
941
|
session.activeTurnId = msg.params?.turn?.id || null
|
|
@@ -887,8 +958,8 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
887
958
|
break
|
|
888
959
|
case "item/completed":
|
|
889
960
|
// T06541 案1: 完了した ThreadItem を高忠実度履歴に追記する(cwd reattach で返す用)。
|
|
890
|
-
//
|
|
891
|
-
this._appendHistoryItem(session,
|
|
961
|
+
// Phase 2: タグ済み item (effectiveParams.item) を積む = reattach でも source が残る。
|
|
962
|
+
this._appendHistoryItem(session, effectiveParams?.item, effectiveParams?.completedAtMs)
|
|
892
963
|
break
|
|
893
964
|
default:
|
|
894
965
|
break
|
|
@@ -902,7 +973,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
902
973
|
producer_seq: session.seq,
|
|
903
974
|
thread_id: session.threadId,
|
|
904
975
|
cwd: session.cwd,
|
|
905
|
-
notification: { method: msg.method, params:
|
|
976
|
+
notification: { method: msg.method, params: effectiveParams },
|
|
906
977
|
})
|
|
907
978
|
}
|
|
908
979
|
|
|
@@ -1416,7 +1487,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1416
1487
|
* @param {string} text
|
|
1417
1488
|
* @returns {{status:"delivered", session:object} | {status:"not_found"}}
|
|
1418
1489
|
*/
|
|
1419
|
-
sendToCwd(cwd, text) {
|
|
1490
|
+
sendToCwd(cwd, text, source) {
|
|
1420
1491
|
if (!cwd || !text) return { status: "not_found" }
|
|
1421
1492
|
// 案B: distinct session で走査(多対一 Map の重複を除く)。
|
|
1422
1493
|
const cands = this._liveSessions().filter((s) => s.cwd === cwd)
|
|
@@ -1449,6 +1520,8 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1449
1520
|
// 明示しておく。ターン実行中は healthy busy = protected なので reconcile で kill されない。
|
|
1450
1521
|
best.detachRequested = false
|
|
1451
1522
|
this._touch(best)
|
|
1523
|
+
// 連携カード種別 (Phase 2): 宛先確定後に記録 (not_found では記録しない)。handle を控え失敗時に除去。
|
|
1524
|
+
const injectHandle = this._recordInjectSource(best, text, source)
|
|
1452
1525
|
this.logger?.info?.(
|
|
1453
1526
|
{
|
|
1454
1527
|
cwd,
|
|
@@ -1461,6 +1534,8 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1461
1534
|
)
|
|
1462
1535
|
this.input({ stream_id: best.stream_id, message: { role: "user", content: text } }).catch(
|
|
1463
1536
|
(err) => {
|
|
1537
|
+
// Phase 2: 配送失敗なら echo は来ないので stale な source entry を取り消す (誤タグ防止)。
|
|
1538
|
+
this._removeInjectSource(best, injectHandle)
|
|
1464
1539
|
this.logger?.error(
|
|
1465
1540
|
{ cwd, stream_id: best.stream_id, err: err?.message },
|
|
1466
1541
|
"sendToCwd: input threw unexpectedly",
|
|
@@ -1498,7 +1573,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1498
1573
|
* @param {string} text
|
|
1499
1574
|
* @returns {{status: "delivered", session: object} | {status: "not_found"} | {status: "ambiguous", candidates: object[]}}
|
|
1500
1575
|
*/
|
|
1501
|
-
sendToStream(cwd, id, text) {
|
|
1576
|
+
sendToStream(cwd, id, text, source) {
|
|
1502
1577
|
if (!cwd || !id || !text) return { status: "not_found" }
|
|
1503
1578
|
let bySid = null
|
|
1504
1579
|
const byThread = []
|
|
@@ -1524,8 +1599,12 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1524
1599
|
// (sendToCwd と同じ扱い)。
|
|
1525
1600
|
target.detachRequested = false
|
|
1526
1601
|
this._touch(target)
|
|
1602
|
+
// 連携カード種別 (Phase 2): 宛先確定後に記録 (not_found/ambiguous では記録しない)。handle を控える。
|
|
1603
|
+
const injectHandle = this._recordInjectSource(target, text, source)
|
|
1527
1604
|
this.input({ stream_id: target.stream_id, message: { role: "user", content: text } }).catch(
|
|
1528
1605
|
(err) => {
|
|
1606
|
+
// Phase 2: 配送失敗なら echo は来ないので stale な source entry を取り消す (誤タグ防止)。
|
|
1607
|
+
this._removeInjectSource(target, injectHandle)
|
|
1529
1608
|
this.logger?.error(
|
|
1530
1609
|
{ id, stream_id: target.stream_id, err: err?.message },
|
|
1531
1610
|
"sendToStream: input threw unexpectedly",
|
package/src/sibling-inject.mjs
CHANGED
|
@@ -123,7 +123,11 @@ export class SiblingInjectBridge {
|
|
|
123
123
|
// (T06271 レビュー major①): 旧 hub-agent は `!text` で invalid_request を返すため、
|
|
124
124
|
// 未知フィールドを無視して sendToCwd 推測注入する事故が **単一リクエストで原理的に
|
|
125
125
|
// 起きない** (probe→注入の TOCTOU を排除)。
|
|
126
|
-
const { op, cwd, target_cli_kind, text, message, token, target_session_id } = body || {}
|
|
126
|
+
const { op, cwd, target_cli_kind, text, message, token, target_session_id, source } = body || {}
|
|
127
|
+
// 連携カード種別 (Phase 2, 2026-07-23): 注入を受け手画面で専用カード表示させるためのタグ。
|
|
128
|
+
// 既知値 ('peer'|'orchestrator') のみ通し、未知値/未指定は undefined に落とす (未知の source を
|
|
129
|
+
// frontend へ流して分岐を汚さない fail-closed)。bridge が user echo にこれを付ける。
|
|
130
|
+
const injectSource = source === "peer" || source === "orchestrator" ? source : undefined
|
|
127
131
|
const isListAll = op === "list-all"
|
|
128
132
|
const isList = op === "list"
|
|
129
133
|
const isExplicit = op === "inject"
|
|
@@ -182,7 +186,9 @@ export class SiblingInjectBridge {
|
|
|
182
186
|
} else if (isExplicit) {
|
|
183
187
|
// 明示宛先: session_id (claude) / stream_id|thread_id (codex) で cwd 一致下に確定解決。
|
|
184
188
|
// sendToStream は {status:"delivered"|"not_found"|"ambiguous"} を返す。
|
|
185
|
-
const r = bridge?.sendToStream?.(cwd, target_session_id, message) ?? {
|
|
189
|
+
const r = bridge?.sendToStream?.(cwd, target_session_id, message, injectSource) ?? {
|
|
190
|
+
status: "not_found",
|
|
191
|
+
}
|
|
186
192
|
if (r.status === "delivered") {
|
|
187
193
|
result = { ok: true, delivered_to: r.session }
|
|
188
194
|
} else if (r.status === "ambiguous") {
|
|
@@ -229,7 +235,7 @@ export class SiblingInjectBridge {
|
|
|
229
235
|
// (サイレント誤配送 + delivered_to に実配送先でない sameCwd[0] を報告する成功偽装) が
|
|
230
236
|
// あった。sendToCwd を status 返却にして解決と配送を 1 呼び出しに統合し、delivered_to は
|
|
231
237
|
// **実配送先 (r.session)** を返す。
|
|
232
|
-
const r = bridge?.sendToCwd?.(cwd, text) ?? { status: "not_found" }
|
|
238
|
+
const r = bridge?.sendToCwd?.(cwd, text, injectSource) ?? { status: "not_found" }
|
|
233
239
|
if (r.status === "delivered") {
|
|
234
240
|
result = { ok: true, delivered_to: r.session ?? null }
|
|
235
241
|
} else if (r.status === "ambiguous") {
|