@cocorograph/hub-agent 0.5.28 → 0.5.29
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/main.mjs +48 -0
- package/src/ws-client.mjs +39 -16
package/package.json
CHANGED
package/src/main.mjs
CHANGED
|
@@ -403,6 +403,45 @@ export async function maybeSyncClaudeSettings(msg, ctx) {
|
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Plan ε: `agent.streams.sync.response` を受けて backend ↔ agent の現存
|
|
408
|
+
* stream_id を能動同期する。
|
|
409
|
+
*
|
|
410
|
+
* - local にあって backend にない → ptyBridge.detach で即時 kill (孤児)
|
|
411
|
+
* - backend にあって local にない → backend に pty.error を通知 (browser に
|
|
412
|
+
* "pty 死んでます" を表示するため)
|
|
413
|
+
*
|
|
414
|
+
* dispatch から呼ぶ用のヘルパーだが、テストから直接叩けるよう export している。
|
|
415
|
+
*/
|
|
416
|
+
export function handleStreamsSyncResponse(msg, ctx) {
|
|
417
|
+
const activeIds = new Set(
|
|
418
|
+
Array.isArray(msg?.active_stream_ids) ? msg.active_stream_ids : [],
|
|
419
|
+
)
|
|
420
|
+
const localIds = new Set(ctx.ptyBridge.list())
|
|
421
|
+
for (const sid of localIds) {
|
|
422
|
+
if (!activeIds.has(sid)) {
|
|
423
|
+
ctx.logger?.warn?.(
|
|
424
|
+
{ stream_id: sid, request_id: msg?.request_id },
|
|
425
|
+
"Plan ε: orphan stream detected, detaching",
|
|
426
|
+
)
|
|
427
|
+
ctx.ptyBridge.detach({ stream_id: sid })
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
for (const sid of activeIds) {
|
|
431
|
+
if (!localIds.has(sid)) {
|
|
432
|
+
ctx.logger?.warn?.(
|
|
433
|
+
{ stream_id: sid, request_id: msg?.request_id },
|
|
434
|
+
"Plan ε: stream missing on agent, notifying backend (pty.error)",
|
|
435
|
+
)
|
|
436
|
+
ctx.client.send({
|
|
437
|
+
type: "pty.error",
|
|
438
|
+
stream_id: sid,
|
|
439
|
+
error: "stream_not_found_on_agent",
|
|
440
|
+
})
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
406
445
|
async function dispatch(msg, ctx) {
|
|
407
446
|
const t = msg?.type || ""
|
|
408
447
|
try {
|
|
@@ -456,6 +495,15 @@ async function dispatch(msg, ctx) {
|
|
|
456
495
|
case "pty.detach":
|
|
457
496
|
ctx.ptyBridge.detach({ stream_id: msg.stream_id })
|
|
458
497
|
return
|
|
498
|
+
case "agent.streams.sync.response":
|
|
499
|
+
// Plan ε: backend ↔ agent の現存 stream_id 能動同期 (WS reconnect 直後に
|
|
500
|
+
// ws-client が agent.streams.sync.request を送って取得した結果)。
|
|
501
|
+
// ロジックは handleStreamsSyncResponse へ抽出 (テストから直接呼べる)。
|
|
502
|
+
// backend が旧 (= Plan ε 未対応) で response が一切来ないケースは何もしない。
|
|
503
|
+
// 5s タイムアウトのような明示的な timer は持たず、再接続時に再度 request
|
|
504
|
+
// が飛ぶので無害。
|
|
505
|
+
handleStreamsSyncResponse(msg, ctx)
|
|
506
|
+
return
|
|
459
507
|
case "tmux.exec": {
|
|
460
508
|
const args = Array.isArray(msg.args) ? msg.args : []
|
|
461
509
|
const hookResult = await runHookChain(ctx.plugins, "interceptTmuxExec", {
|
package/src/ws-client.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* 仕様書: ナレッジ/インフラ/cockpit-hub-hosted-integration-spec (id=6080)
|
|
13
13
|
*/
|
|
14
|
+
import { randomUUID } from "node:crypto"
|
|
14
15
|
import { EventEmitter } from "node:events"
|
|
15
16
|
import fs from "node:fs"
|
|
16
17
|
import os from "node:os"
|
|
@@ -68,22 +69,7 @@ export class WsClient extends EventEmitter {
|
|
|
68
69
|
const ws = new WebSocket(wsUrl, { headers })
|
|
69
70
|
this.ws = ws
|
|
70
71
|
|
|
71
|
-
ws.on("open", () =>
|
|
72
|
-
this.backoff = MIN_BACKOFF_MS
|
|
73
|
-
this.logger?.info("ws open")
|
|
74
|
-
// hello 前に最新の bundle version を読み直す (再接続時の鮮度確保)
|
|
75
|
-
this._refreshBundleVersion()
|
|
76
|
-
this._sendJson({
|
|
77
|
-
type: "hello",
|
|
78
|
-
agent_id: this.config.agent_id,
|
|
79
|
-
hostname: this.hostname,
|
|
80
|
-
version: this.version,
|
|
81
|
-
bundle_version: this.bundleVersion,
|
|
82
|
-
})
|
|
83
|
-
this._startHeartbeat()
|
|
84
|
-
this._startBundleWatcher()
|
|
85
|
-
this.emit("open")
|
|
86
|
-
})
|
|
72
|
+
ws.on("open", () => this._onOpen())
|
|
87
73
|
|
|
88
74
|
ws.on("message", (data) => {
|
|
89
75
|
let msg
|
|
@@ -111,6 +97,43 @@ export class WsClient extends EventEmitter {
|
|
|
111
97
|
})
|
|
112
98
|
}
|
|
113
99
|
|
|
100
|
+
/**
|
|
101
|
+
* WS open 時の初期送信 + 後続タスク起動。
|
|
102
|
+
*
|
|
103
|
+
* - backoff をリセット
|
|
104
|
+
* - bundle version を最新化 → hello 送信
|
|
105
|
+
* - Plan ε: agent.streams.sync.request を送信 (毎 reconnect で能動同期)
|
|
106
|
+
* - heartbeat + bundle watcher を起動
|
|
107
|
+
*
|
|
108
|
+
* test からも直接呼べるように method として切り出している (`ws.on("open")`
|
|
109
|
+
* のクロージャだと spy しづらい)。
|
|
110
|
+
*/
|
|
111
|
+
_onOpen() {
|
|
112
|
+
this.backoff = MIN_BACKOFF_MS
|
|
113
|
+
this.logger?.info("ws open")
|
|
114
|
+
// hello 前に最新の bundle version を読み直す (再接続時の鮮度確保)
|
|
115
|
+
this._refreshBundleVersion()
|
|
116
|
+
this._sendJson({
|
|
117
|
+
type: "hello",
|
|
118
|
+
agent_id: this.config.agent_id,
|
|
119
|
+
hostname: this.hostname,
|
|
120
|
+
version: this.version,
|
|
121
|
+
bundle_version: this.bundleVersion,
|
|
122
|
+
})
|
|
123
|
+
// Plan ε: 毎回 reconnect 直後に backend ↔ agent の stream_id を能動同期する
|
|
124
|
+
// (orphan stream の即時 kill 用)。response は main.mjs の dispatch が拾い、
|
|
125
|
+
// ptyBridge.list() と差分を取って kill する。request_id は uuid で
|
|
126
|
+
// 重複排除。backend が古い (= Plan ε 未対応) なら "unknown_type" error が
|
|
127
|
+
// 返るだけで動作には影響しない。
|
|
128
|
+
this._sendJson({
|
|
129
|
+
type: "agent.streams.sync.request",
|
|
130
|
+
request_id: randomUUID(),
|
|
131
|
+
})
|
|
132
|
+
this._startHeartbeat()
|
|
133
|
+
this._startBundleWatcher()
|
|
134
|
+
this.emit("open")
|
|
135
|
+
}
|
|
136
|
+
|
|
114
137
|
/** メッセージを送る。未接続なら no-op (logger.warn)。 */
|
|
115
138
|
send(obj) {
|
|
116
139
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|