@cocorograph/hub-agent 0.8.33 → 0.8.34
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/codex-stream-bridge.mjs +89 -38
- package/src/main.mjs +40 -24
package/package.json
CHANGED
|
@@ -621,6 +621,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
621
621
|
// 由来で表示がリセットされ得るため、live セッションが保持する高忠実度履歴を返す。
|
|
622
622
|
history,
|
|
623
623
|
historyTruncated: existing.historyTruncated === true,
|
|
624
|
+
subagents: this._subagentSnapshots(existing, history),
|
|
624
625
|
}
|
|
625
626
|
}
|
|
626
627
|
if (r.status === "superseded") {
|
|
@@ -850,8 +851,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
850
851
|
session.historyItems = history.slice()
|
|
851
852
|
session.historyBytes = history.reduce((n, it) => n + estimateItemBytes(it), 0)
|
|
852
853
|
session.historyTruncated = historyTruncated
|
|
853
|
-
if (resuming) await this._restoreSubagentsFromHistory(session,
|
|
854
|
-
if (resuming) this._scheduleSubagentReplay(session)
|
|
854
|
+
if (resuming) await this._restoreSubagentsFromHistory(session, history)
|
|
855
855
|
// 案C: 権威的なターン状態を resume 応答から取り出してフロントへ渡す。
|
|
856
856
|
// これによりフロントは会話末尾からの推測をやめ、Thread.status で turnActive を
|
|
857
857
|
// 決められる (生成中スピナー固着バグの根治)。新規スレッドは常に idle 相当。
|
|
@@ -908,6 +908,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
908
908
|
threadId: session.threadId,
|
|
909
909
|
history,
|
|
910
910
|
historyTruncated,
|
|
911
|
+
subagents: this._subagentSnapshots(session, history),
|
|
911
912
|
threadStatus: turnState.threadStatus,
|
|
912
913
|
lastTurnStatus: turnState.lastTurnStatus,
|
|
913
914
|
turnActive: turnState.turnActive,
|
|
@@ -1078,7 +1079,6 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1078
1079
|
// 出したカードは remount/端末切替で失われ、応答が来ないとターンが永久 pending になる。新 stream へ
|
|
1079
1080
|
// 再 emit してカードを復元させる (frontend は request_id で重複排除するので二重化しない)。
|
|
1080
1081
|
this._replayPendingApprovals(session)
|
|
1081
|
-
this._scheduleSubagentReplay(session)
|
|
1082
1082
|
// 履歴は返さない: reconnect では frontend が表示を保持している (resetDisplay=false) ため
|
|
1083
1083
|
// 再 hydrate 不要。turnActive は session が保持する activeTurnId から権威的に決める。
|
|
1084
1084
|
return {
|
|
@@ -1623,11 +1623,14 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1623
1623
|
description:
|
|
1624
1624
|
metadata?.description || previous?.description || subagentFallbackName(agentPath),
|
|
1625
1625
|
prompt: metadata?.prompt || previous?.prompt || "",
|
|
1626
|
+
// background は spawn 時に確定する属性。後続通知に metadata が無くても true を失わない。
|
|
1627
|
+
background: metadata?.background === true || previous?.background === true,
|
|
1626
1628
|
atMs: transitionAtMs,
|
|
1627
1629
|
startedAtMs:
|
|
1628
1630
|
phase === "started" && terminalSubagentPhase(previous?.phase)
|
|
1629
1631
|
? transitionAtMs
|
|
1630
1632
|
: previous?.startedAtMs ?? transitionAtMs,
|
|
1633
|
+
terminalAtMs: terminalSubagentPhase(phase) ? transitionAtMs : null,
|
|
1631
1634
|
lastEmittedPhase: previous?.lastEmittedPhase || null,
|
|
1632
1635
|
}
|
|
1633
1636
|
session.subagents.set(agentThreadId, record)
|
|
@@ -1646,6 +1649,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1646
1649
|
agent: typeof args.agent_type === "string" && args.agent_type ? args.agent_type : null,
|
|
1647
1650
|
description: typeof args.task_name === "string" && args.task_name ? args.task_name : null,
|
|
1648
1651
|
prompt: typeof args.message === "string" ? args.message : null,
|
|
1652
|
+
background: args.background === true,
|
|
1649
1653
|
}
|
|
1650
1654
|
session.subagentSpawnMetadata.set(item.id, metadata)
|
|
1651
1655
|
while (session.subagentSpawnMetadata.size > 256) {
|
|
@@ -1656,6 +1660,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1656
1660
|
if (item.type !== "subAgentActivity") return
|
|
1657
1661
|
const agentThreadId = item.agentThreadId
|
|
1658
1662
|
if (typeof agentThreadId !== "string" || !agentThreadId) return
|
|
1663
|
+
if (agentThreadId === session.threadId || item.agentPath === "/root") return
|
|
1659
1664
|
const previous = session.subagents.get(agentThreadId)
|
|
1660
1665
|
let phase = null
|
|
1661
1666
|
if (item.kind === "interrupted") phase = "stopped"
|
|
@@ -1668,7 +1673,11 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1668
1673
|
session,
|
|
1669
1674
|
agentThreadId,
|
|
1670
1675
|
phase,
|
|
1671
|
-
{
|
|
1676
|
+
{
|
|
1677
|
+
agentPath: item.agentPath,
|
|
1678
|
+
metadata,
|
|
1679
|
+
atMs,
|
|
1680
|
+
},
|
|
1672
1681
|
{ emit },
|
|
1673
1682
|
)
|
|
1674
1683
|
}
|
|
@@ -1690,49 +1699,65 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1690
1699
|
? Math.round(turn.startedAt * 1000)
|
|
1691
1700
|
: Date.now()
|
|
1692
1701
|
if (msg.method === "turn/started") {
|
|
1693
|
-
this._setSubagentPhase(session, threadId, "started", {
|
|
1702
|
+
this._setSubagentPhase(session, threadId, "started", {
|
|
1703
|
+
agentPath: record.agentPath,
|
|
1704
|
+
atMs,
|
|
1705
|
+
})
|
|
1694
1706
|
} else if (msg.method === "turn/completed") {
|
|
1695
1707
|
const phase = terminalSubagentPhase(msg.params?.turn?.status)
|
|
1696
1708
|
if (phase) {
|
|
1697
|
-
this._setSubagentPhase(session, threadId, phase, {
|
|
1709
|
+
this._setSubagentPhase(session, threadId, phase, {
|
|
1710
|
+
agentPath: record.agentPath,
|
|
1711
|
+
atMs,
|
|
1712
|
+
})
|
|
1698
1713
|
}
|
|
1699
1714
|
}
|
|
1700
1715
|
return true
|
|
1701
1716
|
}
|
|
1702
1717
|
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1718
|
+
_subagentSnapshots(session, history = session?.historyItems) {
|
|
1719
|
+
const items = Array.isArray(history) ? history : []
|
|
1720
|
+
const startedIndexByThread = new Map()
|
|
1721
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
1722
|
+
const item = items[index]
|
|
1723
|
+
if (
|
|
1724
|
+
item?.type === "subAgentActivity" &&
|
|
1725
|
+
item.kind === "started" &&
|
|
1726
|
+
typeof item.agentThreadId === "string" &&
|
|
1727
|
+
item.agentPath !== "/root" &&
|
|
1728
|
+
item.agentThreadId !== session?.threadId &&
|
|
1729
|
+
!startedIndexByThread.has(item.agentThreadId)
|
|
1730
|
+
) {
|
|
1731
|
+
startedIndexByThread.set(item.agentThreadId, index)
|
|
1711
1732
|
}
|
|
1712
|
-
this._emitSubagentState(session, record, { force: true })
|
|
1713
1733
|
}
|
|
1734
|
+
const snapshots = []
|
|
1735
|
+
for (const record of session?.subagents?.values?.() || []) {
|
|
1736
|
+
const startedHistoryIndex = startedIndexByThread.get(record.agentThreadId)
|
|
1737
|
+
if (startedHistoryIndex === undefined) continue
|
|
1738
|
+
snapshots.push({
|
|
1739
|
+
delegationId: record.agentThreadId,
|
|
1740
|
+
agentThreadId: record.agentThreadId,
|
|
1741
|
+
agentPath: record.agentPath,
|
|
1742
|
+
agent: record.agent,
|
|
1743
|
+
description: record.description,
|
|
1744
|
+
prompt: record.prompt,
|
|
1745
|
+
background: record.background === true,
|
|
1746
|
+
startedHistoryIndex,
|
|
1747
|
+
phase: record.phase,
|
|
1748
|
+
startedAtMs: record.startedAtMs,
|
|
1749
|
+
terminalAtMs: record.terminalAtMs,
|
|
1750
|
+
})
|
|
1751
|
+
}
|
|
1752
|
+
return snapshots
|
|
1714
1753
|
}
|
|
1715
1754
|
|
|
1716
|
-
|
|
1717
|
-
//
|
|
1718
|
-
//
|
|
1719
|
-
|
|
1720
|
-
if (
|
|
1721
|
-
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
async _restoreSubagentsFromHistory(session, thread) {
|
|
1725
|
-
// 表示用 history は巨大スレッド対策で末尾だけに truncate されるため、復元元には使わない。
|
|
1726
|
-
// root の全 turn を逐次走査し、subAgentActivity の安定 ID を取りこぼさない。
|
|
1727
|
-
for (const turn of Array.isArray(thread?.turns) ? thread.turns : []) {
|
|
1728
|
-
const atMs =
|
|
1729
|
-
typeof turn?.completedAt === "number"
|
|
1730
|
-
? Math.round(turn.completedAt * 1000)
|
|
1731
|
-
: typeof turn?.startedAt === "number"
|
|
1732
|
-
? Math.round(turn.startedAt * 1000)
|
|
1733
|
-
: Date.now()
|
|
1734
|
-
for (const item of Array.isArray(turn?.items) ? turn.items : []) {
|
|
1735
|
-
this._observeSubagentItem(session, item, atMs, { emit: false })
|
|
1755
|
+
async _restoreSubagentsFromHistory(session, history) {
|
|
1756
|
+
// ready に載せる表示用 history と同じ範囲だけを復元元にする。truncate 外の開始
|
|
1757
|
+
// anchor を拾うと、対応する履歴が無い lifecycle card が末尾へ浮くため全履歴は走査しない。
|
|
1758
|
+
for (const item of Array.isArray(history) ? history : []) {
|
|
1759
|
+
if (item?.type === "dynamicToolCall" || item?.kind === "started") {
|
|
1760
|
+
this._observeSubagentItem(session, item, item?.at_ms, { emit: false })
|
|
1736
1761
|
}
|
|
1737
1762
|
}
|
|
1738
1763
|
const records = [...session.subagents.values()]
|
|
@@ -1745,6 +1770,8 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1745
1770
|
threadId: record.agentThreadId,
|
|
1746
1771
|
includeTurns: true,
|
|
1747
1772
|
})
|
|
1773
|
+
// RPC 待ち中に live lifecycle が進んだ場合、stale read で上書き・削除しない。
|
|
1774
|
+
if (session.subagents.get(record.agentThreadId) !== record) continue
|
|
1748
1775
|
const thread = result?.thread
|
|
1749
1776
|
const turns = Array.isArray(thread?.turns) ? thread.turns : []
|
|
1750
1777
|
const lastTurn = turns.at(-1)
|
|
@@ -1763,23 +1790,34 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1763
1790
|
{ agentPath: record.agentPath, atMs: lastTurnAtMs },
|
|
1764
1791
|
{ emit: false },
|
|
1765
1792
|
)
|
|
1766
|
-
} else if (
|
|
1793
|
+
} else if (
|
|
1794
|
+
thread?.status?.type === "active" &&
|
|
1795
|
+
(!lastTurn || lastTurn.status === "inProgress")
|
|
1796
|
+
) {
|
|
1767
1797
|
this._setSubagentPhase(
|
|
1768
1798
|
session,
|
|
1769
1799
|
record.agentThreadId,
|
|
1770
1800
|
"started",
|
|
1771
|
-
{
|
|
1801
|
+
{
|
|
1802
|
+
agentPath: record.agentPath,
|
|
1803
|
+
atMs: record.startedAtMs,
|
|
1804
|
+
},
|
|
1772
1805
|
{ emit: false },
|
|
1773
1806
|
)
|
|
1807
|
+
} else {
|
|
1808
|
+
session.subagents.delete(record.agentThreadId)
|
|
1774
1809
|
}
|
|
1775
1810
|
} catch (err) {
|
|
1811
|
+
if (session.subagents.get(record.agentThreadId) === record) {
|
|
1812
|
+
session.subagents.delete(record.agentThreadId)
|
|
1813
|
+
}
|
|
1776
1814
|
this.logger?.warn?.(
|
|
1777
1815
|
{
|
|
1778
1816
|
err: err?.message || String(err),
|
|
1779
1817
|
thread_id: session.threadId,
|
|
1780
1818
|
subagent_thread_id: record.agentThreadId,
|
|
1781
1819
|
},
|
|
1782
|
-
"codex subagent thread/read failed during resume",
|
|
1820
|
+
"codex subagent thread/read failed during resume; excluded from ready snapshot",
|
|
1783
1821
|
)
|
|
1784
1822
|
}
|
|
1785
1823
|
}
|
|
@@ -1841,6 +1879,19 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1841
1879
|
// (タグ済み or 素) を history と emit の両方で使う。一致しなければ null → 素の params のまま。
|
|
1842
1880
|
// ※ hub-agent 再起動 (cold resume) では historyItems が揮発するため青バブルへ戻る (claude の
|
|
1843
1881
|
// jsonl at-rest と同じ許容範囲。source 永続マップは TTL/ID 再利用の複雑さに見合わないため不採用)。
|
|
1882
|
+
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 へ通す。背景出力の過剰抑止より既存表示の維持を優先する。
|
|
1893
|
+
return
|
|
1894
|
+
}
|
|
1844
1895
|
const taggedParams = this._tagInjectSourceOnItem(session, msg)
|
|
1845
1896
|
const effectiveParams = taggedParams || msg.params
|
|
1846
1897
|
if (msg.method === "item/started" || msg.method === "item/completed") {
|
package/src/main.mjs
CHANGED
|
@@ -2052,6 +2052,43 @@ export function buildCodexChatPatch(status, contextPct, threadId, isAuthority) {
|
|
|
2052
2052
|
return patch
|
|
2053
2053
|
}
|
|
2054
2054
|
|
|
2055
|
+
/** codex.ready の wire payload を組み立てる。history は同じ配列参照を保持し、
|
|
2056
|
+
* started_history_index が指す位置を main 側の加工でずらさない。表示を保持する reconnect
|
|
2057
|
+
* (`info.subagents` 未定義) では subagents キー自体を省略し、空 snapshot で既存表示を消さない。 */
|
|
2058
|
+
export function buildCodexReadyPayload(streamId, info, queueState) {
|
|
2059
|
+
const history = Array.isArray(info?.history) ? info.history : []
|
|
2060
|
+
const payload = {
|
|
2061
|
+
type: "codex.ready",
|
|
2062
|
+
stream_id: streamId,
|
|
2063
|
+
resuming: info?.resuming === true,
|
|
2064
|
+
thread_id: info?.threadId || null,
|
|
2065
|
+
history,
|
|
2066
|
+
history_truncated: info?.historyTruncated === true,
|
|
2067
|
+
turn_active: info?.turnActive === true,
|
|
2068
|
+
thread_status: info?.threadStatus || null,
|
|
2069
|
+
last_turn_status: info?.lastTurnStatus || null,
|
|
2070
|
+
queue_state: queueState,
|
|
2071
|
+
}
|
|
2072
|
+
if (Array.isArray(info?.subagents)) {
|
|
2073
|
+
payload.subagents = info.subagents.map((subagent) => ({
|
|
2074
|
+
// 現契約では delegation_id と agent_thread_id は同じ child thread ID の alias。
|
|
2075
|
+
delegation_id: subagent.delegationId,
|
|
2076
|
+
agent_thread_id: subagent.agentThreadId,
|
|
2077
|
+
agent_path: subagent.agentPath,
|
|
2078
|
+
agent: subagent.agent,
|
|
2079
|
+
// description は spawn_agent.task_name の既存 alias(人物の表示名ではない)。
|
|
2080
|
+
description: subagent.description,
|
|
2081
|
+
prompt: subagent.prompt,
|
|
2082
|
+
background: subagent.background === true,
|
|
2083
|
+
started_history_index: subagent.startedHistoryIndex,
|
|
2084
|
+
phase: subagent.phase,
|
|
2085
|
+
started_at_ms: subagent.startedAtMs,
|
|
2086
|
+
terminal_at_ms: subagent.terminalAtMs,
|
|
2087
|
+
}))
|
|
2088
|
+
}
|
|
2089
|
+
return payload
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2055
2092
|
/**
|
|
2056
2093
|
* T06407 増分2 (codex-review round2 major#2): usage.response の codexSessions を、集計に
|
|
2057
2094
|
* 使った producer が **今なお cwd の権威**である entry だけへ絞る post-await ガード。
|
|
@@ -4131,30 +4168,9 @@ async function dispatch(msg, ctx) {
|
|
|
4131
4168
|
// すると bridge が rebind し、fresh の二重生成を防ぐ (browser の stream_id は都度変わる)。
|
|
4132
4169
|
attachId: msg.attach_id || undefined,
|
|
4133
4170
|
})
|
|
4134
|
-
ctx.client.send(
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
resuming: info.resuming,
|
|
4138
|
-
thread_id: info.threadId,
|
|
4139
|
-
// Slice 6: 履歴 hydrate。resume 時のみ非空 (thread/start は常に [])。
|
|
4140
|
-
// consumers.py の per-stream relay は content-blind なので backend 側の
|
|
4141
|
-
// 変更は不要 (_AGENT_PER_STREAM_TYPES の codex.ready がそのまま運ぶ)。
|
|
4142
|
-
history: info.history || [],
|
|
4143
|
-
// T06499: 巨大スレッドで末尾 N ターンに絞った場合 true。browser は
|
|
4144
|
-
// 「以前の履歴は省略されています」バナーを出す (会話は resume 済みで継続可)。
|
|
4145
|
-
history_truncated: info.historyTruncated === true,
|
|
4146
|
-
// 案C (権威的ターン状態): フロントは会話末尾からの推測 (deriveTurnActive
|
|
4147
|
-
// FromHistory) をやめ、Codex が明示する Thread.status で turnActive を決める。
|
|
4148
|
-
// resume 後は新プロセスで実行中ターンが無く thread_status='idle' が返るため、
|
|
4149
|
-
// 生成中スピナー固着 (中断/ツールのみ終了ターンで末尾が user メッセージになる
|
|
4150
|
-
// ケース) が根治する。
|
|
4151
|
-
turn_active: info.turnActive === true,
|
|
4152
|
-
thread_status: info.threadStatus || null,
|
|
4153
|
-
last_turn_status: info.lastTurnStatus || null,
|
|
4154
|
-
// Codex の通常送信 FIFO。ready と同じ封筒に初期 snapshot を載せ、
|
|
4155
|
-
// reattach / reload でも送信待ち表示を即時復元する。
|
|
4156
|
-
queue_state: ctx.codexBridge.queueState(stream_id),
|
|
4157
|
-
})
|
|
4171
|
+
ctx.client.send(
|
|
4172
|
+
buildCodexReadyPayload(stream_id, info, ctx.codexBridge.queueState(stream_id)),
|
|
4173
|
+
)
|
|
4158
4174
|
// S1 (2026-07): claude.attach と対称の identity 正本化。Codex は thread/start
|
|
4159
4175
|
// 応答時点で threadId が確定するため (Claude の fresh とは異なり)、fresh でも
|
|
4160
4176
|
// 既に分かっている threadId を sessionId として記録する (直後の turn/started が
|