@cocorograph/hub-agent 0.8.41 → 0.8.43
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 +4 -4
- package/src/sibling-inject.mjs +107 -16
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({
|
|
@@ -3791,11 +3791,11 @@ async function dispatch(msg, ctx) {
|
|
|
3791
3791
|
})
|
|
3792
3792
|
if (resumeSessionId) {
|
|
3793
3793
|
void ctx.siblingInjectBridge
|
|
3794
|
-
?.
|
|
3794
|
+
?.replayDelegations(cwdArg || process.env.HOME || process.cwd(), resumeSessionId)
|
|
3795
3795
|
.catch((err) => {
|
|
3796
3796
|
ctx.logger?.warn?.(
|
|
3797
3797
|
{ err: err?.message || String(err), stream_id, session_id: resumeSessionId },
|
|
3798
|
-
"failed to replay
|
|
3798
|
+
"failed to replay delegations after Claude attach",
|
|
3799
3799
|
)
|
|
3800
3800
|
})
|
|
3801
3801
|
}
|
package/src/sibling-inject.mjs
CHANGED
|
@@ -75,20 +75,68 @@ export const ACTIVE_DELEGATIONS_PER_SESSION_LIMIT = 100
|
|
|
75
75
|
export const DELEGATION_LEDGER_RETENTION_MS = 24 * 60 * 60 * 1000
|
|
76
76
|
const SIBLING_REQUEST_MAX_BYTES = 1024 * 1024
|
|
77
77
|
const DELEGATE_PROMPT_MAX_LENGTH = 64_000
|
|
78
|
+
const TERMINAL_DELEGATE_PROMPT_MAX_LENGTH = 2_000
|
|
79
|
+
const TERMINAL_DELEGATE_AGENT_MAX_LENGTH = 200
|
|
80
|
+
const TERMINAL_DELEGATE_DESCRIPTION_MAX_LENGTH = 2_000
|
|
81
|
+
const DELEGATION_ID_MAX_LENGTH = 200
|
|
78
82
|
|
|
79
83
|
const execFile = promisify(execFileCallback)
|
|
80
84
|
|
|
81
|
-
function
|
|
82
|
-
if (
|
|
83
|
-
let end =
|
|
84
|
-
const lastCodeUnit =
|
|
85
|
+
function truncateString(value, maxLength, truncationNote) {
|
|
86
|
+
if (value.length <= maxLength) return value
|
|
87
|
+
let end = maxLength
|
|
88
|
+
const lastCodeUnit = value.charCodeAt(end - 1)
|
|
85
89
|
if (lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff) end -= 1
|
|
86
|
-
return (
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
return value.slice(0, end) + truncationNote(value.length)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function truncateDelegatePrompt(prompt) {
|
|
94
|
+
return truncateString(
|
|
95
|
+
prompt,
|
|
96
|
+
DELEGATE_PROMPT_MAX_LENGTH,
|
|
97
|
+
(length) => `\n\n[依頼本文はここで打ち切られています(全 ${length} 文字)]`,
|
|
89
98
|
)
|
|
90
99
|
}
|
|
91
100
|
|
|
101
|
+
function compactTerminalEvent(event, includePrompt = false) {
|
|
102
|
+
const storedEvent = {}
|
|
103
|
+
for (const field of [
|
|
104
|
+
"type",
|
|
105
|
+
"subtype",
|
|
106
|
+
"delegation_id",
|
|
107
|
+
"phase",
|
|
108
|
+
"background",
|
|
109
|
+
"timestamp",
|
|
110
|
+
]) {
|
|
111
|
+
if (event[field] !== undefined) storedEvent[field] = event[field]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (typeof event.agent === "string") {
|
|
115
|
+
storedEvent.agent = truncateString(
|
|
116
|
+
event.agent,
|
|
117
|
+
TERMINAL_DELEGATE_AGENT_MAX_LENGTH,
|
|
118
|
+
(length) => `\n\n[エージェント名はここで打ち切られています(全 ${length} 文字)]`,
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
if (typeof event.description === "string") {
|
|
122
|
+
storedEvent.description = truncateString(
|
|
123
|
+
event.description,
|
|
124
|
+
TERMINAL_DELEGATE_DESCRIPTION_MAX_LENGTH,
|
|
125
|
+
(length) => `\n\n[説明はここで打ち切られています(全 ${length} 文字)]`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
if (includePrompt && typeof event.prompt === "string") {
|
|
129
|
+
storedEvent.prompt = truncateString(
|
|
130
|
+
event.prompt,
|
|
131
|
+
TERMINAL_DELEGATE_PROMPT_MAX_LENGTH,
|
|
132
|
+
(length) =>
|
|
133
|
+
`\n\n[終了済み委譲の依頼本文はここで打ち切られています(全 ${length} 文字)]`,
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return storedEvent
|
|
138
|
+
}
|
|
139
|
+
|
|
92
140
|
/**
|
|
93
141
|
* `ps` の起動時刻を送受信双方で比較する契約形へ正規化する。
|
|
94
142
|
* 先頭・末尾の Unicode 空白だけを除去し、日時本体の内部空白は変更しない。
|
|
@@ -312,7 +360,7 @@ export class SiblingInjectBridge {
|
|
|
312
360
|
this._pidStartedAt = pidStartedAt
|
|
313
361
|
/** @type {Map<string, Map<string, {event: object, recordedAt: number}>>} cwd+session ごとの進行中委譲。 */
|
|
314
362
|
this._activeDelegations = new Map()
|
|
315
|
-
/** @type {Map<string, Map<string, number>>}
|
|
363
|
+
/** @type {Map<string, Map<string, {startedEvent?: object, terminalEvent: object, startedAt?: number, recordedAt: number}>>} 終了済み委譲の再送情報を兼ねた墓標。 */
|
|
316
364
|
this._terminalDelegations = new Map()
|
|
317
365
|
}
|
|
318
366
|
|
|
@@ -329,8 +377,8 @@ export class SiblingInjectBridge {
|
|
|
329
377
|
if (entries.size === 0) this._activeDelegations.delete(sessionKey)
|
|
330
378
|
}
|
|
331
379
|
for (const [sessionKey, entries] of this._terminalDelegations) {
|
|
332
|
-
for (const [delegationId,
|
|
333
|
-
if (recordedAt < cutoff) entries.delete(delegationId)
|
|
380
|
+
for (const [delegationId, entry] of entries) {
|
|
381
|
+
if (entry.recordedAt < cutoff) entries.delete(delegationId)
|
|
334
382
|
}
|
|
335
383
|
if (entries.size === 0) this._terminalDelegations.delete(sessionKey)
|
|
336
384
|
}
|
|
@@ -340,6 +388,10 @@ export class SiblingInjectBridge {
|
|
|
340
388
|
this._pruneDelegations()
|
|
341
389
|
const sessionKey = this._delegationSessionKey(cwd, sessionId)
|
|
342
390
|
if (event.phase !== "started") {
|
|
391
|
+
const activeEntry = this._activeDelegations.get(sessionKey)?.get(event.delegation_id)
|
|
392
|
+
const existingTerminalEntry = this._terminalDelegations
|
|
393
|
+
.get(sessionKey)
|
|
394
|
+
?.get(event.delegation_id)
|
|
343
395
|
this._activeDelegations.get(sessionKey)?.delete(event.delegation_id)
|
|
344
396
|
if (this._activeDelegations.get(sessionKey)?.size === 0) {
|
|
345
397
|
this._activeDelegations.delete(sessionKey)
|
|
@@ -350,7 +402,21 @@ export class SiblingInjectBridge {
|
|
|
350
402
|
this._terminalDelegations.set(sessionKey, terminalEntries)
|
|
351
403
|
}
|
|
352
404
|
terminalEntries.delete(event.delegation_id)
|
|
353
|
-
terminalEntries.set(event.delegation_id,
|
|
405
|
+
terminalEntries.set(event.delegation_id, {
|
|
406
|
+
...(activeEntry
|
|
407
|
+
? {
|
|
408
|
+
startedEvent: compactTerminalEvent(activeEntry.event, true),
|
|
409
|
+
startedAt: activeEntry.recordedAt,
|
|
410
|
+
}
|
|
411
|
+
: existingTerminalEntry?.startedEvent && existingTerminalEntry.startedAt !== undefined
|
|
412
|
+
? {
|
|
413
|
+
startedEvent: existingTerminalEntry.startedEvent,
|
|
414
|
+
startedAt: existingTerminalEntry.startedAt,
|
|
415
|
+
}
|
|
416
|
+
: {}),
|
|
417
|
+
terminalEvent: compactTerminalEvent(event),
|
|
418
|
+
recordedAt: this._now(),
|
|
419
|
+
})
|
|
354
420
|
while (terminalEntries.size > ACTIVE_DELEGATIONS_PER_SESSION_LIMIT) {
|
|
355
421
|
terminalEntries.delete(terminalEntries.keys().next().value)
|
|
356
422
|
}
|
|
@@ -394,14 +460,35 @@ export class SiblingInjectBridge {
|
|
|
394
460
|
}
|
|
395
461
|
}
|
|
396
462
|
|
|
397
|
-
/** attach/再接続時に同じ cwd+session
|
|
398
|
-
async
|
|
463
|
+
/** attach/再接続時に同じ cwd+session の進行中・終了済み委譲を開始順で再送する。 */
|
|
464
|
+
async replayDelegations(cwd, sessionId) {
|
|
399
465
|
if (!cwd || !sessionId) return
|
|
400
466
|
this._pruneDelegations()
|
|
401
467
|
const sessionKey = this._delegationSessionKey(cwd, sessionId)
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
468
|
+
const replayEntries = [
|
|
469
|
+
...[...(this._activeDelegations.get(sessionKey) || [])].map(([delegationId, entry]) => ({
|
|
470
|
+
delegationId,
|
|
471
|
+
entry,
|
|
472
|
+
kind: "active",
|
|
473
|
+
startedAt: entry.recordedAt,
|
|
474
|
+
})),
|
|
475
|
+
...[...(this._terminalDelegations.get(sessionKey) || [])]
|
|
476
|
+
.filter(([, entry]) => entry.startedEvent && entry.startedAt !== undefined)
|
|
477
|
+
.map(([delegationId, entry]) => ({
|
|
478
|
+
delegationId,
|
|
479
|
+
entry,
|
|
480
|
+
kind: "terminal",
|
|
481
|
+
startedAt: entry.startedAt,
|
|
482
|
+
})),
|
|
483
|
+
].sort((a, b) => a.startedAt - b.startedAt)
|
|
484
|
+
|
|
485
|
+
for (const { delegationId, entry, kind } of replayEntries) {
|
|
486
|
+
if (kind === "terminal") {
|
|
487
|
+
if (this._terminalDelegations.get(sessionKey)?.get(delegationId) !== entry) continue
|
|
488
|
+
this.claudeBridge?.emitToStream?.(cwd, sessionId, entry.startedEvent)
|
|
489
|
+
this.claudeBridge?.emitToStream?.(cwd, sessionId, entry.terminalEvent)
|
|
490
|
+
continue
|
|
491
|
+
}
|
|
405
492
|
const alive = await this._delegateProcessAlive(entry.event)
|
|
406
493
|
// 生存確認中に終端が届いた場合、古い snapshot の started/stopped を後から流さない。
|
|
407
494
|
if (this._activeDelegations.get(sessionKey)?.get(delegationId) !== entry) continue
|
|
@@ -571,6 +658,7 @@ export class SiblingInjectBridge {
|
|
|
571
658
|
phase,
|
|
572
659
|
agent,
|
|
573
660
|
description,
|
|
661
|
+
background,
|
|
574
662
|
prompt,
|
|
575
663
|
timestamp,
|
|
576
664
|
pid,
|
|
@@ -590,10 +678,12 @@ export class SiblingInjectBridge {
|
|
|
590
678
|
const badDelegateBody =
|
|
591
679
|
typeof delegation_id !== "string" ||
|
|
592
680
|
!delegation_id ||
|
|
681
|
+
delegation_id.length > DELEGATION_ID_MAX_LENGTH ||
|
|
593
682
|
!delegatePhases.has(phase) ||
|
|
594
683
|
(phase === "started" && (typeof agent !== "string" || !agent)) ||
|
|
595
684
|
(agent !== undefined && typeof agent !== "string") ||
|
|
596
685
|
(description !== undefined && typeof description !== "string") ||
|
|
686
|
+
(background !== undefined && typeof background !== "boolean") ||
|
|
597
687
|
(prompt !== undefined && typeof prompt !== "string") ||
|
|
598
688
|
(pid !== undefined && (!Number.isInteger(pid) || pid <= 0)) ||
|
|
599
689
|
(pid_started_at !== undefined && typeof pid_started_at !== "string") ||
|
|
@@ -656,6 +746,7 @@ export class SiblingInjectBridge {
|
|
|
656
746
|
phase,
|
|
657
747
|
...(agent !== undefined ? { agent } : {}),
|
|
658
748
|
...(description !== undefined ? { description } : {}),
|
|
749
|
+
...(background !== undefined ? { background } : {}),
|
|
659
750
|
...(phase === "started" && prompt ? { prompt: truncateDelegatePrompt(prompt) } : {}),
|
|
660
751
|
...(pid !== undefined ? { pid } : {}),
|
|
661
752
|
...(pid_started_at !== undefined ? { pid_started_at } : {}),
|