@cocorograph/hub-agent 0.8.41 → 0.8.42
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 +168 -9
- package/src/main.mjs +2 -2
package/package.json
CHANGED
|
@@ -770,6 +770,8 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
770
770
|
/** 組み込み collaboration subagent の表示用台帳。検索キーは agentThreadId、
|
|
771
771
|
* 外部へ出す安定した委譲 ID は subAgentActivity の lifecycle ID を保持する。 */
|
|
772
772
|
subagents: new Map(),
|
|
773
|
+
/** 停止対象の因果関係と実行中 turn を保持する、表示台帳とは独立した台帳。 */
|
|
774
|
+
subagentInterruptRegistry: new Map(),
|
|
773
775
|
/** dynamicToolCall(spawn_agent) の item.id -> 表示メタデータ。 */
|
|
774
776
|
subagentSpawnMetadata: new Map(),
|
|
775
777
|
/** live subAgentActivity の spawn metadata miss を child thread/read で補完する
|
|
@@ -1759,9 +1761,22 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1759
1761
|
session,
|
|
1760
1762
|
item,
|
|
1761
1763
|
atMs = Date.now(),
|
|
1762
|
-
{ emit = true, sourceThreadId = session?.threadId } = {},
|
|
1764
|
+
{ emit = true, sourceThreadId = session?.threadId, sourceTurnId = null } = {},
|
|
1763
1765
|
) {
|
|
1764
1766
|
if (!item || typeof item !== "object") return
|
|
1767
|
+
if (item.type === "subAgentActivity") {
|
|
1768
|
+
const childThreadId = item.agentThreadId
|
|
1769
|
+
if (typeof childThreadId === "string" && childThreadId && childThreadId !== session?.threadId) {
|
|
1770
|
+
const interruptFields = {}
|
|
1771
|
+
if (typeof sourceThreadId === "string" && sourceThreadId) {
|
|
1772
|
+
interruptFields.parentThreadId = sourceThreadId
|
|
1773
|
+
}
|
|
1774
|
+
if (typeof sourceTurnId === "string" && sourceTurnId) {
|
|
1775
|
+
interruptFields.ownerTurnId = sourceTurnId
|
|
1776
|
+
}
|
|
1777
|
+
this._touchSubagentInterruptRecord(session, childThreadId, interruptFields)
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1765
1780
|
// root thread の collaboration item だけが root subagent 台帳の正本。子 thread 内の
|
|
1766
1781
|
// spawn/subAgentActivity(孫)を root の lifecycle/snapshot へ昇格しない。
|
|
1767
1782
|
if (
|
|
@@ -1823,8 +1838,6 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1823
1838
|
_observeSubagentTurn(session, msg) {
|
|
1824
1839
|
const threadId = msg?.params?.threadId
|
|
1825
1840
|
if (typeof threadId !== "string" || threadId === session.threadId) return false
|
|
1826
|
-
const record = session.subagents.get(threadId)
|
|
1827
|
-
if (!record) return true
|
|
1828
1841
|
const turn = msg.params?.turn
|
|
1829
1842
|
const atMs =
|
|
1830
1843
|
typeof msg.params?.completedAtMs === "number"
|
|
@@ -1836,6 +1849,16 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1836
1849
|
: typeof turn?.startedAt === "number"
|
|
1837
1850
|
? Math.round(turn.startedAt * 1000)
|
|
1838
1851
|
: Date.now()
|
|
1852
|
+
const interruptRecord = this._touchSubagentInterruptRecord(session, threadId)
|
|
1853
|
+
const observedTurnId = typeof turn?.id === "string" ? turn.id : null
|
|
1854
|
+
if (msg.method === "turn/started" && observedTurnId) {
|
|
1855
|
+
interruptRecord.turnId = observedTurnId
|
|
1856
|
+
interruptRecord.startedAtMs = atMs
|
|
1857
|
+
} else if (msg.method === "turn/completed" && interruptRecord.turnId === observedTurnId) {
|
|
1858
|
+
interruptRecord.turnId = null
|
|
1859
|
+
}
|
|
1860
|
+
const record = session.subagents.get(threadId)
|
|
1861
|
+
if (!record) return true
|
|
1839
1862
|
if (msg.method === "turn/started") {
|
|
1840
1863
|
this._setSubagentPhase(session, threadId, "started", {
|
|
1841
1864
|
agentPath: record.agentPath,
|
|
@@ -1853,6 +1876,127 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1853
1876
|
return true
|
|
1854
1877
|
}
|
|
1855
1878
|
|
|
1879
|
+
_touchSubagentInterruptRecord(session, threadId, fields = {}) {
|
|
1880
|
+
const now = Date.now()
|
|
1881
|
+
let record = session.subagentInterruptRegistry.get(threadId)
|
|
1882
|
+
if (!record) {
|
|
1883
|
+
record = {
|
|
1884
|
+
threadId,
|
|
1885
|
+
turnId: null,
|
|
1886
|
+
parentThreadId: null,
|
|
1887
|
+
ownerTurnId: null,
|
|
1888
|
+
startedAtMs: null,
|
|
1889
|
+
lastSeenAtMs: now,
|
|
1890
|
+
}
|
|
1891
|
+
session.subagentInterruptRegistry.set(threadId, record)
|
|
1892
|
+
}
|
|
1893
|
+
const { parentThreadId, ownerTurnId, ...mutableFields } = fields
|
|
1894
|
+
Object.assign(record, mutableFields, { lastSeenAtMs: now })
|
|
1895
|
+
if (!record.parentThreadId && typeof parentThreadId === "string" && parentThreadId) {
|
|
1896
|
+
record.parentThreadId = parentThreadId
|
|
1897
|
+
}
|
|
1898
|
+
if (!record.ownerTurnId && typeof ownerTurnId === "string" && ownerTurnId) {
|
|
1899
|
+
record.ownerTurnId = ownerTurnId
|
|
1900
|
+
}
|
|
1901
|
+
const expiredBefore = now - 4 * 60 * 60 * 1000
|
|
1902
|
+
for (const [id, candidate] of session.subagentInterruptRegistry) {
|
|
1903
|
+
if (candidate.turnId === null && candidate.lastSeenAtMs < expiredBefore) {
|
|
1904
|
+
this._evictSubagentInterruptRecord(session, id, candidate)
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
while (session.subagentInterruptRegistry.size > 256) {
|
|
1908
|
+
const oldest = [...session.subagentInterruptRegistry.entries()]
|
|
1909
|
+
.filter(([, candidate]) => candidate.turnId === null)
|
|
1910
|
+
.sort((a, b) => a[1].lastSeenAtMs - b[1].lastSeenAtMs)[0]
|
|
1911
|
+
if (!oldest) {
|
|
1912
|
+
this.logger?.warn?.(
|
|
1913
|
+
{ size: session.subagentInterruptRegistry.size, limit: 256 },
|
|
1914
|
+
"codex subagent interrupt registry exceeds limit with all records active",
|
|
1915
|
+
)
|
|
1916
|
+
break
|
|
1917
|
+
}
|
|
1918
|
+
this._evictSubagentInterruptRecord(session, oldest[0], oldest[1])
|
|
1919
|
+
}
|
|
1920
|
+
return record
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
_evictSubagentInterruptRecord(session, threadId, record) {
|
|
1924
|
+
if (record.turnId) {
|
|
1925
|
+
this.logger?.warn?.({ thread_id: threadId, turn_id: record.turnId }, "evicting active codex subagent interrupt record")
|
|
1926
|
+
}
|
|
1927
|
+
session.subagentInterruptRegistry.delete(threadId)
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
_subagentOwnership(session, record) {
|
|
1931
|
+
const visited = new Set()
|
|
1932
|
+
let current = record
|
|
1933
|
+
let hasBackgroundAncestor = false
|
|
1934
|
+
while (current) {
|
|
1935
|
+
if (visited.has(current.threadId)) return { ownerRootTurnId: null, hasBackgroundAncestor }
|
|
1936
|
+
visited.add(current.threadId)
|
|
1937
|
+
if (session.subagents.get(current.threadId)?.background === true) {
|
|
1938
|
+
hasBackgroundAncestor = true
|
|
1939
|
+
}
|
|
1940
|
+
if (current.parentThreadId === session.threadId) {
|
|
1941
|
+
return { ownerRootTurnId: current.ownerTurnId || null, hasBackgroundAncestor }
|
|
1942
|
+
}
|
|
1943
|
+
current = session.subagentInterruptRegistry.get(current.parentThreadId)
|
|
1944
|
+
}
|
|
1945
|
+
return { ownerRootTurnId: null, hasBackgroundAncestor }
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
async _interruptSubagent(session, record, targetRootTurnId) {
|
|
1949
|
+
const displayed = session.subagents.get(record.threadId)
|
|
1950
|
+
const backgroundKnown = displayed ? typeof displayed.background === "boolean" : false
|
|
1951
|
+
const background = backgroundKnown ? displayed.background : null
|
|
1952
|
+
try {
|
|
1953
|
+
await session.client.request("turn/interrupt", { threadId: record.threadId, turnId: record.turnId })
|
|
1954
|
+
return { threadId: record.threadId, turnId: record.turnId, outcome: "interrupted", background, backgroundKnown }
|
|
1955
|
+
} catch (err) {
|
|
1956
|
+
const reason = err?.message || String(err)
|
|
1957
|
+
const mismatch = parseTurnMismatch(reason)
|
|
1958
|
+
const latestRecord = session.subagentInterruptRegistry.get(record.threadId)
|
|
1959
|
+
const latestOwnership = latestRecord ? this._subagentOwnership(session, latestRecord) : null
|
|
1960
|
+
if (
|
|
1961
|
+
mismatch &&
|
|
1962
|
+
latestRecord?.turnId === mismatch.found &&
|
|
1963
|
+
latestOwnership?.ownerRootTurnId === targetRootTurnId &&
|
|
1964
|
+
!latestOwnership.hasBackgroundAncestor
|
|
1965
|
+
) {
|
|
1966
|
+
try {
|
|
1967
|
+
await session.client.request("turn/interrupt", {
|
|
1968
|
+
threadId: latestRecord.threadId,
|
|
1969
|
+
turnId: latestRecord.turnId,
|
|
1970
|
+
})
|
|
1971
|
+
return {
|
|
1972
|
+
threadId: latestRecord.threadId,
|
|
1973
|
+
turnId: latestRecord.turnId,
|
|
1974
|
+
outcome: "interrupted",
|
|
1975
|
+
background,
|
|
1976
|
+
backgroundKnown,
|
|
1977
|
+
}
|
|
1978
|
+
} catch (retryErr) {
|
|
1979
|
+
return {
|
|
1980
|
+
threadId: latestRecord.threadId,
|
|
1981
|
+
turnId: latestRecord.turnId,
|
|
1982
|
+
outcome: "failed",
|
|
1983
|
+
reason: retryErr?.message || String(retryErr),
|
|
1984
|
+
background,
|
|
1985
|
+
backgroundKnown,
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
return {
|
|
1990
|
+
threadId: record.threadId,
|
|
1991
|
+
turnId: record.turnId,
|
|
1992
|
+
outcome: "failed",
|
|
1993
|
+
reason,
|
|
1994
|
+
background,
|
|
1995
|
+
backgroundKnown,
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
1856
2000
|
_subagentSnapshots(session, history = session?.historyItems) {
|
|
1857
2001
|
const items = Array.isArray(history) ? history : []
|
|
1858
2002
|
const startedIndexByThread = new Map()
|
|
@@ -2063,7 +2207,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
2063
2207
|
session,
|
|
2064
2208
|
effectiveParams?.item,
|
|
2065
2209
|
effectiveParams?.completedAtMs || Date.now(),
|
|
2066
|
-
{ sourceThreadId: eventThreadId },
|
|
2210
|
+
{ sourceThreadId: eventThreadId, sourceTurnId: msg.params?.turnId },
|
|
2067
2211
|
)
|
|
2068
2212
|
}
|
|
2069
2213
|
const childTurnNotification =
|
|
@@ -2478,20 +2622,23 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
2478
2622
|
async interrupt({ stream_id }) {
|
|
2479
2623
|
const session = this._require(stream_id)
|
|
2480
2624
|
this._touch(session, stream_id)
|
|
2481
|
-
if (!session.activeTurnId) return { outcome: "no_active_turn" }
|
|
2625
|
+
if (!session.activeTurnId) return { outcome: "no_active_turn", children: [] }
|
|
2482
2626
|
|
|
2483
2627
|
const turnId = session.activeTurnId
|
|
2628
|
+
let attemptedRootTurnId = turnId
|
|
2629
|
+
let result
|
|
2484
2630
|
try {
|
|
2485
2631
|
await session.client.request("turn/interrupt", {
|
|
2486
2632
|
threadId: session.threadId,
|
|
2487
2633
|
turnId,
|
|
2488
2634
|
})
|
|
2489
|
-
|
|
2635
|
+
result = { outcome: "interrupted", turnId, corrected: false }
|
|
2490
2636
|
} catch (err) {
|
|
2491
2637
|
const reason = err?.message || String(err)
|
|
2492
2638
|
const mismatch = parseTurnMismatch(reason)
|
|
2493
|
-
if (!mismatch)
|
|
2639
|
+
if (!mismatch) result = { outcome: "failed", reason }
|
|
2494
2640
|
|
|
2641
|
+
if (mismatch) {
|
|
2495
2642
|
const correctedTurnId = mismatch.found
|
|
2496
2643
|
// 是正は再送前に行い、再送中の corrected turn の完了通知を正しく照合できるようにする。
|
|
2497
2644
|
// 初回リクエスト中に状態が進んでいた場合は、その新しい状態を上書きしない。
|
|
@@ -2499,20 +2646,32 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
2499
2646
|
session.activeTurnId = correctedTurnId
|
|
2500
2647
|
}
|
|
2501
2648
|
try {
|
|
2649
|
+
attemptedRootTurnId = correctedTurnId
|
|
2502
2650
|
await session.client.request("turn/interrupt", {
|
|
2503
2651
|
threadId: session.threadId,
|
|
2504
2652
|
turnId: correctedTurnId,
|
|
2505
2653
|
})
|
|
2506
|
-
|
|
2654
|
+
result = { outcome: "interrupted", turnId: correctedTurnId, corrected: true }
|
|
2507
2655
|
} catch (retryErr) {
|
|
2508
2656
|
const retryReason = retryErr?.message || String(retryErr)
|
|
2509
2657
|
const retryMismatch = parseTurnMismatch(retryReason)
|
|
2510
2658
|
if (retryMismatch && session.activeTurnId === correctedTurnId) {
|
|
2511
2659
|
session.activeTurnId = retryMismatch.found
|
|
2512
2660
|
}
|
|
2513
|
-
|
|
2661
|
+
result = { outcome: "failed", reason: retryReason }
|
|
2662
|
+
}
|
|
2514
2663
|
}
|
|
2515
2664
|
}
|
|
2665
|
+
const children = []
|
|
2666
|
+
const targetRootTurnId = attemptedRootTurnId
|
|
2667
|
+
const interruptSnapshot = [...session.subagentInterruptRegistry.values()].map((record) => ({ ...record }))
|
|
2668
|
+
for (const record of interruptSnapshot) {
|
|
2669
|
+
if (!record.turnId) continue
|
|
2670
|
+
const ownership = this._subagentOwnership(session, record)
|
|
2671
|
+
if (ownership.ownerRootTurnId !== targetRootTurnId || ownership.hasBackgroundAncestor) continue
|
|
2672
|
+
children.push(await this._interruptSubagent(session, { ...record }, targetRootTurnId))
|
|
2673
|
+
}
|
|
2674
|
+
return { ...result, children }
|
|
2516
2675
|
}
|
|
2517
2676
|
|
|
2518
2677
|
/** サーバー発の承認/入力要求への応答。result の shape は method ごとに呼び出し側が組み立てる。 */
|
package/src/main.mjs
CHANGED
|
@@ -3197,7 +3197,7 @@ export function handleCodexChatInput(msg, ctx) {
|
|
|
3197
3197
|
export async function handleCodexInterrupt(msg, ctx) {
|
|
3198
3198
|
let result
|
|
3199
3199
|
if (!ctx.codexBridge) {
|
|
3200
|
-
result = { outcome: "failed", reason: "bridge_unavailable" }
|
|
3200
|
+
result = { outcome: "failed", reason: "bridge_unavailable", children: [] }
|
|
3201
3201
|
} else {
|
|
3202
3202
|
try {
|
|
3203
3203
|
result = await ctx.codexBridge.interrupt({ stream_id: msg.stream_id })
|
|
@@ -3207,7 +3207,7 @@ export async function handleCodexInterrupt(msg, ctx) {
|
|
|
3207
3207
|
} catch (err) {
|
|
3208
3208
|
const reason = err?.message || String(err)
|
|
3209
3209
|
ctx.logger?.warn?.({ err: reason }, "codex.interrupt failed")
|
|
3210
|
-
result = { outcome: "failed", reason }
|
|
3210
|
+
result = { outcome: "failed", reason, children: [] }
|
|
3211
3211
|
}
|
|
3212
3212
|
}
|
|
3213
3213
|
ctx.client.send({
|