@cocorograph/hub-agent 0.7.16 → 0.7.17
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 +23 -1
- package/src/usage.mjs +136 -0
package/package.json
CHANGED
package/src/main.mjs
CHANGED
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
getSessionUsages,
|
|
92
92
|
getUsage,
|
|
93
93
|
recordChatRateLimit,
|
|
94
|
+
turnActiveForCwd,
|
|
94
95
|
} from "./usage.mjs"
|
|
95
96
|
import {
|
|
96
97
|
clearChatSignal,
|
|
@@ -1298,6 +1299,21 @@ function startStateLoop({ client, plugins, logger, intervalMs, claudeBridge, rea
|
|
|
1298
1299
|
stable,
|
|
1299
1300
|
now: nowMs,
|
|
1300
1301
|
})
|
|
1302
|
+
|
|
1303
|
+
// turn_active: ターンが現在進行中か (true=生成/ツール実行中 / false=待機中 / null=不明)。
|
|
1304
|
+
// CockpitStatusDot の青/赤の権威ソース。サイドバーのドットと送信/停止ボタンを同じ真実で
|
|
1305
|
+
// 同期させ、Esc 中断後の青固着 (PROMPT_STALE_SHORT_MS=60s) を解消する。
|
|
1306
|
+
// - チャットモード: SDK の rate_limit_event/assistant が最も新鮮なので chat.status を採用
|
|
1307
|
+
// - TUI モード: jsonl 末尾 assistant.stop_reason から判定 (turnActiveForCwd)
|
|
1308
|
+
let turnActive = null
|
|
1309
|
+
if (chat?.status === "processing") {
|
|
1310
|
+
turnActive = true
|
|
1311
|
+
} else if (chat?.status === "waiting" || chat?.status === "idle") {
|
|
1312
|
+
turnActive = false
|
|
1313
|
+
} else if (s.cwd) {
|
|
1314
|
+
turnActive = await turnActiveForCwd(s.cwd)
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1301
1317
|
const prev = lastByName.get(s.session_name)
|
|
1302
1318
|
if (
|
|
1303
1319
|
!prev ||
|
|
@@ -1307,7 +1323,8 @@ function startStateLoop({ client, plugins, logger, intervalMs, claudeBridge, rea
|
|
|
1307
1323
|
prev.stable !== stable ||
|
|
1308
1324
|
prev.proc_busy !== procBusy ||
|
|
1309
1325
|
prev.child_busy !== childBusy ||
|
|
1310
|
-
prev.stalled !== stalled
|
|
1326
|
+
prev.stalled !== stalled ||
|
|
1327
|
+
prev.turn_active !== turnActive
|
|
1311
1328
|
) {
|
|
1312
1329
|
lastByName.set(s.session_name, {
|
|
1313
1330
|
status,
|
|
@@ -1317,6 +1334,7 @@ function startStateLoop({ client, plugins, logger, intervalMs, claudeBridge, rea
|
|
|
1317
1334
|
proc_busy: procBusy,
|
|
1318
1335
|
child_busy: childBusy,
|
|
1319
1336
|
stalled,
|
|
1337
|
+
turn_active: turnActive,
|
|
1320
1338
|
})
|
|
1321
1339
|
// 計装 (2026-06-19, 既定 OFF=2026-06-20): 状態変化時に値を記録する。常時 info はログ肥大の
|
|
1322
1340
|
// 主因のため HUB_AGENT_STATE_TRACE=1 のときだけ出す。childBusy/outputActive を分けて出し、
|
|
@@ -1333,6 +1351,7 @@ function startStateLoop({ client, plugins, logger, intervalMs, claudeBridge, rea
|
|
|
1333
1351
|
armed,
|
|
1334
1352
|
stable,
|
|
1335
1353
|
context_pct: contextPct,
|
|
1354
|
+
turn_active: turnActive,
|
|
1336
1355
|
},
|
|
1337
1356
|
"session.state push",
|
|
1338
1357
|
)
|
|
@@ -1349,6 +1368,9 @@ function startStateLoop({ client, plugins, logger, intervalMs, claudeBridge, rea
|
|
|
1349
1368
|
// 出力残り火 (proc_busy 残存) でアイドルなのにキューへ逃がす誤キュー (症状A) を断つために使う。
|
|
1350
1369
|
child_busy: childBusy,
|
|
1351
1370
|
stalled,
|
|
1371
|
+
// turn_active: ステータスドット 4 状態統一の権威ソース。null は旧クライアント互換
|
|
1372
|
+
// (frontend は欠落時に旧 prompt_submit/stale 降格ロジックへフォールバック)。
|
|
1373
|
+
turn_active: turnActive,
|
|
1352
1374
|
})
|
|
1353
1375
|
}
|
|
1354
1376
|
// 消滅セッションの GC (perf監査/メモリリーク対策): tmux から消えたセッションの状態 Map を
|
package/src/usage.mjs
CHANGED
|
@@ -657,6 +657,142 @@ export function _resetJsonlSessionUuids() {
|
|
|
657
657
|
_jsonlSessionUuidByCwd.clear()
|
|
658
658
|
}
|
|
659
659
|
|
|
660
|
+
// ターン完了を示す Anthropic API `stop_reason`。これ以外 (代表的には `tool_use`)
|
|
661
|
+
// は「ターン継続中 = 生成 / ツール処理中」とみなす。
|
|
662
|
+
const TURN_END_STOP_REASONS = new Set([
|
|
663
|
+
"end_turn",
|
|
664
|
+
"stop_sequence",
|
|
665
|
+
"max_tokens",
|
|
666
|
+
])
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* jsonl の末尾を順走査してターンが現在 active かを返す。
|
|
670
|
+
*
|
|
671
|
+
* ステートマシン:
|
|
672
|
+
* - `assistant` 行を見たら `stop_reason` でターン状態を更新する
|
|
673
|
+
* - end_turn / stop_sequence / max_tokens → 完了 (active=false)
|
|
674
|
+
* - tool_use (or それ以外) → 継続 (active=true)
|
|
675
|
+
* - `user` 行を見たとき、それが「実ユーザー入力」(tool_result でない) かつ
|
|
676
|
+
* 直前ターンが完了済みだったら、新ターンが開始されたので active=true
|
|
677
|
+
* - その他の型 (attachment / mode / hook_* / file-history-snapshot 等) は無視
|
|
678
|
+
*
|
|
679
|
+
* 初期値は「ターン完了状態」(lastTurnEnded=true)。空 jsonl や応答未着 のセッション
|
|
680
|
+
* は active=false でフォールバックする。
|
|
681
|
+
*
|
|
682
|
+
* @param {string} text jsonl の (末尾) テキスト
|
|
683
|
+
* @returns {boolean}
|
|
684
|
+
*/
|
|
685
|
+
export function scanTurnActive(text) {
|
|
686
|
+
if (!text) return false
|
|
687
|
+
let active = false
|
|
688
|
+
let lastTurnEnded = true
|
|
689
|
+
const lines = text.split("\n")
|
|
690
|
+
for (const line of lines) {
|
|
691
|
+
if (!line) continue
|
|
692
|
+
let d
|
|
693
|
+
try {
|
|
694
|
+
d = JSON.parse(line)
|
|
695
|
+
} catch {
|
|
696
|
+
continue
|
|
697
|
+
}
|
|
698
|
+
if (d.type === "assistant") {
|
|
699
|
+
const stop = d.message?.stop_reason
|
|
700
|
+
if (stop && TURN_END_STOP_REASONS.has(stop)) {
|
|
701
|
+
active = false
|
|
702
|
+
lastTurnEnded = true
|
|
703
|
+
} else {
|
|
704
|
+
// tool_use / pause_turn / null / 未知 → ターン継続中扱い (安全側)
|
|
705
|
+
active = true
|
|
706
|
+
lastTurnEnded = false
|
|
707
|
+
}
|
|
708
|
+
} else if (d.type === "user") {
|
|
709
|
+
// 「実ユーザー入力」と「tool_result の自動 user ラッパー」を区別する。
|
|
710
|
+
// tool_result が含まれる user は前ターンの一部 (claude code 内部生成) なので、
|
|
711
|
+
// ターン状態を変えない。実ユーザー入力 (text/image/document) が直前ターン完了後に
|
|
712
|
+
// 来たら新ターン開始 → active=true。
|
|
713
|
+
const content = Array.isArray(d.message?.content) ? d.message.content : []
|
|
714
|
+
const isToolResult = content.some((c) => c?.type === "tool_result")
|
|
715
|
+
if (!isToolResult && lastTurnEnded) {
|
|
716
|
+
active = true
|
|
717
|
+
lastTurnEnded = false
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return active
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** P5: turnActiveForCwd のメモ (newest fp + mtime + size をキー)。 */
|
|
725
|
+
const _turnActiveMemo = new Map()
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* 指定 cwd の Claude セッション jsonl から「現在ターンが進行中か」を返す。
|
|
729
|
+
*
|
|
730
|
+
* `CockpitStatusDot` の中央ドットの判定権威ソース。サイドバーのドット (非選択
|
|
731
|
+
* セッション) と送信/停止ボタン (選択セッション、`turnActive` ストリーム由来) を
|
|
732
|
+
* 同じ真実で同期させ、Esc 中断後の「青固着 60s」を解消する。
|
|
733
|
+
*
|
|
734
|
+
* 動作:
|
|
735
|
+
* 1. cwd → encoded dir → 最新 mtime の *.jsonl を選ぶ
|
|
736
|
+
* 2. 末尾 256KB を tail 読みし `scanTurnActive` でターン状態を判定
|
|
737
|
+
* 3. tail で結論が出ない巨大ツール結果ケースのみ全文 read にフォールバック
|
|
738
|
+
*
|
|
739
|
+
* @param {string} cwd
|
|
740
|
+
* @returns {Promise<boolean | null>} true=生成中 / false=待機中 / null=jsonl 不在
|
|
741
|
+
*/
|
|
742
|
+
export async function turnActiveForCwd(cwd) {
|
|
743
|
+
if (!cwd) return null
|
|
744
|
+
const dirName = encodeCwdToDirName(cwd)
|
|
745
|
+
if (!dirName) return null
|
|
746
|
+
const dir = path.join(projectsDir(), dirName)
|
|
747
|
+
const files = await fs.readdir(dir).catch(() => null)
|
|
748
|
+
if (!files) return null
|
|
749
|
+
|
|
750
|
+
let newest = null
|
|
751
|
+
await Promise.all(
|
|
752
|
+
files.map(async (f) => {
|
|
753
|
+
if (!f.endsWith(".jsonl")) return
|
|
754
|
+
const fp = path.join(dir, f)
|
|
755
|
+
const st = await fs.stat(fp).catch(() => null)
|
|
756
|
+
if (!st) return
|
|
757
|
+
if (!newest || st.mtimeMs > newest.mtimeMs) {
|
|
758
|
+
newest = { fp, mtimeMs: st.mtimeMs, size: st.size }
|
|
759
|
+
}
|
|
760
|
+
}),
|
|
761
|
+
)
|
|
762
|
+
if (!newest) return null
|
|
763
|
+
|
|
764
|
+
const memo = _turnActiveMemo.get(newest.fp)
|
|
765
|
+
if (memo && memo.mtimeMs === newest.mtimeMs && memo.size === newest.size) {
|
|
766
|
+
return memo.result
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// 256KB tail で多くの場合は決着がつく (1 ターン分の assistant + tool_use/result サイクル) 。
|
|
770
|
+
// tail 内に assistant 行が 0 件のみのケース (= 巨大 tool_result で先頭を tail から外れた)
|
|
771
|
+
// に備え、結果が「全行で active 状態が変化しない」場合は全文読みにフォールバックする。
|
|
772
|
+
const tail = await readTail(newest.fp, 256 * 1024)
|
|
773
|
+
let active = false
|
|
774
|
+
if (tail != null) {
|
|
775
|
+
active = scanTurnActive(tail)
|
|
776
|
+
}
|
|
777
|
+
// tail で active=false の場合のみ、巨大 tool_result で先頭の assistant 行が
|
|
778
|
+
// 落ちて誤って完了扱いになっていないか全文で確認する (高コストだが必要時のみ)。
|
|
779
|
+
if (active === false) {
|
|
780
|
+
const full = await readOrNull(newest.fp)
|
|
781
|
+
if (full != null) active = scanTurnActive(full)
|
|
782
|
+
}
|
|
783
|
+
_turnActiveMemo.set(newest.fp, {
|
|
784
|
+
mtimeMs: newest.mtimeMs,
|
|
785
|
+
size: newest.size,
|
|
786
|
+
result: active,
|
|
787
|
+
})
|
|
788
|
+
return active
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** テスト用: turnActive メモをクリアする。 */
|
|
792
|
+
export function _resetTurnActiveMemo() {
|
|
793
|
+
_turnActiveMemo.clear()
|
|
794
|
+
}
|
|
795
|
+
|
|
660
796
|
/**
|
|
661
797
|
* 5h / 7d 集計の UsageStats を返す。statusLine が無い環境では transcript
|
|
662
798
|
* から推定する。
|