@cocorograph/hub-agent 0.8.102 → 0.8.103
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 +82 -11
package/package.json
CHANGED
package/src/claude-history.mjs
CHANGED
|
@@ -84,7 +84,7 @@ export const DISPLAY_TYPES = new Set(["user", "assistant", "system", "result"])
|
|
|
84
84
|
* 数秒〜数分内に再 hydrate されるケースが大半 (行ったり来たりのセッション切替、複数タブ)。
|
|
85
85
|
* readFile → split → 全行 JSON.parse (+ MASK_ENABLED 時は tool_use 走査) は jsonl が
|
|
86
86
|
* 大きいほど無視できないコストになるため、`mtimeMs + size` が前回と一致する間は
|
|
87
|
-
*
|
|
87
|
+
* 親のパース結果を再利用する。子は毎回読み直して追記・新規出現を反映する。
|
|
88
88
|
* ファイルが変わっていれば (ライブ会話が進行 / 別ターミナルから追記等) 通常どおり再読み込み
|
|
89
89
|
* して上書きするので、常に最新のファイル内容を正しく反映する (stale 表示のリスクは無い)。
|
|
90
90
|
*
|
|
@@ -421,6 +421,7 @@ export function normalizeHistoryEvent(obj) {
|
|
|
421
421
|
if (obj.message !== undefined) event.message = obj.message
|
|
422
422
|
if (obj.subtype !== undefined) event.subtype = obj.subtype
|
|
423
423
|
if (obj.uuid !== undefined) event.uuid = obj.uuid
|
|
424
|
+
if (obj.parent_tool_use_id !== undefined) event.parent_tool_use_id = obj.parent_tool_use_id
|
|
424
425
|
// jsonl 各行の ISO8601 タイムスタンプ。Browser 側でバブル上の時刻表示・日付区切りに
|
|
425
426
|
// 使う。履歴 hydrate と live watch の両経路がこの関数を通るため、ここ 1 箇所で拾えば
|
|
426
427
|
// 両方に時刻が乗る (純粋なライブ SDK stream には timestamp が無く、Browser が受信時刻で
|
|
@@ -443,7 +444,7 @@ const TASK_NOTIFICATION_MARKER = "<task-notification>"
|
|
|
443
444
|
const TASK_NOTIFICATION_CLOSE = "</task-notification>"
|
|
444
445
|
// ヘッダ領域の終わり。ここから先は自由文 (報告全文) なので、鍵と状態の抽出対象にしない。
|
|
445
446
|
const TASK_NOTIFICATION_BODY_TAGS = ["<result>", "<summary>", "<note>"]
|
|
446
|
-
//
|
|
447
|
+
// 合成する鍵と状態は長さでも制限する (実データの tool_use_id は 30〜40 字)。
|
|
447
448
|
const MAX_TOOL_USE_ID_LEN = 200
|
|
448
449
|
const MAX_STATUS_LEN = 64
|
|
449
450
|
|
|
@@ -498,12 +499,10 @@ function firstTagValue(text, tag) {
|
|
|
498
499
|
* **CLI 自身が書いた終端宣言**なので、時間で推測せずに終端を復元できる。
|
|
499
500
|
*
|
|
500
501
|
* ## ⚠️ 載せてよいのは「鍵」と「状態」と「時刻」だけ
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
* 絶対に載せない (載せると秘匿情報がマスクされないまま browser へ流れる)。`<task-id>` も
|
|
506
|
-
* 画面側が使っていないので載せない。
|
|
502
|
+
* maskMessageObject は message の有無を判定する前にイベント全体の秘匿値を伏せる。
|
|
503
|
+
* ただし合成イベントは終端の復元に必要な最小情報に限定し、<result> / <summary> /
|
|
504
|
+
* <note> / <output-file> の本文(報告全文等)は新たに載せない方針を維持する。
|
|
505
|
+
* <task-id> も画面側が使っていないので載せない。
|
|
507
506
|
*
|
|
508
507
|
* 画面側 (Hub 本体 frontend/lib/cockpit/agentRequest.ts の applyTaskNotificationEvent) は
|
|
509
508
|
* `type:"system" / subtype:"task_notification" / tool_use_id / status` を終端として処理し、
|
|
@@ -714,6 +713,67 @@ export async function deleteSession({
|
|
|
714
713
|
return { ok: true }
|
|
715
714
|
}
|
|
716
715
|
|
|
716
|
+
/** 親の取得窓内の背景依頼だけに、直接の子 transcript を結合する。 */
|
|
717
|
+
async function attachBackgroundSubagentHistory(filePath, { result, requests }) {
|
|
718
|
+
if (requests.size === 0) return result
|
|
719
|
+
const subagentsDir = path.join(filePath.slice(0, -".jsonl".length), "subagents")
|
|
720
|
+
let names
|
|
721
|
+
try {
|
|
722
|
+
names = await readdir(subagentsDir)
|
|
723
|
+
} catch {
|
|
724
|
+
return result // subagents 不在は正常系。
|
|
725
|
+
}
|
|
726
|
+
const children = new Map()
|
|
727
|
+
for (const name of names) {
|
|
728
|
+
if (!/^agent-.+\.meta\.json$/.test(name)) continue
|
|
729
|
+
try {
|
|
730
|
+
const meta = JSON.parse(await readFile(path.join(subagentsDir, name), "utf-8"))
|
|
731
|
+
const toolUseId = meta?.toolUseId
|
|
732
|
+
if (typeof toolUseId !== "string" || !toolUseId || !requests.has(toolUseId)) continue
|
|
733
|
+
const text = await readFile(path.join(subagentsDir, name.replace(/\.meta\.json$/, ".jsonl")), "utf-8")
|
|
734
|
+
// 子は実行中にも読まれるので、追記途中の「未完の末尾行」だけは捨てて完成済みの行を残す。
|
|
735
|
+
// ⚠️ 途中の行が壊れている場合は子ごと捨てる。壊れた行が tool_use だと、後続の tool_result を
|
|
736
|
+
// 伏せるためのツール名が引けず、非公開本文 (get_skill_prompt 等) がマスクを素通りするため。
|
|
737
|
+
// 末尾行は後続の行を持たないので、捨ててもこの露出は起きない。
|
|
738
|
+
const lines = text.split("\n").filter((line) => line.trim())
|
|
739
|
+
const rows = []
|
|
740
|
+
for (let i = 0; i < lines.length; i++) {
|
|
741
|
+
try {
|
|
742
|
+
rows.push(JSON.parse(lines[i]))
|
|
743
|
+
} catch (err) {
|
|
744
|
+
if (i === lines.length - 1) break
|
|
745
|
+
throw err
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
const toolNames = new Map()
|
|
749
|
+
if (MASK_ENABLED) {
|
|
750
|
+
for (const row of rows) collectToolUseNames(row, toolNames)
|
|
751
|
+
}
|
|
752
|
+
const events = []
|
|
753
|
+
for (const row of rows) {
|
|
754
|
+
if (!DISPLAY_TYPES.has(row?.type)) continue
|
|
755
|
+
// 孫の依頼は親の取得窓にないため再帰しない。子から終端イベントも合成しない。
|
|
756
|
+
let event = normalizeHistoryEvent({ ...row, parent_tool_use_id: toolUseId })
|
|
757
|
+
if (MASK_ENABLED) event = maskMessageObject(event, toolNames)
|
|
758
|
+
events.push(event)
|
|
759
|
+
}
|
|
760
|
+
const index = requests.get(toolUseId)
|
|
761
|
+
if (!children.has(index)) children.set(index, [])
|
|
762
|
+
for (const event of events) children.get(index).push(event)
|
|
763
|
+
} catch {
|
|
764
|
+
// 不完全な meta / transcript は推定で紐付けず、その子だけスキップする。
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (children.size === 0) return result
|
|
768
|
+
const events = []
|
|
769
|
+
for (let i = 0; i < result.events.length; i++) {
|
|
770
|
+
events.push(result.events[i])
|
|
771
|
+
// 追加取得が先頭イベントを境界にするため、時刻ソートせず親直後に置く。
|
|
772
|
+
for (const child of children.get(i) || []) events.push(child)
|
|
773
|
+
}
|
|
774
|
+
return { ...result, events }
|
|
775
|
+
}
|
|
776
|
+
|
|
717
777
|
/**
|
|
718
778
|
* jsonl 1 ファイルを読んで UI 表示用イベント配列を返す。
|
|
719
779
|
*
|
|
@@ -739,7 +799,7 @@ export async function readSessionHistory(filePath, { maxLines = MAX_HISTORY_LINE
|
|
|
739
799
|
throw err
|
|
740
800
|
}
|
|
741
801
|
const cached = getHydrateCache(filePath, st.mtimeMs, st.size, maxLines)
|
|
742
|
-
if (cached) return cached
|
|
802
|
+
if (cached) return attachBackgroundSubagentHistory(filePath, cached)
|
|
743
803
|
|
|
744
804
|
const readStarted = performance.now()
|
|
745
805
|
let text
|
|
@@ -774,6 +834,7 @@ export async function readSessionHistory(filePath, { maxLines = MAX_HISTORY_LINE
|
|
|
774
834
|
}
|
|
775
835
|
|
|
776
836
|
const events = []
|
|
837
|
+
const requests = new Map()
|
|
777
838
|
for (const line of slice) {
|
|
778
839
|
let obj
|
|
779
840
|
try {
|
|
@@ -785,6 +846,15 @@ export async function readSessionHistory(filePath, { maxLines = MAX_HISTORY_LINE
|
|
|
785
846
|
// historyEventFromLine 1 箇所に集約 (live watch も同じ関数を通る)。
|
|
786
847
|
let event = historyEventFromLine(obj)
|
|
787
848
|
if (!event) continue
|
|
849
|
+
if (obj.type === "assistant" && Array.isArray(obj.message?.content)) {
|
|
850
|
+
for (const block of obj.message.content) {
|
|
851
|
+
// 作業ログの依頼カードを持つ背景依頼だけが対象。前景は取り込まない。
|
|
852
|
+
if (block?.type === "tool_use" && (block.name === "Agent" || block.name === "Task")
|
|
853
|
+
&& block.input?.run_in_background === true && typeof block.id === "string" && block.id) {
|
|
854
|
+
requests.set(block.id, events.length)
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
788
858
|
// 秘匿情報 / 非公開スキルプロンプト本文を hydrate 時にも伏せる (古いセッション再表示対策)。
|
|
789
859
|
if (MASK_ENABLED) event = maskMessageObject(event, toolNames)
|
|
790
860
|
events.push(event)
|
|
@@ -795,8 +865,9 @@ export async function readSessionHistory(filePath, { maxLines = MAX_HISTORY_LINE
|
|
|
795
865
|
read_ms: parseStarted - readStarted, parse_ms: finished - parseStarted, total_ms: finished - readStarted,
|
|
796
866
|
})
|
|
797
867
|
const result = { events, total_lines, truncated }
|
|
798
|
-
|
|
799
|
-
|
|
868
|
+
const parent = { result, requests }
|
|
869
|
+
setHydrateCache(filePath, st.mtimeMs, st.size, maxLines, parent)
|
|
870
|
+
return attachBackgroundSubagentHistory(filePath, parent)
|
|
800
871
|
}
|
|
801
872
|
|
|
802
873
|
/**
|