@cocorograph/hub-agent 0.8.14 → 0.8.16
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 +33 -4
- package/src/dispatch-scheduler.mjs +51 -1
- package/src/main.mjs +65 -2
package/package.json
CHANGED
|
@@ -1408,11 +1408,20 @@ export class ClaudeStreamSession {
|
|
|
1408
1408
|
* メッセージ本文。drain 由来の emit でのみ渡す。frontend はこれを user バブルへ
|
|
1409
1409
|
* 昇格させる。キャンセル / 追加由来の emit では空 (昇格させない)。これにより
|
|
1410
1410
|
* 「先頭の pending をキャンセルした」のを「実行開始した」と誤認しなくなる (0.6.26)。
|
|
1411
|
-
* @param {{force?: boolean}} [opts]
|
|
1412
|
-
* emit する。再アタッチ時のキュー
|
|
1413
|
-
*
|
|
1411
|
+
* @param {{force?: boolean, startedIds?: (string|null)[], discarded?: string[]}} [opts]
|
|
1412
|
+
* force=true のとき署名重複チェックを無視して必ず emit する。再アタッチ時のキュー
|
|
1413
|
+
* snapshot 再送 (キュー再表示バグ修正, 0.6.30) に使う。署名が前回と同一でも browser が
|
|
1414
|
+
* ライブ配信を取りこぼしている可能性があるため。
|
|
1415
|
+
* discarded は「実行せずに捨てた input_id」(close 時の pending 破棄)。browser はこれを
|
|
1416
|
+
* 受けてチップを消さず未配信印を付ける (無言削除の根治, 2026-07-31)。 */
|
|
1414
1417
|
_emitQueueState(started = [], opts = undefined) {
|
|
1415
1418
|
const force = opts?.force === true
|
|
1419
|
+
// 無言削除の根治 (2026-07-31): 実行せずに捨てた input_id。空配列は載せない
|
|
1420
|
+
// (旧 browser との後方互換 = 未知フィールドは無視されるが、無駄なノイズも出さない)。
|
|
1421
|
+
const discarded =
|
|
1422
|
+
Array.isArray(opts?.discarded) && opts.discarded.length > 0
|
|
1423
|
+
? [...opts.discarded]
|
|
1424
|
+
: undefined
|
|
1416
1425
|
// 実行開始した項目の input_id (started と parallel。T06603 cross-review major#2)。
|
|
1417
1426
|
// 本文テキストだけだと、同一本文を連投したときに 2 通目のユーザー発言バブルが
|
|
1418
1427
|
// 「既に同じ本文のバブルがある」と誤判定されて永久に出ない。id で突合させる。
|
|
@@ -1446,6 +1455,8 @@ export class ClaudeStreamSession {
|
|
|
1446
1455
|
// items は id 付きでキャンセルボタンの対象特定に使う (0.6.26)。
|
|
1447
1456
|
this.onEvent?.({
|
|
1448
1457
|
...this._buildQueueSnapshot(),
|
|
1458
|
+
// 実行せずに捨てた id (close 時の pending 破棄)。載る時だけキーを出す。
|
|
1459
|
+
...(discarded ? { discarded } : {}),
|
|
1449
1460
|
started,
|
|
1450
1461
|
// started と parallel。連携注入なら 'peer'|'orchestrator'、通常は null。
|
|
1451
1462
|
startedSources,
|
|
@@ -2105,12 +2116,30 @@ export class ClaudeStreamSession {
|
|
|
2105
2116
|
close() {
|
|
2106
2117
|
this._closed = true
|
|
2107
2118
|
// 改修3: 未処理の pending を破棄 (セッション終了後に drain しないため)。
|
|
2119
|
+
const discardedIds = this._pendingMessages
|
|
2120
|
+
.map((m) => m.id)
|
|
2121
|
+
.filter((id) => typeof id === "string" && id)
|
|
2108
2122
|
const hadPending = this._pendingMessages.length > 0
|
|
2109
2123
|
this._pendingMessages = []
|
|
2124
|
+
// 無言削除の根治 (2026-07-31): 破棄した id を **受理済みリングから外す**。
|
|
2125
|
+
// browser の 2 軸判定は「キューに無い + 受理済み = 実行完了」なので、破棄した id を
|
|
2126
|
+
// accepted に残したまま空 snapshot を送ると、**実行していない送信を実行完了と誤認して
|
|
2127
|
+
// チップを無言削除**する (ユーザーが送った本文が痕跡なく消える)。accepted から外し、
|
|
2128
|
+
// かつ discarded として明示して「捨てた」ことを browser に伝える。
|
|
2129
|
+
for (const id of discardedIds) {
|
|
2130
|
+
const at = this._acceptedInputIds.indexOf(id)
|
|
2131
|
+
if (at !== -1) this._acceptedInputIds.splice(at, 1)
|
|
2132
|
+
}
|
|
2110
2133
|
// T06603 (2026-07-25): 破棄したことを必ず通知する。従来は無言で捨てていたため、
|
|
2111
2134
|
// 「セッションが畳まれた後もチップだけが残る」表示が残留していた。破棄は状態変化なので
|
|
2112
2135
|
// force で貫通させる (started=[] なのでバブル昇格は起きない)。
|
|
2113
|
-
if (hadPending)
|
|
2136
|
+
if (hadPending) {
|
|
2137
|
+
this.logger?.info(
|
|
2138
|
+
{ stream_id: this.stream_id, discarded: discardedIds.length },
|
|
2139
|
+
"pending messages discarded on close",
|
|
2140
|
+
)
|
|
2141
|
+
this._emitQueueState([], { force: true, discarded: discardedIds })
|
|
2142
|
+
}
|
|
2114
2143
|
// 常駐 query は input キューを閉じて generator を終わらせ query を完走させる。
|
|
2115
2144
|
if (this._inputQueue) this._inputQueue.close()
|
|
2116
2145
|
this.abortTurn()
|
|
@@ -23,10 +23,44 @@ const LIFECYCLE_TYPES = new Set([
|
|
|
23
23
|
|
|
24
24
|
const MAX_STREAM_LANE_KEYS = 4096
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* dispatch の所要時間ログを出す閾値 (ms)。
|
|
28
|
+
*
|
|
29
|
+
* ⚠️ これは計測のために足したもので、性能を変えるものではない。
|
|
30
|
+
* 「セッションの停止に 10 秒経っても応答が返らない」の中身が **一切観測できなかった**
|
|
31
|
+
* ため入れた (2026-07-31)。kill 実体は実測 0.00 秒・レーン内の tmux 処理も 0.1 秒未満
|
|
32
|
+
* なので、遅延の候補は「レーンの順番待ち」か「そもそも届いていない」に絞られるが、
|
|
33
|
+
* どちらなのかを判別する材料がログに 1 行も無かった。
|
|
34
|
+
*
|
|
35
|
+
* 全 dispatch を出すとログが膨れるので、閾値超えのものだけ出す。
|
|
36
|
+
*/
|
|
37
|
+
const SLOW_DISPATCH_MS = 1000
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 閾値に関係なく必ず所要時間を出す type。
|
|
41
|
+
*
|
|
42
|
+
* 停止 (kill) は低頻度なのでログ量の心配が無く、かつ「遅い / 返ってこない」の
|
|
43
|
+
* 調査対象そのものなので、速かった場合も含めて毎回記録する
|
|
44
|
+
* (速いことの記録が無いと「詰まっていない」を示せない)。
|
|
45
|
+
*/
|
|
46
|
+
const ALWAYS_TIMED_TYPES = new Set(["tmux.kill_session"])
|
|
47
|
+
|
|
26
48
|
export function isConnectionLifecycleMessage(type) {
|
|
27
49
|
return LIFECYCLE_TYPES.has(type)
|
|
28
50
|
}
|
|
29
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 所要時間ログを出すべきか。テストから直接検査できるよう export する。
|
|
54
|
+
*
|
|
55
|
+
* @param {string} type メッセージ種別
|
|
56
|
+
* @param {number} waitMs レーンで順番待ちしていた時間
|
|
57
|
+
* @param {number} runMs ハンドラ本体の実行時間
|
|
58
|
+
*/
|
|
59
|
+
export function shouldLogDispatchTiming(type, waitMs, runMs) {
|
|
60
|
+
if (ALWAYS_TIMED_TYPES.has(type)) return true
|
|
61
|
+
return waitMs >= SLOW_DISPATCH_MS || runMs >= SLOW_DISPATCH_MS
|
|
62
|
+
}
|
|
63
|
+
|
|
30
64
|
function chatMode(type) {
|
|
31
65
|
if (typeof type !== "string") return null
|
|
32
66
|
if (type === "claude.attach" || type.startsWith("claude.")) return "claude"
|
|
@@ -179,6 +213,8 @@ export class DispatchScheduler {
|
|
|
179
213
|
enqueue(msg) {
|
|
180
214
|
const type = msg?.type
|
|
181
215
|
const sequence = ++this.sequence
|
|
216
|
+
// 順番待ち時間の起点。bypass 経路は待たないので測らない。
|
|
217
|
+
const enqueuedAt = Date.now()
|
|
182
218
|
if (this.shouldBypass(type) || isConnectionLifecycleMessage(type)) {
|
|
183
219
|
return this._runNow(msg)
|
|
184
220
|
}
|
|
@@ -217,7 +253,21 @@ export class DispatchScheduler {
|
|
|
217
253
|
)
|
|
218
254
|
return
|
|
219
255
|
}
|
|
220
|
-
|
|
256
|
+
// レーンの順番待ち時間と、ハンドラ本体の実行時間を分けて測る。
|
|
257
|
+
// この 2 つを分けないと「遅いのは順番待ちか処理そのものか」が判別できない。
|
|
258
|
+
const startedAt = Date.now()
|
|
259
|
+
const waitMs = startedAt - enqueuedAt
|
|
260
|
+
try {
|
|
261
|
+
await this.run(msg)
|
|
262
|
+
} finally {
|
|
263
|
+
const runMs = Date.now() - startedAt
|
|
264
|
+
if (shouldLogDispatchTiming(type, waitMs, runMs)) {
|
|
265
|
+
this.logger?.info?.(
|
|
266
|
+
{ type, lane, wait_ms: waitMs, run_ms: runMs },
|
|
267
|
+
"dispatch timing",
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
221
271
|
})
|
|
222
272
|
.catch((err) => {
|
|
223
273
|
this.logger?.error?.(
|
package/src/main.mjs
CHANGED
|
@@ -295,6 +295,36 @@ async function loadClaudeSdk(logger) {
|
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
/** アカウント利用量取得の時間制限 (ms)。超えたらレーンを解放して次へ進む。 */
|
|
299
|
+
export const ACCOUNT_USAGE_TIMEOUT_MS = 8_000
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* `promise` に時間制限を設ける。制限を超えたら `null` で解決し `onTimeout` を呼ぶ。
|
|
303
|
+
*
|
|
304
|
+
* ⚠️ **元の promise はキャンセルできない**ので、裏では走り続ける。ここでの目的は
|
|
305
|
+
* 「返ってこない処理に dispatch レーンを占有させ続けない」ことであり、処理そのものを
|
|
306
|
+
* 止めることではない。時間切れを黙って握りつぶさず呼び出し側でログに残すこと
|
|
307
|
+
* (時間切れの発生回数と頻度が、詰まりの実測値そのものになる)。
|
|
308
|
+
*
|
|
309
|
+
* テストから直接呼べるよう export する。
|
|
310
|
+
*/
|
|
311
|
+
export function withDeadline(promise, timeoutMs, onTimeout) {
|
|
312
|
+
let timer
|
|
313
|
+
return Promise.race([
|
|
314
|
+
Promise.resolve(promise).finally(() => clearTimeout(timer)),
|
|
315
|
+
new Promise((resolve) => {
|
|
316
|
+
timer = setTimeout(() => {
|
|
317
|
+
try {
|
|
318
|
+
onTimeout?.()
|
|
319
|
+
} catch {
|
|
320
|
+
/* ログ失敗で本筋を止めない */
|
|
321
|
+
}
|
|
322
|
+
resolve(null)
|
|
323
|
+
}, timeoutMs)
|
|
324
|
+
}),
|
|
325
|
+
])
|
|
326
|
+
}
|
|
327
|
+
|
|
298
328
|
/**
|
|
299
329
|
* B7: 直列 dispatchChain をバイパスして即時処理してよい高頻度・低レイテンシ経路かを判定する。
|
|
300
330
|
* - pty 出力データ (pty.data) と resize (pty.resize): 高頻度・順序保証不要。
|
|
@@ -3242,13 +3272,33 @@ export async function handleKillSession(msg, ctx) {
|
|
|
3242
3272
|
})
|
|
3243
3273
|
return
|
|
3244
3274
|
}
|
|
3275
|
+
// ⚠️ kill の受信・完了は必ずログに残す (2026-07-31 追加)。
|
|
3276
|
+
// 「停止に 10 秒経っても応答が返らない」の調査で、agent 側に kill の痕跡が
|
|
3277
|
+
// 1 行も無く、遅いのか・届いていないのか・返事が捨てられたのかを**判別できなかった**。
|
|
3278
|
+
// kill は低頻度なのでログ量の懸念は無い。速かった場合も記録する
|
|
3279
|
+
// (速いことの記録が無いと「agent は詰まっていない」を示せない)。
|
|
3280
|
+
ctx.logger?.info?.({ names, request_id: msg.request_id }, "tmux kill_session received")
|
|
3281
|
+
const killStartedAt = Date.now()
|
|
3245
3282
|
const r = await killFn(names)
|
|
3246
|
-
|
|
3283
|
+
const killMs = Date.now() - killStartedAt
|
|
3284
|
+
const sent = ctx.client.send({
|
|
3247
3285
|
type: "tmux.kill_session.result",
|
|
3248
3286
|
request_id: msg.request_id,
|
|
3249
3287
|
killed: r.killed,
|
|
3250
3288
|
failed: r.failed,
|
|
3251
3289
|
})
|
|
3290
|
+
// 送信できたか自体を残す。ws が閉じていると result は黙って捨てられるため、
|
|
3291
|
+
// 「kill は成功したのに要求元へ返っていない」を後から特定できるようにする。
|
|
3292
|
+
ctx.logger?.info?.(
|
|
3293
|
+
{
|
|
3294
|
+
request_id: msg.request_id,
|
|
3295
|
+
killed: r.killed.length,
|
|
3296
|
+
failed: r.failed.length,
|
|
3297
|
+
kill_ms: killMs,
|
|
3298
|
+
result_sent: sent === true,
|
|
3299
|
+
},
|
|
3300
|
+
"tmux kill_session done",
|
|
3301
|
+
)
|
|
3252
3302
|
for (const name of r.killed) {
|
|
3253
3303
|
try {
|
|
3254
3304
|
ctx.client.send({ type: "session.gone", session_name: name })
|
|
@@ -4926,7 +4976,20 @@ async function dispatch(msg, ctx) {
|
|
|
4926
4976
|
// は no-op) ので、既存の statusLine/transcript フォールバックは無傷で残る。
|
|
4927
4977
|
if (ctx.claudeBridge) {
|
|
4928
4978
|
try {
|
|
4929
|
-
|
|
4979
|
+
// ⚠️ getAccountUsage は SDK のサブプロセスを spawn する。**時間制限が無いと
|
|
4980
|
+
// 返ってこないときに control レーン全体が止まる** (usage.request は 60 秒周期で
|
|
4981
|
+
// control レーンに乗り、そこにセッション停止も並ぶ)。同種の head-of-line
|
|
4982
|
+
// blocking で cold p95 40〜54 秒を計測した記録が isReadOnlyRpc の注記にある。
|
|
4983
|
+
// 時間切れは握りつぶさず warn で残す (これ自体が「詰まりの実測値」になる)。
|
|
4984
|
+
const acct = await withDeadline(
|
|
4985
|
+
ctx.claudeBridge.getAccountUsage(),
|
|
4986
|
+
ACCOUNT_USAGE_TIMEOUT_MS,
|
|
4987
|
+
() =>
|
|
4988
|
+
ctx.logger?.warn?.(
|
|
4989
|
+
{ timeout_ms: ACCOUNT_USAGE_TIMEOUT_MS },
|
|
4990
|
+
"account usage timed out (dispatch lane released)",
|
|
4991
|
+
),
|
|
4992
|
+
)
|
|
4930
4993
|
if (acct) recordChatUsageSnapshot(acct, Date.now(), activeClaudeProfile.id)
|
|
4931
4994
|
} catch (err) {
|
|
4932
4995
|
ctx.logger?.debug?.({ err: err?.message }, "account usage snapshot skipped")
|