@cocorograph/hub-agent 0.6.17 → 0.6.18
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 +1 -1
- package/src/claude-stream-bridge.mjs +70 -12
- package/src/main.mjs +6 -3
package/package.json
CHANGED
|
@@ -47,6 +47,21 @@ const CHAT_RESIDENT_ENABLED = process.env.HUB_AGENT_CHAT_RESIDENT !== "0"
|
|
|
47
47
|
const CHAT_RESIDENT_RESUME_ENABLED =
|
|
48
48
|
process.env.HUB_AGENT_CHAT_RESIDENT_RESUME !== "0"
|
|
49
49
|
|
|
50
|
+
/** 多端末共有セッション (2026-05-30)。同じ Claude セッション (session_id) を複数端末で
|
|
51
|
+
* 同時に開いて、どの端末でもライブ表示・入力・許可応答できるようにする。env
|
|
52
|
+
* HUB_AGENT_CHAT_SHARED="1" で有効化 (デフォルト無効 = 従来の「最後の端末だけが live」)。
|
|
53
|
+
*
|
|
54
|
+
* 有効時の差分:
|
|
55
|
+
* - reattach で旧 stream_id を sessions Map から消さず、1 セッションに複数の端末
|
|
56
|
+
* stream_id を購読登録する (this.streamIds)。これにより旧端末からの input /
|
|
57
|
+
* permission.reply / interrupt も sessions.get(stream_id) で解決できる。
|
|
58
|
+
* - detach は購読端末の参照カウントを下げ、最後の 1 台が外れて初めて idle softDetach
|
|
59
|
+
* する (1 台閉じただけで走行中セッションを撤去しない)。
|
|
60
|
+
* 配信のファンアウト自体は Hub backend が session_id 単位の group broadcast で行うため、
|
|
61
|
+
* agent 側はイベントに session_id を載せるだけでよい (既に emit 済み)。無効時は従来の
|
|
62
|
+
* 単一 stream_id 挙動と完全に一致する (this.streamIds は未使用のまま)。 */
|
|
63
|
+
const CHAT_SHARED_ENABLED = process.env.HUB_AGENT_CHAT_SHARED === "1"
|
|
64
|
+
|
|
50
65
|
/** 文字列を SDK streaming input の SDKUserMessage に包む。
|
|
51
66
|
* SDKUserMessage は parent_tool_use_id: string|null が必須フィールド。現行 SDK は入力側で
|
|
52
67
|
* 寛容なので省略しても動くが、将来の型厳格化に備えて明示する。トップレベルのユーザー入力
|
|
@@ -133,6 +148,10 @@ class ClaudeStreamSession {
|
|
|
133
148
|
onReap,
|
|
134
149
|
}) {
|
|
135
150
|
this.stream_id = stream_id
|
|
151
|
+
/** 多端末共有: このセッションを購読中の全端末 stream_id。CHAT_SHARED_ENABLED 時のみ
|
|
152
|
+
* 参照され、reattach で増え detach で減る。最後の 1 台が外れると idle softDetach に入る。
|
|
153
|
+
* this.stream_id は「最も新しく attach した端末」= legacy emit / primary 用に残す。 */
|
|
154
|
+
this.streamIds = new Set([stream_id])
|
|
136
155
|
this.cwd = cwd
|
|
137
156
|
this.model = model || null
|
|
138
157
|
this.permissionMode = permissionMode || null
|
|
@@ -278,6 +297,9 @@ class ClaudeStreamSession {
|
|
|
278
297
|
* 不具合の原因)。 */
|
|
279
298
|
reattach(stream_id, opts = undefined) {
|
|
280
299
|
this.stream_id = stream_id
|
|
300
|
+
// 多端末共有: この端末を購読集合に追加 (無効時は未使用)。旧端末の stream_id は
|
|
301
|
+
// bridge.attach 側で sessions Map に残されるため、ここでは増やすだけでよい。
|
|
302
|
+
this.streamIds.add(stream_id)
|
|
281
303
|
this._detached = false
|
|
282
304
|
if (this._idleTimer) {
|
|
283
305
|
clearTimeout(this._idleTimer)
|
|
@@ -779,6 +801,23 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
779
801
|
this._liveBySession = new Map()
|
|
780
802
|
}
|
|
781
803
|
|
|
804
|
+
/** 多端末共有: あるセッションに紐づく全端末 stream_id を sessions Map から外し、
|
|
805
|
+
* session_id 索引も掃除する。撤去 (reap / idle / shutdown) で 1 端末分だけ消すと
|
|
806
|
+
* 他端末キーが Map に残って孤児化するため、まとめて消す。 */
|
|
807
|
+
_dropSessionMappings(session) {
|
|
808
|
+
// session.streamIds に載っている端末キーをすべて外す。
|
|
809
|
+
for (const sid of Array.from(session.streamIds || [])) {
|
|
810
|
+
if (this.sessions.get(sid) === session) this.sessions.delete(sid)
|
|
811
|
+
}
|
|
812
|
+
// 念のため primary stream_id も (streamIds に無い経路があった場合の保険)。
|
|
813
|
+
if (this.sessions.get(session.stream_id) === session) {
|
|
814
|
+
this.sessions.delete(session.stream_id)
|
|
815
|
+
}
|
|
816
|
+
if (session.sessionId && this._liveBySession.get(session.sessionId) === session) {
|
|
817
|
+
this._liveBySession.delete(session.sessionId)
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
782
821
|
/**
|
|
783
822
|
* 新しい Claude セッションを開始する。
|
|
784
823
|
*
|
|
@@ -814,7 +853,12 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
814
853
|
if (resumeSessionId) {
|
|
815
854
|
const live = this._liveBySession.get(resumeSessionId)
|
|
816
855
|
if (live && !live._closed) {
|
|
817
|
-
|
|
856
|
+
// 多端末共有 (CHAT_SHARED_ENABLED): 旧 stream_id を Map から消さない。同一セッションを
|
|
857
|
+
// 複数端末が購読する状態を作り、どの端末の input/permission/interrupt も解決できるように
|
|
858
|
+
// する。無効時は従来どおり旧 stream_id を撤去し「最後の 1 端末だけ live」に戻す。
|
|
859
|
+
if (!CHAT_SHARED_ENABLED) {
|
|
860
|
+
this.sessions.delete(live.stream_id)
|
|
861
|
+
}
|
|
818
862
|
// 改修5: 再アタッチ時に model/permission/maxThinkingTokens を引き継ぎ反映する。
|
|
819
863
|
// browser はバッジ変更時に新しい値を載せた claude.attach を同一 resume で送る
|
|
820
864
|
// ため、ここで適用すると常駐 query を維持したまま次ターンから切り替わる。
|
|
@@ -858,24 +902,27 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
858
902
|
if (session.sessionId) this._liveBySession.set(session.sessionId, session)
|
|
859
903
|
},
|
|
860
904
|
onPermission: ({ tool_name, input, request_id }) => {
|
|
905
|
+
// 多端末共有: session_id を載せて backend が session group へ broadcast できるように
|
|
906
|
+
// する (全端末に許可ダイアログを出し、先着 1 件の reply を採用する)。
|
|
861
907
|
this.emit("permission", {
|
|
862
908
|
stream_id: session.stream_id,
|
|
909
|
+
session_id: session.sessionId,
|
|
863
910
|
request_id,
|
|
864
911
|
tool_name,
|
|
865
912
|
input,
|
|
866
913
|
})
|
|
867
914
|
},
|
|
868
915
|
onError: (err) => {
|
|
869
|
-
this.emit("error", {
|
|
916
|
+
this.emit("error", {
|
|
917
|
+
stream_id: session.stream_id,
|
|
918
|
+
session_id: session.sessionId,
|
|
919
|
+
error: err?.message || String(err),
|
|
920
|
+
})
|
|
870
921
|
},
|
|
871
922
|
onReap: () => {
|
|
872
923
|
// ターン完走後の遅延クローズ。Map / 索引から撤去し exit を emit する。
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
}
|
|
876
|
-
if (session.sessionId && this._liveBySession.get(session.sessionId) === session) {
|
|
877
|
-
this._liveBySession.delete(session.sessionId)
|
|
878
|
-
}
|
|
924
|
+
// 多端末共有では複数端末キーが Map に載っているのでまとめて外す。
|
|
925
|
+
this._dropSessionMappings(session)
|
|
879
926
|
this.emit("exit", {
|
|
880
927
|
stream_id: session.stream_id,
|
|
881
928
|
code: 0,
|
|
@@ -941,14 +988,25 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
941
988
|
detach({ stream_id }) {
|
|
942
989
|
const s = this.sessions.get(stream_id)
|
|
943
990
|
if (!s) return false
|
|
991
|
+
// 多端末共有: この端末分の購読だけ外す。まだ他端末が見ているならセッションは生かし、
|
|
992
|
+
// softDetach (idle 撤去タイマー) には入らない。最後の 1 台が外れたときだけ従来の
|
|
993
|
+
// idle softDetach に進む。無効時は streamIds が常に 1 要素なので即 softDetach に落ちる。
|
|
994
|
+
if (CHAT_SHARED_ENABLED) {
|
|
995
|
+
s.streamIds.delete(stream_id)
|
|
996
|
+
if (this.sessions.get(stream_id) === s) this.sessions.delete(stream_id)
|
|
997
|
+
if (s.streamIds.size > 0) {
|
|
998
|
+
// primary を生存端末へ寄せ替え (legacy emit / softDetach 撤去ログ用)。
|
|
999
|
+
if (s.stream_id === stream_id) {
|
|
1000
|
+
s.stream_id = Array.from(s.streamIds)[s.streamIds.size - 1]
|
|
1001
|
+
}
|
|
1002
|
+
return true
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
944
1005
|
// 常駐化: browser 切断ではセッションを止めない (reap しない)。走行中ターンは完走し、
|
|
945
1006
|
// idle のまま IDLE_DETACH_TTL_MS 経過して初めて撤去する。TTL 内に再接続 (再アタッチ)
|
|
946
1007
|
// されればキャンセルされる。明示的に止めたい場合は interrupt()/明示終了を使う。
|
|
947
1008
|
s.softDetach(IDLE_DETACH_TTL_MS, () => {
|
|
948
|
-
|
|
949
|
-
if (s.sessionId && this._liveBySession.get(s.sessionId) === s) {
|
|
950
|
-
this._liveBySession.delete(s.sessionId)
|
|
951
|
-
}
|
|
1009
|
+
this._dropSessionMappings(s)
|
|
952
1010
|
s.close()
|
|
953
1011
|
this.emit("exit", {
|
|
954
1012
|
stream_id: s.stream_id,
|
package/src/main.mjs
CHANGED
|
@@ -190,10 +190,13 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
190
190
|
}
|
|
191
191
|
client.send({ type: "claude.event", stream_id, session_id, event })
|
|
192
192
|
})
|
|
193
|
-
claudeBridge.on("permission", ({ stream_id, request_id, tool_name, input }) => {
|
|
193
|
+
claudeBridge.on("permission", ({ stream_id, session_id, request_id, tool_name, input }) => {
|
|
194
|
+
// 多端末共有: session_id を載せて backend が session group へ broadcast できるように
|
|
195
|
+
// する (全端末に許可ダイアログ → 先着 1 件採用)。
|
|
194
196
|
client.send({
|
|
195
197
|
type: "claude.permission.request",
|
|
196
198
|
stream_id,
|
|
199
|
+
session_id,
|
|
197
200
|
request_id,
|
|
198
201
|
tool_name,
|
|
199
202
|
input,
|
|
@@ -202,8 +205,8 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
202
205
|
claudeBridge.on("exit", ({ stream_id, code, reason, session_id }) => {
|
|
203
206
|
client.send({ type: "claude.exit", stream_id, code, reason, session_id })
|
|
204
207
|
})
|
|
205
|
-
claudeBridge.on("error", ({ stream_id, error }) => {
|
|
206
|
-
client.send({ type: "claude.error", stream_id, error })
|
|
208
|
+
claudeBridge.on("error", ({ stream_id, session_id, error }) => {
|
|
209
|
+
client.send({ type: "claude.error", stream_id, session_id, error })
|
|
207
210
|
})
|
|
208
211
|
}
|
|
209
212
|
|