@cocorograph/hub-agent 0.8.60 → 0.8.62
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-history.mjs +70 -0
- package/src/dispatch-scheduler.mjs +4 -0
- package/src/main.mjs +27 -3
package/package.json
CHANGED
package/src/claude-history.mjs
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* - 上限 MAX_HISTORY_LINES (デフォルト 500) で末尾から切る
|
|
16
16
|
* - ファイル不在 (新規セッション) なら空配列を返す (エラーにしない)
|
|
17
17
|
*/
|
|
18
|
+
import { randomUUID } from "node:crypto"
|
|
19
|
+
import { constants as fsConstants } from "node:fs"
|
|
18
20
|
import { open, readFile, readdir, stat } from "node:fs/promises"
|
|
19
21
|
import os from "node:os"
|
|
20
22
|
import path from "node:path"
|
|
@@ -576,6 +578,74 @@ export function jsonlPath({ cwd, session_id, projectsRoot }) {
|
|
|
576
578
|
return path.join(root, encodeCwdToDirName(cwd), `${session_id}.jsonl`)
|
|
577
579
|
}
|
|
578
580
|
|
|
581
|
+
/**
|
|
582
|
+
* Claude CLI の `/rename` と同じ custom-title レコードを追記する。
|
|
583
|
+
* ターミナルでの改名と整合させ、表示名の抽出経路を 1 本に保つため同じ形式を使う。
|
|
584
|
+
*
|
|
585
|
+
* @param {{cwd: string, session_id: string, title: unknown, projectsRoot?: string, logger?: import('pino').Logger, openImpl?: typeof open}} args
|
|
586
|
+
* `openImpl` はテスト専用(stat と write の間に別 writer を割り込ませる競合再現に使う)。本番では渡さない。
|
|
587
|
+
* @returns {Promise<{ok: true, custom_title: string}|{ok: false, error: string}>}
|
|
588
|
+
*/
|
|
589
|
+
export async function renameSession({ cwd, session_id, title, projectsRoot, logger, openImpl = open }) {
|
|
590
|
+
if (typeof cwd !== "string" || cwd.length === 0) return { ok: false, error: "cwd required" }
|
|
591
|
+
if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(session_id)) {
|
|
592
|
+
return { ok: false, error: "invalid session_id" }
|
|
593
|
+
}
|
|
594
|
+
if (typeof title !== "string") return { ok: false, error: "title must be a string" }
|
|
595
|
+
|
|
596
|
+
const customTitle = title.trim().replace(/\s+/g, " ")
|
|
597
|
+
if (Array.from(customTitle).length > 200) return { ok: false, error: "title too long" }
|
|
598
|
+
|
|
599
|
+
const filePath = jsonlPath({ cwd, session_id, projectsRoot })
|
|
600
|
+
let handle
|
|
601
|
+
try {
|
|
602
|
+
// ⚠️ O_CREAT を含めない。`stat` → `appendFile(path)` の 2 段にすると、その間に履歴掃除や
|
|
603
|
+
// 別プロセスが jsonl を消したとき appendFile が「custom-title だけの jsonl」を新規作成して
|
|
604
|
+
// 削除済みセッションを復活させる(cross-review 1 巡目)。存在確認・末尾読取・追記を同じ
|
|
605
|
+
// handle で行い、作成経路をそもそも持たない。
|
|
606
|
+
// ⚠️ O_APPEND を付ける。"r+" の明示 offset 書込みは「stat 時点の末尾」への上書きなので、
|
|
607
|
+
// stat と write の間に Claude CLI が会話レコードを追記するとそれを潰す(cross-review 2 巡目)。
|
|
608
|
+
// O_APPEND なら kernel が write 時点の末尾へ原子的に置く。
|
|
609
|
+
handle = await openImpl(filePath, fsConstants.O_RDWR | fsConstants.O_APPEND)
|
|
610
|
+
const fileStat = await handle.stat()
|
|
611
|
+
if (!fileStat.isFile()) return { ok: false, error: "session not found" }
|
|
612
|
+
|
|
613
|
+
let prefix = ""
|
|
614
|
+
if (fileStat.size > 0) {
|
|
615
|
+
const tail = Buffer.allocUnsafe(1)
|
|
616
|
+
// 読取は O_APPEND の影響を受けないので明示 offset で末尾 1 バイトを見る。
|
|
617
|
+
const { bytesRead } = await handle.read(tail, 0, 1, fileStat.size - 1)
|
|
618
|
+
if (bytesRead === 1 && tail[0] !== 0x0a) prefix = "\n"
|
|
619
|
+
}
|
|
620
|
+
const record = {
|
|
621
|
+
type: "custom-title",
|
|
622
|
+
customTitle,
|
|
623
|
+
sessionId: session_id,
|
|
624
|
+
uuid: randomUUID(),
|
|
625
|
+
timestamp: new Date().toISOString(),
|
|
626
|
+
}
|
|
627
|
+
// 1 レコードを単一 write に保つ(CLI 側の追記と行が交錯しても改行境界は壊れない)。
|
|
628
|
+
// 末尾改行の確認後に別 writer が追記した場合は空行が 1 つ増えうるが、reader は空行を無視する。
|
|
629
|
+
const line = Buffer.from(`${prefix}${JSON.stringify(record)}\n`)
|
|
630
|
+
// position は null(O_APPEND により write 時点の末尾)。short write は再送しない(重複・
|
|
631
|
+
// 破損の方が害が大きい)。
|
|
632
|
+
const { bytesWritten } = await handle.write(line, 0, line.length, null)
|
|
633
|
+
if (bytesWritten !== line.length) {
|
|
634
|
+
logger?.warn({ bytesWritten, expected: line.length, filePath }, "claude session rename short write")
|
|
635
|
+
return { ok: false, error: "rename failed" }
|
|
636
|
+
}
|
|
637
|
+
return { ok: true, custom_title: customTitle }
|
|
638
|
+
} catch (err) {
|
|
639
|
+
// クライアントへ返す文言は固定語に限定する。Node の生メッセージ(絶対パス・ユーザー名を
|
|
640
|
+
// 含みうる)は logger だけに残す。
|
|
641
|
+
const notFound = err?.code === "ENOENT" || err?.code === "EISDIR"
|
|
642
|
+
logger?.warn({ err: err?.message || String(err), code: err?.code, filePath }, "claude session rename failed")
|
|
643
|
+
return { ok: false, error: notFound ? "session not found" : "rename failed" }
|
|
644
|
+
} finally {
|
|
645
|
+
await handle?.close().catch(() => {})
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
579
649
|
/**
|
|
580
650
|
* jsonl 1 ファイルを読んで UI 表示用イベント配列を返す。
|
|
581
651
|
*
|
|
@@ -159,6 +159,10 @@ export class DispatchScheduler {
|
|
|
159
159
|
const mutationMode = globalMutationMode(type)
|
|
160
160
|
if (mutationMode) return `${mutationMode}:global`
|
|
161
161
|
|
|
162
|
+
// usage build は最大 25 秒かかり、同じ control lane の tmux.list_sessions が
|
|
163
|
+
// frontend の 10 秒打ち切りで捨てられていたため分離する (2026-09-04 本番計測)。
|
|
164
|
+
if (type === "usage.request") return "usage"
|
|
165
|
+
|
|
162
166
|
const mode = chatMode(type)
|
|
163
167
|
if (!mode) return "control"
|
|
164
168
|
|
package/src/main.mjs
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
drainTaskNotificationEnvelopeDrops,
|
|
62
62
|
fetchSessionHistory,
|
|
63
63
|
listSessions,
|
|
64
|
+
renameSession,
|
|
64
65
|
} from "./claude-history.mjs"
|
|
65
66
|
import { listAgents } from "./agents.mjs"
|
|
66
67
|
import { listSkills } from "./skills.mjs"
|
|
@@ -3787,9 +3788,11 @@ export async function buildUsagePayload(ctx) {
|
|
|
3787
3788
|
const accountUsageStartedAt = Date.now()
|
|
3788
3789
|
try {
|
|
3789
3790
|
// ⚠️ getAccountUsage は SDK のサブプロセスを spawn する。**時間制限が無いと
|
|
3790
|
-
// 返ってこないときに
|
|
3791
|
-
// control
|
|
3792
|
-
//
|
|
3791
|
+
// 返ってこないときに usage レーンが止まり、後続の usage.request (60 秒周期) が
|
|
3792
|
+
// 無期限に詰まる**。2026-09-04 までは control レーン (tmux.list_sessions や
|
|
3793
|
+
// セッション停止が並ぶ) に乗っていて head-of-line blocking を起こしていたため
|
|
3794
|
+
// dispatch-scheduler の _laneFor で専用 "usage" レーンへ分離した。同種の詰まりで
|
|
3795
|
+
// cold p95 40〜54 秒を計測した記録が isReadOnlyRpc の注記にある。
|
|
3793
3796
|
// 時間切れは握りつぶさず warn で残す (これ自体が「詰まりの実測値」になる)。
|
|
3794
3797
|
const acct = await withDeadline(
|
|
3795
3798
|
ctx.claudeBridge.getAccountUsage(),
|
|
@@ -5008,6 +5011,27 @@ export async function dispatch(msg, ctx) {
|
|
|
5008
5011
|
})
|
|
5009
5012
|
return
|
|
5010
5013
|
}
|
|
5014
|
+
case "claude.session.rename": {
|
|
5015
|
+
// Cockpit ピッカーからの改名。jsonl へ custom-title を追記する書き込み RPC なので
|
|
5016
|
+
// isReadOnlyRpc には載せない(直列 dispatchChain を通す)。
|
|
5017
|
+
const stream_id = msg.stream_id
|
|
5018
|
+
const cwd = msg.cwd || ""
|
|
5019
|
+
const session_id = typeof msg.session_id === "string" ? msg.session_id : ""
|
|
5020
|
+
// request_id は要求ごとに一意(ブラウザが採番)。同一 session への再試行と遅延応答を
|
|
5021
|
+
// ブラウザ側が相関できるよう、そのまま echo する(欠けていれば undefined のまま)。
|
|
5022
|
+
const request_id = typeof msg.request_id === "string" ? msg.request_id : undefined
|
|
5023
|
+
const { projectsRoot, profileId } = await resolveProjectsRoot(msg.profile_id)
|
|
5024
|
+
const result = await renameSession({ cwd, session_id, title: msg.title, projectsRoot, logger: ctx.logger })
|
|
5025
|
+
ctx.client.send({
|
|
5026
|
+
type: "claude.session.rename.response",
|
|
5027
|
+
request_id, stream_id, cwd, session_id,
|
|
5028
|
+
profile_id: profileId,
|
|
5029
|
+
ok: result.ok === true,
|
|
5030
|
+
custom_title: result.ok ? result.custom_title : undefined,
|
|
5031
|
+
error: result.ok ? undefined : result.error,
|
|
5032
|
+
})
|
|
5033
|
+
return
|
|
5034
|
+
}
|
|
5011
5035
|
case "profile.list": {
|
|
5012
5036
|
// Claude アカウント・プロファイル一覧と active を返す (Cockpit のアカウント切替UI用)。
|
|
5013
5037
|
// 各プロファイルの「最後にログインしていたアカウント」(<configDir>/.claude.json の
|