@cocorograph/hub-agent 0.8.36 → 0.8.38
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 +88 -3
- package/src/sibling-inject.mjs +41 -1
- package/src/tool-result-mask.mjs +16 -0
package/package.json
CHANGED
|
@@ -394,8 +394,8 @@ function explicitSubagentRoleFromThread(thread, expectedParentThreadId) {
|
|
|
394
394
|
const threadSpawn = thread?.source?.subAgent?.thread_spawn
|
|
395
395
|
if (
|
|
396
396
|
threadSpawn &&
|
|
397
|
-
(
|
|
398
|
-
|
|
397
|
+
Object.prototype.hasOwnProperty.call(threadSpawn, "parent_thread_id") &&
|
|
398
|
+
threadSpawn.parent_thread_id !== expectedParentThreadId
|
|
399
399
|
) return null
|
|
400
400
|
const directRole = typeof thread?.agentRole === "string" ? thread.agentRole.trim() : ""
|
|
401
401
|
if (directRole) return directRole
|
|
@@ -767,6 +767,12 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
767
767
|
subagents: new Map(),
|
|
768
768
|
/** dynamicToolCall(spawn_agent) の item.id -> 表示メタデータ。 */
|
|
769
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(),
|
|
770
776
|
/** 実行中の背景コマンド(commandExecution)の台帳。claude 側 `_liveTasks` と同型。
|
|
771
777
|
* key = `item.id`(`item/started` / `item/completed` の両方に載る唯一の安定 ID)、
|
|
772
778
|
* value = { threadId, startedAt, lastSeenAt }。
|
|
@@ -1670,6 +1676,80 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1670
1676
|
return record
|
|
1671
1677
|
}
|
|
1672
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
|
+
|
|
1673
1753
|
_observeSubagentItem(
|
|
1674
1754
|
session,
|
|
1675
1755
|
item,
|
|
@@ -1716,7 +1796,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1716
1796
|
}
|
|
1717
1797
|
if (!phase) return
|
|
1718
1798
|
const metadata = session.subagentSpawnMetadata.get(item.id) || null
|
|
1719
|
-
this._setSubagentPhase(
|
|
1799
|
+
const record = this._setSubagentPhase(
|
|
1720
1800
|
session,
|
|
1721
1801
|
agentThreadId,
|
|
1722
1802
|
phase,
|
|
@@ -1728,6 +1808,11 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1728
1808
|
},
|
|
1729
1809
|
{ emit },
|
|
1730
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
|
+
}
|
|
1731
1816
|
}
|
|
1732
1817
|
|
|
1733
1818
|
_observeSubagentTurn(session, msg) {
|
package/src/sibling-inject.mjs
CHANGED
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
*/
|
|
61
61
|
|
|
62
62
|
import { watch } from "node:fs"
|
|
63
|
-
import { chmod, lstat, mkdir, readFile, writeFile, rename, unlink, readdir } from "node:fs/promises"
|
|
63
|
+
import { chmod, lstat, mkdir, readFile, writeFile, rename, stat, unlink, readdir } from "node:fs/promises"
|
|
64
64
|
import { execFile as execFileCallback } from "node:child_process"
|
|
65
65
|
import { promisify } from "node:util"
|
|
66
66
|
import path from "node:path"
|
|
@@ -73,9 +73,22 @@ export const SIBLING_REQUESTS_DIR =
|
|
|
73
73
|
|
|
74
74
|
export const ACTIVE_DELEGATIONS_PER_SESSION_LIMIT = 100
|
|
75
75
|
export const DELEGATION_LEDGER_RETENTION_MS = 24 * 60 * 60 * 1000
|
|
76
|
+
const SIBLING_REQUEST_MAX_BYTES = 1024 * 1024
|
|
77
|
+
const DELEGATE_PROMPT_MAX_LENGTH = 64_000
|
|
76
78
|
|
|
77
79
|
const execFile = promisify(execFileCallback)
|
|
78
80
|
|
|
81
|
+
function truncateDelegatePrompt(prompt) {
|
|
82
|
+
if (prompt.length <= DELEGATE_PROMPT_MAX_LENGTH) return prompt
|
|
83
|
+
let end = DELEGATE_PROMPT_MAX_LENGTH
|
|
84
|
+
const lastCodeUnit = prompt.charCodeAt(end - 1)
|
|
85
|
+
if (lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff) end -= 1
|
|
86
|
+
return (
|
|
87
|
+
prompt.slice(0, end) +
|
|
88
|
+
`\n\n[依頼本文はここで打ち切られています(全 ${prompt.length} 文字)]`
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
79
92
|
/**
|
|
80
93
|
* `ps` の起動時刻を送受信双方で比較する契約形へ正規化する。
|
|
81
94
|
* 先頭・末尾の Unicode 空白だけを除去し、日時本体の内部空白は変更しない。
|
|
@@ -506,6 +519,30 @@ export class SiblingInjectBridge {
|
|
|
506
519
|
const fp = path.join(this.dir, filename)
|
|
507
520
|
let body
|
|
508
521
|
try {
|
|
522
|
+
const fileStat = await stat(fp)
|
|
523
|
+
if (fileStat.size > SIBLING_REQUEST_MAX_BYTES) {
|
|
524
|
+
this.logger?.warn(
|
|
525
|
+
{ filename, size: fileStat.size, max_size: SIBLING_REQUEST_MAX_BYTES },
|
|
526
|
+
"sibling-inject: request file rejected because it exceeds the size limit",
|
|
527
|
+
)
|
|
528
|
+
// ⚠️ **結果ファイルを必ず書く**。ここで書かずに戻ると、結果をポーリングしている
|
|
529
|
+
// 呼び出し側 (cockpit_orch.py) には「タイムアウト」としか見えない。タイムアウトは
|
|
530
|
+
// 「不達とは限らない」と扱う契約なので、拒否した事実が呼び出し側で不達に化ける
|
|
531
|
+
// (=黙って捨てたのと同じになる)。delegate-event の送り手は結果を読まないため、
|
|
532
|
+
// op を判別できないまま無条件に書いて構わない (余分な結果ファイルは stale 掃除の対象)。
|
|
533
|
+
await this._writeResult(request_id, {
|
|
534
|
+
ok: false,
|
|
535
|
+
error: "request_too_large",
|
|
536
|
+
size: fileStat.size,
|
|
537
|
+
max_size: SIBLING_REQUEST_MAX_BYTES,
|
|
538
|
+
})
|
|
539
|
+
try {
|
|
540
|
+
await unlink(fp)
|
|
541
|
+
} catch {
|
|
542
|
+
/* 既に無ければ no-op */
|
|
543
|
+
}
|
|
544
|
+
return
|
|
545
|
+
}
|
|
509
546
|
body = JSON.parse(await readFile(fp, "utf-8"))
|
|
510
547
|
} catch {
|
|
511
548
|
// partial write / 既に消えた → 次の watch イベントで拾い直す (seen から外す)。
|
|
@@ -534,6 +571,7 @@ export class SiblingInjectBridge {
|
|
|
534
571
|
phase,
|
|
535
572
|
agent,
|
|
536
573
|
description,
|
|
574
|
+
prompt,
|
|
537
575
|
timestamp,
|
|
538
576
|
pid,
|
|
539
577
|
pid_started_at,
|
|
@@ -556,6 +594,7 @@ export class SiblingInjectBridge {
|
|
|
556
594
|
(phase === "started" && (typeof agent !== "string" || !agent)) ||
|
|
557
595
|
(agent !== undefined && typeof agent !== "string") ||
|
|
558
596
|
(description !== undefined && typeof description !== "string") ||
|
|
597
|
+
(prompt !== undefined && typeof prompt !== "string") ||
|
|
559
598
|
(pid !== undefined && (!Number.isInteger(pid) || pid <= 0)) ||
|
|
560
599
|
(pid_started_at !== undefined && typeof pid_started_at !== "string") ||
|
|
561
600
|
(timestamp !== undefined &&
|
|
@@ -617,6 +656,7 @@ export class SiblingInjectBridge {
|
|
|
617
656
|
phase,
|
|
618
657
|
...(agent !== undefined ? { agent } : {}),
|
|
619
658
|
...(description !== undefined ? { description } : {}),
|
|
659
|
+
...(phase === "started" && prompt ? { prompt: truncateDelegatePrompt(prompt) } : {}),
|
|
620
660
|
...(pid !== undefined ? { pid } : {}),
|
|
621
661
|
...(pid_started_at !== undefined ? { pid_started_at } : {}),
|
|
622
662
|
timestamp: timestamp || new Date().toISOString(),
|
package/src/tool-result-mask.mjs
CHANGED
|
@@ -163,7 +163,23 @@ function redactBlocks(blocks) {
|
|
|
163
163
|
* @returns {Record<string, unknown>}
|
|
164
164
|
*/
|
|
165
165
|
export function maskMessageObject(obj, toolNames) {
|
|
166
|
+
if (!MASK_ENABLED) return obj
|
|
166
167
|
if (!obj || typeof obj !== "object") return obj
|
|
168
|
+
|
|
169
|
+
if (obj.type === "system" && obj.subtype === "codex_delegate") {
|
|
170
|
+
let changed = false
|
|
171
|
+
const next = { ...obj }
|
|
172
|
+
for (const field of ["prompt", "description"]) {
|
|
173
|
+
if (typeof obj[field] !== "string") continue
|
|
174
|
+
const masked = redactSecrets(obj[field])
|
|
175
|
+
if (masked !== obj[field]) {
|
|
176
|
+
next[field] = masked
|
|
177
|
+
changed = true
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return changed ? next : obj
|
|
181
|
+
}
|
|
182
|
+
|
|
167
183
|
const message = obj.message
|
|
168
184
|
if (!message || typeof message !== "object") return obj
|
|
169
185
|
const content = message.content
|