@cocorograph/hub-agent 0.8.37 → 0.8.39
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/claude-md-sync.mjs +211 -15
- package/src/claude-md.mjs +12 -3
- package/src/sibling-inject.mjs +41 -1
- package/src/tool-result-mask.mjs +16 -0
package/package.json
CHANGED
package/src/claude-md-sync.mjs
CHANGED
|
@@ -23,6 +23,16 @@
|
|
|
23
23
|
* 詳細な条件と、なぜ `localFilePreexisted` を永続化しないのかは
|
|
24
24
|
* `syncClaudeMdWithHub` 内の `isInitialRegistration` のコメントを参照。
|
|
25
25
|
*
|
|
26
|
+
* **AGENTS.md は受け取り専用**(2026-08-09)。Hub の正本は `Project.claude_md` 1 本で、
|
|
27
|
+
* GET は `cli_kind` に応じて CLI 別へレンダリングして返す。AGENTS.md はその
|
|
28
|
+
* **レンダリング結果**であって正本ではないので、
|
|
29
|
+
* - GET には必ず `cli_kind=codex` を載せる(載せないと CLAUDE.md 向け本文を掴む)
|
|
30
|
+
* - **PUT は一切行わない**(push / conflict のどちらも)。乖離は warn +
|
|
31
|
+
* `agents-md-receive-only-drift` で見える形にし、ローカルも黙って上書きしない
|
|
32
|
+
* `cli_kind` を載せていなかった頃に作られた壊れた同期状態(AGENTS.md の base に
|
|
33
|
+
* CLAUDE.md 本文が入っている)は、sync の冒頭で meta / base を捨てて初回 sync へ戻す
|
|
34
|
+
* (`migrateBrokenAgentsBase`)。
|
|
35
|
+
*
|
|
26
36
|
* 失敗時のポリシー: ローカル CLAUDE.md は触らない(warn ログ + skip)。
|
|
27
37
|
* session 起動を止めない(ensureClaudeMd と同じく best-effort)。
|
|
28
38
|
*
|
|
@@ -58,6 +68,35 @@ function configPaths(configFilename = "CLAUDE.md") {
|
|
|
58
68
|
}
|
|
59
69
|
}
|
|
60
70
|
|
|
71
|
+
/**
|
|
72
|
+
* backend へ渡す `cli_kind`(レンダリング先の CLI 種別)を設定ファイル名から導出する。
|
|
73
|
+
*
|
|
74
|
+
* Hub は `Project.claude_md` を唯一の正本として持ち、**GET のたびに CLI 別へ
|
|
75
|
+
* レンダリングして返す**(`cli_kind=codex` なら AGENTS.md 向け)。省略すると
|
|
76
|
+
* `claude` 扱いになるため、Codex セッションで載せ忘れると **CLAUDE.md 向けの本文を
|
|
77
|
+
* AGENTS.md へ書き込む**ことになる(実際にそうなっていた)。
|
|
78
|
+
*
|
|
79
|
+
* ⚠️ 判定は `configPaths().filename` を通す(`configFilename` の生値で分岐すると、
|
|
80
|
+
* 未知の値の扱いがこの関数とファイル名解決とで食い違う)。
|
|
81
|
+
*/
|
|
82
|
+
function cliKindForConfig(configFilename) {
|
|
83
|
+
return configPaths(configFilename).filename === "AGENTS.md" ? "codex" : "claude"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* `claude-md-by-dir` の URL を組み立てる(GET / PUT 共通)。
|
|
88
|
+
*
|
|
89
|
+
* ⚠️ `cliKind` は **GET のときだけ** 渡す。PUT はレンダリング結果ではなく正本そのものを
|
|
90
|
+
* 送る経路で、backend 側の解釈が GET と同じとは限らないため、こちらの都合で
|
|
91
|
+
* 勝手にパラメータを増やさない。
|
|
92
|
+
*/
|
|
93
|
+
function claudeMdByDirUrl({ hubUrl, dirName, cliKind }) {
|
|
94
|
+
const base =
|
|
95
|
+
`${hubUrl.replace(/\/+$/, "")}/api/admin/project/claude-md-by-dir/` +
|
|
96
|
+
`?dir_name=${encodeURIComponent(dirName)}`
|
|
97
|
+
return cliKind ? `${base}&cli_kind=${encodeURIComponent(cliKind)}` : base
|
|
98
|
+
}
|
|
99
|
+
|
|
61
100
|
async function readJsonOrNull(p) {
|
|
62
101
|
try {
|
|
63
102
|
const text = await fs.readFile(p, "utf-8")
|
|
@@ -298,14 +337,19 @@ async function fetchHubClaudeMd({
|
|
|
298
337
|
dirName,
|
|
299
338
|
fetchImpl,
|
|
300
339
|
logger,
|
|
340
|
+
configFilename = "CLAUDE.md",
|
|
301
341
|
timeoutMs = HUB_REQUEST_TIMEOUT_MS,
|
|
302
342
|
budget,
|
|
303
343
|
}) {
|
|
304
344
|
const f = fetchImpl || globalThis.fetch
|
|
305
345
|
if (!f) return null
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
346
|
+
// ⚠️ `cli_kind` を必ず載せる。省略すると backend は claude 向けにレンダリングする
|
|
347
|
+
// ので、Codex セッションが CLAUDE.md 向け本文を AGENTS.md の正本として掴む。
|
|
348
|
+
const url = claudeMdByDirUrl({
|
|
349
|
+
hubUrl,
|
|
350
|
+
dirName,
|
|
351
|
+
cliKind: cliKindForConfig(configFilename),
|
|
352
|
+
})
|
|
309
353
|
const deadline = acquireDeadline(budget, timeoutMs)
|
|
310
354
|
try {
|
|
311
355
|
const fetched = await deadline.guard(
|
|
@@ -571,9 +615,7 @@ async function putHubClaudeMd({
|
|
|
571
615
|
}) {
|
|
572
616
|
const f = fetchImpl || globalThis.fetch
|
|
573
617
|
if (!f) return { ok: false, status: null, data: null, error: null }
|
|
574
|
-
const url =
|
|
575
|
-
`${hubUrl.replace(/\/+$/, "")}/api/admin/project/claude-md-by-dir/` +
|
|
576
|
-
`?dir_name=${encodeURIComponent(dirName)}`
|
|
618
|
+
const url = claudeMdByDirUrl({ hubUrl, dirName })
|
|
577
619
|
// expected_hash は **string なら必ず載せる(空文字を含む)**。
|
|
578
620
|
//
|
|
579
621
|
// ⚠️ 空文字は「Hub が空であることを期待する」という **有効な CAS トークン**である。
|
|
@@ -668,6 +710,97 @@ function putResponseUpdatedAt(data) {
|
|
|
668
710
|
return typeof v === "string" && v ? v : null
|
|
669
711
|
}
|
|
670
712
|
|
|
713
|
+
/**
|
|
714
|
+
* **同じ本文を再送しても永久に通らない** 400 拒否の一覧(backend の `error` 値 → reason)。
|
|
715
|
+
*
|
|
716
|
+
* リトライ前提の通信失敗(`hub-put-failed`)と混ぜないために分ける。理由文字列は
|
|
717
|
+
* 拒否ごとに変える — 「CLI 種別を取り違えた」と「プレースホルダを失った平坦化本文を
|
|
718
|
+
* 送った」は原因も直し方も違うので、ログと戻り値で区別できないと調査が止まる。
|
|
719
|
+
*
|
|
720
|
+
* - `config_md_kind_mismatch` … CLAUDE.md の正本へ AGENTS.md の内容を送った等
|
|
721
|
+
* - `config_md_flattened` … レンダリング結果(プレースホルダが展開済み)を正本として
|
|
722
|
+
* 送った。正本にはあるプレースホルダが保存本文に無いので backend が拒否する
|
|
723
|
+
*/
|
|
724
|
+
const PERMANENT_PUT_REJECTIONS = {
|
|
725
|
+
config_md_kind_mismatch: "hub-put-kind-mismatch",
|
|
726
|
+
config_md_flattened: "hub-put-flattened",
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* PUT の拒否理由が「再送しても通らない種類」なら、その reason を返す(違えば null)。
|
|
731
|
+
*
|
|
732
|
+
* ⚠️ 判定は 1 か所に束ねる(push 経路と conflict 経路で条件が分岐すると、片方だけ
|
|
733
|
+
* 新しい拒否理由に対応していない状態が生まれる)。呼び出し側は接尾辞を足すだけ。
|
|
734
|
+
*/
|
|
735
|
+
function permanentPutRejectionReason(putError) {
|
|
736
|
+
const key = putError?.error
|
|
737
|
+
return (typeof key === "string" && PERMANENT_PUT_REJECTIONS[key]) || null
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* 「AGENTS.md の base に CLAUDE.md 本文が入っている」壊れた同期状態か。
|
|
742
|
+
*
|
|
743
|
+
* `cli_kind` を載せずに GET していた頃、Codex セッションは CLAUDE.md 向けの本文を
|
|
744
|
+
* 掴んで AGENTS.md 系の base / meta に書いていた。その base を残したまま新しい
|
|
745
|
+
* (AGENTS.md 向けにレンダリングされた)本文を受け取ると、3-way merge の base が
|
|
746
|
+
* 別 CLI の本文を指したままになる。
|
|
747
|
+
*
|
|
748
|
+
* ⚠️ 判定は **先頭の意味のある行**(空行を飛ばした最初の行)だけで行い、meta の
|
|
749
|
+
* `baseHash` の有無に依存させない。実端末の `agents-md.json` には `baseHash` キーが
|
|
750
|
+
* 無い(旧バージョンが書いた meta)ため、それを条件にすると移行が発火しない。
|
|
751
|
+
*/
|
|
752
|
+
function looksLikeClaudeMdBody(text) {
|
|
753
|
+
if (typeof text !== "string") return false
|
|
754
|
+
for (const rawLine of text.split("\n")) {
|
|
755
|
+
const line = rawLine.trim()
|
|
756
|
+
if (line === "") continue
|
|
757
|
+
return /^#{1,6}\s*CLAUDE\.md\b/.test(line)
|
|
758
|
+
}
|
|
759
|
+
return false
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* 壊れた AGENTS.md の同期状態(base に CLAUDE.md 本文)を捨てて、次回を初回 sync に戻す。
|
|
764
|
+
*
|
|
765
|
+
* 1. ローカル本文を `.bak` へ退避(手元の内容を失わせない)
|
|
766
|
+
* 2. meta → base の順で削除する。**meta を先に消す**のは、meta だけ残ると
|
|
767
|
+
* `repairBaseIfInconsistent` が「ローカル本文で base を書き直す」経路に入り、
|
|
768
|
+
* 壊れた base を作り直しかねないため
|
|
769
|
+
*
|
|
770
|
+
* 呼び出し側はこの回をそこで終える(同一実行内で続けて判定しない = 中間状態を作らない)。
|
|
771
|
+
*
|
|
772
|
+
* @returns {Promise<boolean>} 移行を実行したら true
|
|
773
|
+
*/
|
|
774
|
+
async function migrateBrokenAgentsBase({ targetDir, configFilename, logger, dirName }) {
|
|
775
|
+
const { filename, metaFile, baseFile } = configPaths(configFilename)
|
|
776
|
+
if (filename !== "AGENTS.md") return false
|
|
777
|
+
const base = await readBaseBody(targetDir, configFilename)
|
|
778
|
+
if (!looksLikeClaudeMdBody(base)) return false
|
|
779
|
+
|
|
780
|
+
const localBody = await readLocalConfigMd(targetDir, configFilename)
|
|
781
|
+
await backupLocalConfigMd(targetDir, localBody, "normal", configFilename)
|
|
782
|
+
const metaDir = path.join(targetDir, META_DIR)
|
|
783
|
+
const unlinkIfPresent = async (name) => {
|
|
784
|
+
try {
|
|
785
|
+
await fs.unlink(path.join(metaDir, name))
|
|
786
|
+
} catch (err) {
|
|
787
|
+
if (err?.code !== "ENOENT") throw err
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
await unlinkIfPresent(metaFile)
|
|
791
|
+
await unlinkIfPresent(baseFile)
|
|
792
|
+
logger?.warn?.(
|
|
793
|
+
{
|
|
794
|
+
dirName,
|
|
795
|
+
config_filename: filename,
|
|
796
|
+
base_bytes: Buffer.byteLength(base, "utf-8"),
|
|
797
|
+
local_backed_up: localBody != null,
|
|
798
|
+
},
|
|
799
|
+
"syncClaudeMdWithHub: AGENTS.md sync state held CLAUDE.md content; discarded meta/base so the next sync starts fresh",
|
|
800
|
+
)
|
|
801
|
+
return true
|
|
802
|
+
}
|
|
803
|
+
|
|
671
804
|
async function readLocalConfigMd(targetDir, configFilename) {
|
|
672
805
|
try {
|
|
673
806
|
return await fs.readFile(path.join(targetDir, configPaths(configFilename).filename), "utf-8")
|
|
@@ -946,15 +1079,35 @@ export async function syncClaudeMdWithHub({
|
|
|
946
1079
|
)
|
|
947
1080
|
return { action: "skipped", reason: "budget-exhausted" }
|
|
948
1081
|
}
|
|
1082
|
+
|
|
1083
|
+
// 0) 壊れた同期状態(AGENTS.md の base に CLAUDE.md 本文)の移行。
|
|
1084
|
+
//
|
|
1085
|
+
// ⚠️ **通信より前・判定より前**に置き、実行したらその回はそこで終える。同一実行内で
|
|
1086
|
+
// 続けて判定すると「meta を消した直後の初回 sync」が同じ回に走り、移行が中途半端に
|
|
1087
|
+
// 効いた中間状態を作る(次回セッションの初回 sync で pull させれば足りる)。
|
|
1088
|
+
try {
|
|
1089
|
+
if (await migrateBrokenAgentsBase({ targetDir, configFilename, logger, dirName })) {
|
|
1090
|
+
return { action: "skipped", reason: "agents-md-base-migrated" }
|
|
1091
|
+
}
|
|
1092
|
+
} catch (err) {
|
|
1093
|
+
// 退避 / 削除に失敗した回は同期を進めない(壊れた base を基準に使わせない)。
|
|
1094
|
+
logger?.warn?.(
|
|
1095
|
+
{ dirName, err: err?.message || String(err) },
|
|
1096
|
+
"syncClaudeMdWithHub: failed to discard the broken AGENTS.md sync state; skip (retry next sync)",
|
|
1097
|
+
)
|
|
1098
|
+
return { action: "skipped", reason: "agents-md-base-migration-failed" }
|
|
1099
|
+
}
|
|
1100
|
+
|
|
949
1101
|
const url = hubUrl || DEFAULT_HUB_API
|
|
950
1102
|
|
|
951
|
-
// 1) Hub
|
|
1103
|
+
// 1) Hub の正本を取得(`cli_kind` を載せて CLI 別レンダリング結果を受け取る)
|
|
952
1104
|
const hub = await fetchHubClaudeMd({
|
|
953
1105
|
hubUrl: url,
|
|
954
1106
|
accessToken: token,
|
|
955
1107
|
dirName,
|
|
956
1108
|
fetchImpl,
|
|
957
1109
|
logger,
|
|
1110
|
+
configFilename,
|
|
958
1111
|
timeoutMs: requestTimeoutMs,
|
|
959
1112
|
budget,
|
|
960
1113
|
})
|
|
@@ -1291,6 +1444,39 @@ export async function syncClaudeMdWithHub({
|
|
|
1291
1444
|
return { action: "pull" }
|
|
1292
1445
|
}
|
|
1293
1446
|
|
|
1447
|
+
// ---- AGENTS.md は受け取り専用(ここから下は必ず localChanged === true)--------
|
|
1448
|
+
//
|
|
1449
|
+
// Hub は `Project.claude_md`(CLAUDE.md 正本)を CLI 別にレンダリングして返す。
|
|
1450
|
+
// AGENTS.md はその **レンダリング結果** であって正本ではないので、端末側で編集された
|
|
1451
|
+
// からといって Hub へ送り返してはいけない(正本にプレースホルダが残っていても、
|
|
1452
|
+
// 送るのは展開済みの平坦な本文になる = backend の平坦化ガードに 400 で弾かれる)。
|
|
1453
|
+
//
|
|
1454
|
+
// ⚠️ **push 経路と conflict 経路で別々に判定しない**。ここから下は両方とも
|
|
1455
|
+
// 「ローカルが(前回同期時点から)変化した」経路で、PUT する / しないの分岐は
|
|
1456
|
+
// 1 か所に置く。経路ごとに掛けると、片方だけ塞がった状態が生まれる。
|
|
1457
|
+
//
|
|
1458
|
+
// ⚠️ **ローカルを黙って上書きもしない**。conflict 側で「端末が勝てないなら Hub で
|
|
1459
|
+
// 上書き」に倒すと、退避 `.bak` は残っても人は気づかず、端末の編集が事実上消える。
|
|
1460
|
+
// 乖離は **warn + 専用の reason** で見える形にして残し、取り込みは人に委ねる。
|
|
1461
|
+
//
|
|
1462
|
+
// ⚠️ ログに本文を載せないこと(接続情報が混ざりうる)。出してよいのは長さ・行数・
|
|
1463
|
+
// ハッシュのような、中身を復元できない指標だけ。
|
|
1464
|
+
if (configPaths(configFilename).filename === "AGENTS.md") {
|
|
1465
|
+
logger?.warn?.(
|
|
1466
|
+
{
|
|
1467
|
+
dirName,
|
|
1468
|
+
config_filename: "AGENTS.md",
|
|
1469
|
+
hub_changed: hubChanged,
|
|
1470
|
+
local_bytes: localBody == null ? 0 : Buffer.byteLength(localBody, "utf-8"),
|
|
1471
|
+
local_lines: localBody == null ? 0 : localBody.split("\n").length,
|
|
1472
|
+
local_hash: localCurrentHash,
|
|
1473
|
+
hub_hash: hubHash,
|
|
1474
|
+
},
|
|
1475
|
+
"syncClaudeMdWithHub: local AGENTS.md drifted from the rendered hub source of truth; AGENTS.md is receive-only so nothing was pushed and nothing was overwritten (edit CLAUDE.md on Hub, then let the next sync pull)",
|
|
1476
|
+
)
|
|
1477
|
+
return { action: "skipped", reason: "agents-md-receive-only-drift" }
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1294
1480
|
if (!hubChanged && localChanged) {
|
|
1295
1481
|
// ⚠️ hash の観測と snapshot の自己整合性は **入口の門で保証済み**。ここに
|
|
1296
1482
|
// 個別チェックを再掲しないこと(検査の本体が 2 つになると、片方だけ適用範囲が
|
|
@@ -1320,14 +1506,15 @@ export async function syncClaudeMdWithHub({
|
|
|
1320
1506
|
)
|
|
1321
1507
|
return { action: "skipped", reason: "hub-put-stale" }
|
|
1322
1508
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
//
|
|
1509
|
+
const permanent = permanentPutRejectionReason(put.error)
|
|
1510
|
+
if (permanent) {
|
|
1511
|
+
// CLI 種別の取り違え / 平坦化本文の送信。**同じ本文を再送しても永久に通らない**
|
|
1512
|
+
// ので、リトライ前提の通信失敗と混ぜない(meta も進めない)。
|
|
1326
1513
|
logger?.warn?.(
|
|
1327
1514
|
{ dirName, ...put.error },
|
|
1328
|
-
"syncClaudeMdWithHub: push rejected
|
|
1515
|
+
"syncClaudeMdWithHub: push rejected by a permanent guard; resending the same body will keep failing",
|
|
1329
1516
|
)
|
|
1330
|
-
return { action: "skipped", reason:
|
|
1517
|
+
return { action: "skipped", reason: permanent }
|
|
1331
1518
|
}
|
|
1332
1519
|
return { action: "skipped", reason: "hub-put-failed" }
|
|
1333
1520
|
}
|
|
@@ -1389,13 +1576,14 @@ export async function syncClaudeMdWithHub({
|
|
|
1389
1576
|
)
|
|
1390
1577
|
return { action: "skipped", reason: "hub-put-stale-in-conflict" }
|
|
1391
1578
|
}
|
|
1392
|
-
|
|
1579
|
+
const permanent = permanentPutRejectionReason(put.error)
|
|
1580
|
+
if (permanent) {
|
|
1393
1581
|
// push 経路と同じ理由(再送しても通らない拒否)。通信失敗と区別する。
|
|
1394
1582
|
logger?.warn?.(
|
|
1395
1583
|
{ dirName, ...put.error },
|
|
1396
|
-
"syncClaudeMdWithHub: conflict push rejected
|
|
1584
|
+
"syncClaudeMdWithHub: conflict push rejected by a permanent guard; resending the same body will keep failing",
|
|
1397
1585
|
)
|
|
1398
|
-
return { action: "skipped", reason:
|
|
1586
|
+
return { action: "skipped", reason: `${permanent}-in-conflict` }
|
|
1399
1587
|
}
|
|
1400
1588
|
return { action: "skipped", reason: "hub-put-failed-in-conflict" }
|
|
1401
1589
|
}
|
|
@@ -1441,6 +1629,11 @@ export {
|
|
|
1441
1629
|
readBaseBody,
|
|
1442
1630
|
nowIso,
|
|
1443
1631
|
configPaths,
|
|
1632
|
+
// `cli_kind` の導出と URL 組み立ては初期化経路(claude-md.mjs)でも使う。
|
|
1633
|
+
// ⚠️ 複製しないこと(片方だけ `cli_kind` を載せ忘れると、初回配信だけ別 CLI 向けの
|
|
1634
|
+
// 本文を掴むという非対称が生まれる)。
|
|
1635
|
+
cliKindForConfig,
|
|
1636
|
+
claudeMdByDirUrl,
|
|
1444
1637
|
}
|
|
1445
1638
|
|
|
1446
1639
|
// 内部テスト用エクスポート
|
|
@@ -1458,4 +1651,7 @@ export const _internals = {
|
|
|
1458
1651
|
// 壊れるため、両者を突き合わせるテストから参照できるように公開する。
|
|
1459
1652
|
makeBakName,
|
|
1460
1653
|
bakNamePattern,
|
|
1654
|
+
cliKindForConfig,
|
|
1655
|
+
claudeMdByDirUrl,
|
|
1656
|
+
looksLikeClaudeMdBody,
|
|
1461
1657
|
}
|
package/src/claude-md.mjs
CHANGED
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
nowIso,
|
|
41
41
|
isHubSnapshotTrustworthy,
|
|
42
42
|
usableHubUpdatedAt,
|
|
43
|
+
cliKindForConfig,
|
|
44
|
+
claudeMdByDirUrl,
|
|
43
45
|
} from "./claude-md-sync.mjs"
|
|
44
46
|
import {
|
|
45
47
|
HUB_REQUEST_TIMEOUT_MS,
|
|
@@ -92,12 +94,18 @@ async function fetchHubClaudeMdProject({
|
|
|
92
94
|
fetchImpl,
|
|
93
95
|
deadline,
|
|
94
96
|
logger,
|
|
97
|
+
configFilename = "CLAUDE.md",
|
|
95
98
|
}) {
|
|
96
99
|
const f = fetchImpl || globalThis.fetch
|
|
97
100
|
if (!f) return null
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
// ⚠️ 同期側(`fetchHubClaudeMd`)と **同じ導出・同じ URL 組み立て**を使う。初回配信だけ
|
|
102
|
+
// `cli_kind` を落とすと、Codex の初回セッションが CLAUDE.md 向けの本文を AGENTS.md
|
|
103
|
+
// として書き込み、その本文が base / meta(同期の基準)にまで入る。
|
|
104
|
+
const url = claudeMdByDirUrl({
|
|
105
|
+
hubUrl,
|
|
106
|
+
dirName,
|
|
107
|
+
cliKind: cliKindForConfig(configFilename),
|
|
108
|
+
})
|
|
101
109
|
try {
|
|
102
110
|
const fetched = await deadline.guard(
|
|
103
111
|
// 同期的に throw する fetch 実装でも guard に載るよう then で包む。
|
|
@@ -325,6 +333,7 @@ export async function ensureClaudeMd({
|
|
|
325
333
|
fetchImpl,
|
|
326
334
|
deadline,
|
|
327
335
|
logger,
|
|
336
|
+
configFilename,
|
|
328
337
|
})
|
|
329
338
|
} finally {
|
|
330
339
|
deadline.clear()
|
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
|