@cocorograph/hub-agent 0.8.35 → 0.8.37

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.35",
3
+ "version": "0.8.37",
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
+ Object.prototype.hasOwnProperty.call(threadSpawn, "parent_thread_id") &&
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
@@ -749,10 +763,16 @@ export class CodexStreamBridge extends EventEmitter {
749
763
  * deliveryPromise: Promise<object>, settleDelivery: Function}>} */
750
764
  pendingInjectSources: [],
751
765
  /** 組み込み collaboration subagent の表示用台帳。検索キーは agentThreadId、
752
- * 外部へ出す安定した委譲 ID は spawn dynamicToolCall の item.id を保持する。 */
766
+ * 外部へ出す安定した委譲 ID は subAgentActivity の lifecycle ID を保持する。 */
753
767
  subagents: new Map(),
754
768
  /** dynamicToolCall(spawn_agent) の item.id -> 表示メタデータ。 */
755
769
  subagentSpawnMetadata: new Map(),
770
+ /** live subAgentActivity の spawn metadata miss を child thread/read で補完する
771
+ * single-flight 台帳。key = agentThreadId, value = 進行中 Promise。 */
772
+ subagentRoleReads: new Map(),
773
+ /** role 補完を試行済みの child thread。取得不能・親不一致・RPC失敗後の後続 activity で
774
+ * 無制限に read/warn を再発火しない negative cache(session と同じ寿命)。 */
775
+ subagentRoleReadAttempted: new Set(),
756
776
  /** 実行中の背景コマンド(commandExecution)の台帳。claude 側 `_liveTasks` と同型。
757
777
  * key = `item.id`(`item/started` / `item/completed` の両方に載る唯一の安定 ID)、
758
778
  * value = { threadId, startedAt, lastSeenAt }。
@@ -1593,6 +1613,7 @@ export class CodexStreamBridge extends EventEmitter {
1593
1613
  agentPath: record.agentPath,
1594
1614
  phase: record.phase,
1595
1615
  agent: record.agent,
1616
+ agentExplicit: record.agentExplicit === true,
1596
1617
  description: record.description,
1597
1618
  prompt: record.prompt,
1598
1619
  background: record.background === true,
@@ -1636,6 +1657,7 @@ export class CodexStreamBridge extends EventEmitter {
1636
1657
  agentPath,
1637
1658
  phase,
1638
1659
  agent: metadata?.agent || previous?.agent || subagentFallbackName(agentPath),
1660
+ agentExplicit: metadata?.agentExplicit === true || previous?.agentExplicit === true,
1639
1661
  description:
1640
1662
  metadata?.description || previous?.description || subagentFallbackName(agentPath),
1641
1663
  prompt: metadata?.prompt || previous?.prompt || "",
@@ -1654,6 +1676,80 @@ export class CodexStreamBridge extends EventEmitter {
1654
1676
  return record
1655
1677
  }
1656
1678
 
1679
+ _recoverLiveSubagentRole(session, record, itemId) {
1680
+ if (!session || !record || record.agentExplicit === true || session.dead) return
1681
+ const agentThreadId = record.agentThreadId
1682
+ if (
1683
+ session.subagentRoleReads.has(agentThreadId) ||
1684
+ session.subagentRoleReadAttempted.has(agentThreadId)
1685
+ ) return
1686
+ session.subagentRoleReadAttempted.add(agentThreadId)
1687
+ this.logger?.warn?.(
1688
+ {
1689
+ item_id: itemId,
1690
+ parent_thread_id: session.threadId,
1691
+ subagent_thread_id: agentThreadId,
1692
+ },
1693
+ "codex live subagent spawn metadata miss; recovering role from child thread",
1694
+ )
1695
+ const attempt = (async () => {
1696
+ let result
1697
+ try {
1698
+ result = await session.client.request("thread/read", {
1699
+ threadId: agentThreadId,
1700
+ includeTurns: false,
1701
+ })
1702
+ } catch {
1703
+ this.logger?.warn?.(
1704
+ {
1705
+ item_id: itemId,
1706
+ parent_thread_id: session.threadId,
1707
+ subagent_thread_id: agentThreadId,
1708
+ },
1709
+ "codex live subagent role recovery thread/read failed",
1710
+ )
1711
+ return
1712
+ }
1713
+ if (session.dead) return
1714
+ const current = session.subagents.get(agentThreadId)
1715
+ if (!current || current.agentExplicit === true) return
1716
+ const explicitRole = explicitSubagentRoleFromThread(result?.thread, session.threadId)
1717
+ if (!explicitRole) return
1718
+ const updated = this._setSubagentPhase(
1719
+ session,
1720
+ agentThreadId,
1721
+ current.phase,
1722
+ {
1723
+ agentPath: current.agentPath,
1724
+ metadata: { agent: explicitRole, agentExplicit: true },
1725
+ atMs: current.atMs,
1726
+ },
1727
+ { force: true },
1728
+ )
1729
+ // 成功後は record.agentExplicit が再試行を止めるため negative cache は不要。
1730
+ if (updated?.agentExplicit === true) {
1731
+ session.subagentRoleReadAttempted.delete(agentThreadId)
1732
+ }
1733
+ })()
1734
+ const pending = attempt
1735
+ .catch(() => {
1736
+ this.logger?.warn?.(
1737
+ {
1738
+ item_id: itemId,
1739
+ parent_thread_id: session.threadId,
1740
+ subagent_thread_id: agentThreadId,
1741
+ },
1742
+ "codex live subagent role recovery failed",
1743
+ )
1744
+ })
1745
+ .finally(() => {
1746
+ if (session.subagentRoleReads.get(agentThreadId) === pending) {
1747
+ session.subagentRoleReads.delete(agentThreadId)
1748
+ }
1749
+ })
1750
+ session.subagentRoleReads.set(agentThreadId, pending)
1751
+ }
1752
+
1657
1753
  _observeSubagentItem(
1658
1754
  session,
1659
1755
  item,
@@ -1673,8 +1769,11 @@ export class CodexStreamBridge extends EventEmitter {
1673
1769
  if (namespace !== "collaboration" || item.tool !== "spawn_agent" || !item.id) return
1674
1770
  const args = parseDynamicToolArguments(item.arguments)
1675
1771
  if (!args) return
1772
+ const agent = typeof args.agent_type === "string" ? args.agent_type.trim() : ""
1773
+ const agentExplicit = agent.length > 0
1676
1774
  const metadata = {
1677
- agent: typeof args.agent_type === "string" && args.agent_type ? args.agent_type : null,
1775
+ agent: agentExplicit ? agent : null,
1776
+ agentExplicit,
1678
1777
  description: typeof args.task_name === "string" && args.task_name ? args.task_name : null,
1679
1778
  prompt: typeof args.message === "string" ? args.message : null,
1680
1779
  background: args.background === true,
@@ -1697,7 +1796,7 @@ export class CodexStreamBridge extends EventEmitter {
1697
1796
  }
1698
1797
  if (!phase) return
1699
1798
  const metadata = session.subagentSpawnMetadata.get(item.id) || null
1700
- this._setSubagentPhase(
1799
+ const record = this._setSubagentPhase(
1701
1800
  session,
1702
1801
  agentThreadId,
1703
1802
  phase,
@@ -1709,6 +1808,11 @@ export class CodexStreamBridge extends EventEmitter {
1709
1808
  },
1710
1809
  { emit },
1711
1810
  )
1811
+ // metadata が存在して agentExplicit=false なら agent_type 未指定という明示入力。
1812
+ // thread/read 補完は metadata lookup 自体が外れた場合だけに限定し、その指定を上書きしない。
1813
+ if (emit && record && !metadata) {
1814
+ this._recoverLiveSubagentRole(session, record, item.id)
1815
+ }
1712
1816
  }
1713
1817
 
1714
1818
  _observeSubagentTurn(session, msg) {
@@ -1769,6 +1873,7 @@ export class CodexStreamBridge extends EventEmitter {
1769
1873
  agentThreadId: record.agentThreadId,
1770
1874
  agentPath: record.agentPath,
1771
1875
  agent: record.agent,
1876
+ agentExplicit: record.agentExplicit === true,
1772
1877
  description: record.description,
1773
1878
  prompt: record.prompt,
1774
1879
  background: record.background === true,
@@ -1802,6 +1907,10 @@ export class CodexStreamBridge extends EventEmitter {
1802
1907
  // RPC 待ち中に live lifecycle が進んだ場合、stale read で上書き・削除しない。
1803
1908
  if (session.subagents.get(record.agentThreadId) !== record) continue
1804
1909
  const thread = result?.thread
1910
+ const explicitRole = explicitSubagentRoleFromThread(thread, session.threadId)
1911
+ const metadata = explicitRole
1912
+ ? { agent: explicitRole, agentExplicit: true }
1913
+ : null
1805
1914
  const turns = Array.isArray(thread?.turns) ? thread.turns : []
1806
1915
  const lastTurn = turns.at(-1)
1807
1916
  const phase = terminalSubagentPhase(lastTurn?.status)
@@ -1816,7 +1925,7 @@ export class CodexStreamBridge extends EventEmitter {
1816
1925
  session,
1817
1926
  record.agentThreadId,
1818
1927
  phase,
1819
- { agentPath: record.agentPath, atMs: lastTurnAtMs },
1928
+ { agentPath: record.agentPath, metadata, atMs: lastTurnAtMs },
1820
1929
  { emit: false },
1821
1930
  )
1822
1931
  } else if (
@@ -1829,6 +1938,7 @@ export class CodexStreamBridge extends EventEmitter {
1829
1938
  "started",
1830
1939
  {
1831
1940
  agentPath: record.agentPath,
1941
+ metadata,
1832
1942
  atMs: record.startedAtMs,
1833
1943
  },
1834
1944
  { emit: false },
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,