@cocorograph/hub-agent 0.8.102 → 0.8.104

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.8.102",
3
+ "version": "0.8.104",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -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
- // マスクを素通りする経路なので、長さでも歯止めを掛ける (実データの tool_use_id は 30〜40 字)。
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
- * `<result>` にはサブエージェントの報告全文が入る (実データで確認済み)。しかしマスク処理
502
- * `maskMessageObject` (src/tool-result-mask.mjs:165-168) は `const message = obj.message;
503
- * if (!message …) return obj` で **即 return** するため、`message` を持たないこの合成イベントは
504
- * **マスクを素通りする**。よって `<result>` / `<summary>` / `<note>` / `<output-file>` の本文は
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
- setHydrateCache(filePath, st.mtimeMs, st.size, maxLines, result)
799
- return result
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
  /**
@@ -51,8 +51,8 @@
51
51
  * session 起動を止めない(ensureClaudeMd と同じく best-effort)。
52
52
  *
53
53
  * 関連: backend Stage 1 / 1.5
54
- * GET /api/admin/project/claude-md-by-dir/?dir_name=...
55
- * PUT /api/admin/project/claude-md-by-dir/?dir_name=... body: { claude_md, expected_hash? }
54
+ * GET /api/user/project/claude-md-by-dir/?dir_name=...
55
+ * PUT /api/user/project/claude-md-by-dir/?dir_name=... body: { claude_md, expected_hash? }
56
56
  * expected_hash は楽観ロック。GET で読んだ hash を必ず添えて送る(不一致なら 409)。
57
57
  * **空文字も有効なトークン**(「Hub が空である」ことを期待する)。falsy 判定で
58
58
  * 落とすとキーごと消えて照合されず、盲目的な上書きになる。
@@ -106,7 +106,7 @@ function cliKindForConfig(configFilename) {
106
106
  */
107
107
  function claudeMdByDirUrl({ hubUrl, dirName, cliKind }) {
108
108
  const base =
109
- `${hubUrl.replace(/\/+$/, "")}/api/admin/project/claude-md-by-dir/` +
109
+ `${hubUrl.replace(/\/+$/, "")}/api/user/project/claude-md-by-dir/` +
110
110
  `?dir_name=${encodeURIComponent(dirName)}`
111
111
  return cliKind ? `${base}&cli_kind=${encodeURIComponent(cliKind)}` : base
112
112
  }