@cocorograph/hub-agent 0.8.34 → 0.8.36

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.34",
3
+ "version": "0.8.36",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの Claude / Codex チャットと tmux セッションを中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -390,6 +390,20 @@ function subagentFallbackName(agentPath) {
390
390
  return agentPath.split("/").filter(Boolean).at(-1) || "subagent"
391
391
  }
392
392
 
393
+ function explicitSubagentRoleFromThread(thread, expectedParentThreadId) {
394
+ const threadSpawn = thread?.source?.subAgent?.thread_spawn
395
+ if (
396
+ threadSpawn &&
397
+ (typeof threadSpawn.parent_thread_id !== "string" ||
398
+ threadSpawn.parent_thread_id !== expectedParentThreadId)
399
+ ) return null
400
+ const directRole = typeof thread?.agentRole === "string" ? thread.agentRole.trim() : ""
401
+ if (directRole) return directRole
402
+ const sourceRole = threadSpawn?.agent_role
403
+ const normalizedSourceRole = typeof sourceRole === "string" ? sourceRole.trim() : ""
404
+ return normalizedSourceRole || null
405
+ }
406
+
393
407
  function parseDynamicToolArguments(value) {
394
408
  if (value && typeof value === "object" && !Array.isArray(value)) return value
395
409
  if (typeof value !== "string") return null
@@ -748,8 +762,8 @@ export class CodexStreamBridge extends EventEmitter {
748
762
  * deliveryId: string, sourceStreamId: string, deliveryPending: boolean,
749
763
  * deliveryPromise: Promise<object>, settleDelivery: Function}>} */
750
764
  pendingInjectSources: [],
751
- /** 組み込み collaboration subagent の表示用台帳。ライフサイクルの安定キーは
752
- * agentThreadId(item.id は dynamicToolCall とのメタデータ結合にだけ使う)。 */
765
+ /** 組み込み collaboration subagent の表示用台帳。検索キーは agentThreadId、
766
+ * 外部へ出す安定した委譲 ID は subAgentActivity の lifecycle ID を保持する。 */
753
767
  subagents: new Map(),
754
768
  /** dynamicToolCall(spawn_agent) の item.id -> 表示メタデータ。 */
755
769
  subagentSpawnMetadata: new Map(),
@@ -1460,6 +1474,12 @@ export class CodexStreamBridge extends EventEmitter {
1460
1474
  const list = session?.pendingInjectSources
1461
1475
  if (!list || !list.length) return null
1462
1476
  if (msg?.method !== "item/completed") return null
1477
+ const sourceThreadId = msg.params?.threadId
1478
+ if (
1479
+ typeof sourceThreadId === "string" &&
1480
+ sourceThreadId &&
1481
+ sourceThreadId !== session?.threadId
1482
+ ) return null
1463
1483
  const item = msg.params?.item
1464
1484
  if (!item || item.type !== "userMessage") return null
1465
1485
  const content = item.content
@@ -1582,13 +1602,15 @@ export class CodexStreamBridge extends EventEmitter {
1582
1602
  notification: {
1583
1603
  method: "cockpit/subagent_state",
1584
1604
  params: {
1585
- delegationId: record.agentThreadId,
1605
+ delegationId: record.delegationId,
1586
1606
  agentThreadId: record.agentThreadId,
1587
1607
  agentPath: record.agentPath,
1588
1608
  phase: record.phase,
1589
1609
  agent: record.agent,
1610
+ agentExplicit: record.agentExplicit === true,
1590
1611
  description: record.description,
1591
1612
  prompt: record.prompt,
1613
+ background: record.background === true,
1592
1614
  atMs: record.atMs,
1593
1615
  },
1594
1616
  },
@@ -1609,6 +1631,14 @@ export class CodexStreamBridge extends EventEmitter {
1609
1631
  ? details.agentPath
1610
1632
  : previous?.agentPath || ""
1611
1633
  const metadata = details.metadata || null
1634
+ const delegationId =
1635
+ previous?.delegationId ||
1636
+ (typeof details.delegationId === "string" && details.delegationId
1637
+ ? details.delegationId
1638
+ : null)
1639
+ // raw child thread ID を委譲 ID に代用しない。spawn lifecycle 由来の安定 ID が無い
1640
+ // 通知は台帳へ登録せず、後続 item も unknown として未タグのまま扱う。
1641
+ if (typeof delegationId !== "string" || !delegationId) return null
1612
1642
  const transitionAtMs =
1613
1643
  previous?.phase === phase
1614
1644
  ? previous.atMs
@@ -1616,10 +1646,12 @@ export class CodexStreamBridge extends EventEmitter {
1616
1646
  ? details.atMs
1617
1647
  : Date.now()
1618
1648
  const record = {
1649
+ delegationId,
1619
1650
  agentThreadId,
1620
1651
  agentPath,
1621
1652
  phase,
1622
1653
  agent: metadata?.agent || previous?.agent || subagentFallbackName(agentPath),
1654
+ agentExplicit: metadata?.agentExplicit === true || previous?.agentExplicit === true,
1623
1655
  description:
1624
1656
  metadata?.description || previous?.description || subagentFallbackName(agentPath),
1625
1657
  prompt: metadata?.prompt || previous?.prompt || "",
@@ -1638,15 +1670,30 @@ export class CodexStreamBridge extends EventEmitter {
1638
1670
  return record
1639
1671
  }
1640
1672
 
1641
- _observeSubagentItem(session, item, atMs = Date.now(), { emit = true } = {}) {
1673
+ _observeSubagentItem(
1674
+ session,
1675
+ item,
1676
+ atMs = Date.now(),
1677
+ { emit = true, sourceThreadId = session?.threadId } = {},
1678
+ ) {
1642
1679
  if (!item || typeof item !== "object") return
1680
+ // root thread の collaboration item だけが root subagent 台帳の正本。子 thread 内の
1681
+ // spawn/subAgentActivity(孫)を root の lifecycle/snapshot へ昇格しない。
1682
+ if (
1683
+ typeof sourceThreadId === "string" &&
1684
+ sourceThreadId &&
1685
+ sourceThreadId !== session?.threadId
1686
+ ) return
1643
1687
  if (item.type === "dynamicToolCall") {
1644
1688
  const namespace = item.namespace
1645
1689
  if (namespace !== "collaboration" || item.tool !== "spawn_agent" || !item.id) return
1646
1690
  const args = parseDynamicToolArguments(item.arguments)
1647
1691
  if (!args) return
1692
+ const agent = typeof args.agent_type === "string" ? args.agent_type.trim() : ""
1693
+ const agentExplicit = agent.length > 0
1648
1694
  const metadata = {
1649
- agent: typeof args.agent_type === "string" && args.agent_type ? args.agent_type : null,
1695
+ agent: agentExplicit ? agent : null,
1696
+ agentExplicit,
1650
1697
  description: typeof args.task_name === "string" && args.task_name ? args.task_name : null,
1651
1698
  prompt: typeof args.message === "string" ? args.message : null,
1652
1699
  background: args.background === true,
@@ -1674,6 +1721,7 @@ export class CodexStreamBridge extends EventEmitter {
1674
1721
  agentThreadId,
1675
1722
  phase,
1676
1723
  {
1724
+ delegationId: item.id,
1677
1725
  agentPath: item.agentPath,
1678
1726
  metadata,
1679
1727
  atMs,
@@ -1736,10 +1784,11 @@ export class CodexStreamBridge extends EventEmitter {
1736
1784
  const startedHistoryIndex = startedIndexByThread.get(record.agentThreadId)
1737
1785
  if (startedHistoryIndex === undefined) continue
1738
1786
  snapshots.push({
1739
- delegationId: record.agentThreadId,
1787
+ delegationId: record.delegationId,
1740
1788
  agentThreadId: record.agentThreadId,
1741
1789
  agentPath: record.agentPath,
1742
1790
  agent: record.agent,
1791
+ agentExplicit: record.agentExplicit === true,
1743
1792
  description: record.description,
1744
1793
  prompt: record.prompt,
1745
1794
  background: record.background === true,
@@ -1773,6 +1822,10 @@ export class CodexStreamBridge extends EventEmitter {
1773
1822
  // RPC 待ち中に live lifecycle が進んだ場合、stale read で上書き・削除しない。
1774
1823
  if (session.subagents.get(record.agentThreadId) !== record) continue
1775
1824
  const thread = result?.thread
1825
+ const explicitRole = explicitSubagentRoleFromThread(thread, session.threadId)
1826
+ const metadata = explicitRole
1827
+ ? { agent: explicitRole, agentExplicit: true }
1828
+ : null
1776
1829
  const turns = Array.isArray(thread?.turns) ? thread.turns : []
1777
1830
  const lastTurn = turns.at(-1)
1778
1831
  const phase = terminalSubagentPhase(lastTurn?.status)
@@ -1787,7 +1840,7 @@ export class CodexStreamBridge extends EventEmitter {
1787
1840
  session,
1788
1841
  record.agentThreadId,
1789
1842
  phase,
1790
- { agentPath: record.agentPath, atMs: lastTurnAtMs },
1843
+ { agentPath: record.agentPath, metadata, atMs: lastTurnAtMs },
1791
1844
  { emit: false },
1792
1845
  )
1793
1846
  } else if (
@@ -1800,6 +1853,7 @@ export class CodexStreamBridge extends EventEmitter {
1800
1853
  "started",
1801
1854
  {
1802
1855
  agentPath: record.agentPath,
1856
+ metadata,
1803
1857
  atMs: record.startedAtMs,
1804
1858
  },
1805
1859
  { emit: false },
@@ -1880,25 +1934,46 @@ export class CodexStreamBridge extends EventEmitter {
1880
1934
  // ※ hub-agent 再起動 (cold resume) では historyItems が揮発するため青バブルへ戻る (claude の
1881
1935
  // jsonl at-rest と同じ許容範囲。source 永続マップは TTL/ID 再利用の複雑さに見合わないため不採用)。
1882
1936
  const eventThreadId = msg?.params?.threadId
1883
- const backgroundChildItemNotification =
1884
- typeof eventThreadId === "string" &&
1885
- typeof msg?.method === "string" &&
1886
- msg.method.startsWith("item/") &&
1887
- session.subagents.get(eventThreadId)?.background === true
1888
- if (backgroundChildItemNotification) {
1889
- // 背景子の item は root の履歴・バブルへ混ぜない。子自身の lifecycle は
1890
- // turn 通知と root 側 subAgentActivity が正本なので、子 item から孫を登録しない。
1891
- // foreground / background 不明(spawn record 生成前の短い窓を含む)は fail-open で
1892
- // 既存どおり root へ通す。背景出力の過剰抑止より既存表示の維持を優先する。
1937
+ const taggedParams = this._tagInjectSourceOnItem(session, msg)
1938
+ let effectiveParams = taggedParams || msg.params
1939
+ const childRecord =
1940
+ typeof eventThreadId === "string" ? session.subagents.get(eventThreadId) : null
1941
+ const childTextDelta =
1942
+ msg.method === "item/agentMessage/delta" ||
1943
+ msg.method === "item/reasoning/summaryTextDelta" ||
1944
+ msg.method === "item/reasoning/textDelta"
1945
+ if (childRecord && childTextDelta) {
1946
+ // 子の message/reasoning 途中 delta を root liveText へ混ぜない。前景・背景とも
1947
+ // attribution 付き item/completed が表示の正本で、root/unknown delta は従来どおり
1948
+ // fail-open で通す。
1893
1949
  return
1894
1950
  }
1895
- const taggedParams = this._tagInjectSourceOnItem(session, msg)
1896
- const effectiveParams = taggedParams || msg.params
1951
+ if (
1952
+ childRecord &&
1953
+ (msg.method === "item/started" || msg.method === "item/completed") &&
1954
+ effectiveParams?.item &&
1955
+ typeof effectiveParams.item === "object"
1956
+ ) {
1957
+ // app-server 所有の item は変更せず、既知の子 thread だけを attribution 付きで複製する。
1958
+ // unknown/root を推測で子扱いしない。delegationId は child thread ID ではなく、spawn
1959
+ // lifecycle が保持する安定 ID を使う。
1960
+ const attributedItem = {
1961
+ ...effectiveParams.item,
1962
+ _hubSubagent: {
1963
+ delegationId: childRecord.delegationId,
1964
+ threadId: eventThreadId,
1965
+ runtime: "codex",
1966
+ background: childRecord.background === true,
1967
+ },
1968
+ }
1969
+ effectiveParams = { ...effectiveParams, item: attributedItem }
1970
+ }
1897
1971
  if (msg.method === "item/started" || msg.method === "item/completed") {
1898
1972
  this._observeSubagentItem(
1899
1973
  session,
1900
1974
  effectiveParams?.item,
1901
1975
  effectiveParams?.completedAtMs || Date.now(),
1976
+ { sourceThreadId: eventThreadId },
1902
1977
  )
1903
1978
  }
1904
1979
  const childTurnNotification =
@@ -1968,8 +2043,8 @@ export class CodexStreamBridge extends EventEmitter {
1968
2043
  void this._pumpInputs(session)
1969
2044
  break
1970
2045
  case "item/completed":
1971
- // T06541 案1: 完了した ThreadItem を高忠実度履歴に追記する(cwd reattach で返す用)。
1972
- // Phase 2: タグ済み item (effectiveParams.item) を積む = reattach でも source が残る。
2046
+ // 完了した ThreadItem を高忠実度履歴に追記する(cwd reattach で返す用)。
2047
+ // attribution/source 済み item (effectiveParams.item) を積み、emit と同じ clone を使う。
1973
2048
  this._appendHistoryItem(session, effectiveParams?.item, effectiveParams?.completedAtMs)
1974
2049
  break
1975
2050
  default:
package/src/main.mjs CHANGED
@@ -2076,6 +2076,7 @@ export function buildCodexReadyPayload(streamId, info, queueState) {
2076
2076
  agent_thread_id: subagent.agentThreadId,
2077
2077
  agent_path: subagent.agentPath,
2078
2078
  agent: subagent.agent,
2079
+ agent_explicit: subagent.agentExplicit === true,
2079
2080
  // description は spawn_agent.task_name の既存 alias(人物の表示名ではない)。
2080
2081
  description: subagent.description,
2081
2082
  prompt: subagent.prompt,