@tansr/sdk 0.11.0 → 0.11.1
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/dist/index.d.ts +100 -2
- package/dist/index.js +1197 -322
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,20 +16,20 @@ var TOOL_EFFECTS = ["irreversible", "financial", "external", "affects-others"];
|
|
|
16
16
|
// ../kernel/src/world/default-fs.ts
|
|
17
17
|
import fs from "node:fs/promises";
|
|
18
18
|
var defaultFs = {
|
|
19
|
-
stat(
|
|
20
|
-
return fs.stat(
|
|
19
|
+
stat(path29) {
|
|
20
|
+
return fs.stat(path29);
|
|
21
21
|
},
|
|
22
|
-
lstat(
|
|
23
|
-
return fs.lstat(
|
|
22
|
+
lstat(path29) {
|
|
23
|
+
return fs.lstat(path29);
|
|
24
24
|
},
|
|
25
|
-
realpath(
|
|
26
|
-
return fs.realpath(
|
|
25
|
+
realpath(path29) {
|
|
26
|
+
return fs.realpath(path29);
|
|
27
27
|
},
|
|
28
|
-
readFile(
|
|
29
|
-
return fs.readFile(
|
|
28
|
+
readFile(path29) {
|
|
29
|
+
return fs.readFile(path29);
|
|
30
30
|
},
|
|
31
|
-
async readFilePrefix(
|
|
32
|
-
const handle = await fs.open(
|
|
31
|
+
async readFilePrefix(path29, bytes) {
|
|
32
|
+
const handle = await fs.open(path29, "r");
|
|
33
33
|
try {
|
|
34
34
|
const prefix = Buffer.alloc(bytes);
|
|
35
35
|
const { bytesRead } = await handle.read(prefix, 0, bytes, 0);
|
|
@@ -38,17 +38,17 @@ var defaultFs = {
|
|
|
38
38
|
await handle.close();
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
|
-
async writeFile(
|
|
42
|
-
await fs.writeFile(
|
|
41
|
+
async writeFile(path29, data) {
|
|
42
|
+
await fs.writeFile(path29, data);
|
|
43
43
|
},
|
|
44
|
-
async appendFile(
|
|
45
|
-
await fs.appendFile(
|
|
44
|
+
async appendFile(path29, data) {
|
|
45
|
+
await fs.appendFile(path29, data);
|
|
46
46
|
},
|
|
47
|
-
async mkdir(
|
|
48
|
-
await fs.mkdir(
|
|
47
|
+
async mkdir(path29, opts) {
|
|
48
|
+
await fs.mkdir(path29, opts);
|
|
49
49
|
},
|
|
50
|
-
readdir(
|
|
51
|
-
return fs.readdir(
|
|
50
|
+
readdir(path29) {
|
|
51
|
+
return fs.readdir(path29, { withFileTypes: true });
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
54
|
|
|
@@ -3456,29 +3456,29 @@ function buildSpillStubText(input) {
|
|
|
3456
3456
|
].join("\n");
|
|
3457
3457
|
}
|
|
3458
3458
|
function assertSpillStoreInvariants() {
|
|
3459
|
-
const
|
|
3459
|
+
const fail2 = (detail) => {
|
|
3460
3460
|
throw new Error(`spill-store 不变式违约(装载期 fail-fast):${detail}`);
|
|
3461
3461
|
};
|
|
3462
3462
|
const nameRe = /^[A-Za-z0-9._-]+$/;
|
|
3463
3463
|
if (!nameRe.test(SPILL_DIR_NAME)) {
|
|
3464
|
-
|
|
3464
|
+
fail2(`SPILL_DIR_NAME 必须为安全文件名词形,得到 ${JSON.stringify(SPILL_DIR_NAME)}`);
|
|
3465
3465
|
}
|
|
3466
3466
|
if (!nameRe.test(SPILL_PRUNE_FILE_PREFIX)) {
|
|
3467
|
-
|
|
3467
|
+
fail2(`SPILL_PRUNE_FILE_PREFIX 必须为安全文件名词形,得到 ${JSON.stringify(SPILL_PRUNE_FILE_PREFIX)}`);
|
|
3468
3468
|
}
|
|
3469
3469
|
if (!Number.isInteger(SPILL_HASH_HEX_CHARS) || SPILL_HASH_HEX_CHARS < 16 || SPILL_HASH_HEX_CHARS > 64) {
|
|
3470
|
-
|
|
3470
|
+
fail2(`SPILL_HASH_HEX_CHARS 必须在 [16, 64](sha256 hex 截断域),得到 ${SPILL_HASH_HEX_CHARS}`);
|
|
3471
3471
|
}
|
|
3472
3472
|
const emptyHash = hashSpillContent("");
|
|
3473
3473
|
if (emptyHash !== "e3b0c44298fc1c149afbf4c8996fb924".slice(0, SPILL_HASH_HEX_CHARS)) {
|
|
3474
|
-
|
|
3474
|
+
fail2(`hashSpillContent 算法漂移:sha256('') 截断应为公知值,得到 ${emptyHash}`);
|
|
3475
3475
|
}
|
|
3476
3476
|
if (spillFileName("content", emptyHash) === spillFileName("prune", emptyHash)) {
|
|
3477
|
-
|
|
3477
|
+
fail2("content 与 prune 命名空间的文件名必须互异");
|
|
3478
3478
|
}
|
|
3479
3479
|
const sample = buildSpillStubText({ path: "X:/s/spill/abc.txt", totalChars: 12345 });
|
|
3480
3480
|
if (!sample.includes("12345") || !sample.includes("X:/s/spill/abc.txt") || !sample.includes("Read")) {
|
|
3481
|
-
|
|
3481
|
+
fail2("buildSpillStubText 必须恒含原始规模、真实路径与 Read 指引");
|
|
3482
3482
|
}
|
|
3483
3483
|
}
|
|
3484
3484
|
assertSpillStoreInvariants();
|
|
@@ -4342,63 +4342,63 @@ var CONTEXT_BUDGET_REMINDER_CRITICAL_FRACTION = 0.1;
|
|
|
4342
4342
|
var CONTEXT_BUDGET_REMINDER_ROUND_TOKENS = 1e3;
|
|
4343
4343
|
var CONTEXT_BUDGET_REMINDER_MIN_BUDGET_TOKENS = 2e4;
|
|
4344
4344
|
function assertEntryBudgetInvariants() {
|
|
4345
|
-
const
|
|
4345
|
+
const fail2 = (detail) => {
|
|
4346
4346
|
throw new Error(`entry-budgets 不变式违约(装载期 fail-fast):${detail}`);
|
|
4347
4347
|
};
|
|
4348
4348
|
if (DEFAULT_TOOL_RESULT_BUDGET_CHARS !== 5e4) {
|
|
4349
|
-
|
|
4349
|
+
fail2(
|
|
4350
4350
|
`DEFAULT_TOOL_RESULT_BUDGET_CHARS 必须为 50000(用户拍板 2026-08-21 维持不动),得到 ${DEFAULT_TOOL_RESULT_BUDGET_CHARS}`
|
|
4351
4351
|
);
|
|
4352
4352
|
}
|
|
4353
4353
|
if (PERSISTED_PREVIEW_HEAD_CHARS + PERSISTED_PREVIEW_TAIL_CHARS >= DEFAULT_TOOL_RESULT_BUDGET_CHARS) {
|
|
4354
|
-
|
|
4354
|
+
fail2("clamp 存根预览(head+tail)必须严格小于工具结果预算");
|
|
4355
4355
|
}
|
|
4356
4356
|
for (const [name, value] of [
|
|
4357
4357
|
["READ_MAX_LINES", READ_MAX_LINES],
|
|
4358
4358
|
["READ_MAX_LINE_CHARS", READ_MAX_LINE_CHARS],
|
|
4359
4359
|
["READ_MAX_FULL_BYTES", READ_MAX_FULL_BYTES]
|
|
4360
4360
|
]) {
|
|
4361
|
-
if (!Number.isInteger(value) || value <= 0)
|
|
4361
|
+
if (!Number.isInteger(value) || value <= 0) fail2(`${name} 必须为正整数,得到 ${value}`);
|
|
4362
4362
|
}
|
|
4363
4363
|
if (!Number.isInteger(PRUNE_MIN_RESULT_CHARS) || !Number.isInteger(PRUNE_HEAD_CHARS) || !Number.isInteger(PRUNE_TAIL_CHARS) || PRUNE_HEAD_CHARS < 0 || PRUNE_TAIL_CHARS < 0 || PRUNE_MIN_RESULT_CHARS <= 0) {
|
|
4364
|
-
|
|
4364
|
+
fail2("剪枝常量必须为非负整数且门槛为正");
|
|
4365
4365
|
}
|
|
4366
4366
|
if (PRUNE_HEAD_CHARS + PRUNE_TAIL_CHARS + PRUNE_MARKER_MAX_CHARS >= PRUNE_MIN_RESULT_CHARS) {
|
|
4367
|
-
|
|
4367
|
+
fail2(
|
|
4368
4368
|
`剪后产物上界(head ${PRUNE_HEAD_CHARS} + tail ${PRUNE_TAIL_CHARS} + 标记 ≤${PRUNE_MARKER_MAX_CHARS})必须严格小于剪枝门槛 ${PRUNE_MIN_RESULT_CHARS}——否则剪后仍是候选,重复触发字节漂移`
|
|
4369
4369
|
);
|
|
4370
4370
|
}
|
|
4371
4371
|
if (PRUNE_MIN_RESULT_CHARS >= DEFAULT_TOOL_RESULT_BUDGET_CHARS) {
|
|
4372
|
-
|
|
4372
|
+
fail2("剪枝门槛必须低于工具结果 clamp 预算(50K 之上无剪枝对象)");
|
|
4373
4373
|
}
|
|
4374
4374
|
if (!Number.isInteger(SUBAGENT_SUMMARY_MAX_TOKENS) || SUBAGENT_SUMMARY_MAX_TOKENS <= 0) {
|
|
4375
|
-
|
|
4375
|
+
fail2(`SUBAGENT_SUMMARY_MAX_TOKENS 必须为正整数,得到 ${SUBAGENT_SUMMARY_MAX_TOKENS}`);
|
|
4376
4376
|
}
|
|
4377
4377
|
if (!Number.isInteger(DEFAULT_OUTPUT_CAP_TOKENS) || DEFAULT_OUTPUT_CAP_TOKENS < 1024) {
|
|
4378
|
-
|
|
4378
|
+
fail2(`DEFAULT_OUTPUT_CAP_TOKENS 必须为 ≥1024 的整数(输出预算地板),得到 ${DEFAULT_OUTPUT_CAP_TOKENS}`);
|
|
4379
4379
|
}
|
|
4380
4380
|
if (!Number.isInteger(OUTPUT_CAP_ESCALATION_TOKENS) || OUTPUT_CAP_ESCALATION_TOKENS <= DEFAULT_OUTPUT_CAP_TOKENS) {
|
|
4381
|
-
|
|
4381
|
+
fail2(
|
|
4382
4382
|
`OUTPUT_CAP_ESCALATION_TOKENS(${OUTPUT_CAP_ESCALATION_TOKENS})必须为严格大于缺省输出帽 ${DEFAULT_OUTPUT_CAP_TOKENS} 的整数——否则触帽升档恒无增益`
|
|
4383
4383
|
);
|
|
4384
4384
|
}
|
|
4385
4385
|
if (!(CONTEXT_BUDGET_REMINDER_CRITICAL_FRACTION > 0) || !(CONTEXT_BUDGET_REMINDER_CRITICAL_FRACTION < CONTEXT_BUDGET_REMINDER_LOW_FRACTION) || !(CONTEXT_BUDGET_REMINDER_LOW_FRACTION < 1)) {
|
|
4386
|
-
|
|
4386
|
+
fail2(
|
|
4387
4387
|
`预算提醒档位必须满足 0 < critical(${CONTEXT_BUDGET_REMINDER_CRITICAL_FRACTION}) < low(${CONTEXT_BUDGET_REMINDER_LOW_FRACTION}) < 1`
|
|
4388
4388
|
);
|
|
4389
4389
|
}
|
|
4390
4390
|
if (!Number.isInteger(CONTEXT_BUDGET_REMINDER_ROUND_TOKENS) || CONTEXT_BUDGET_REMINDER_ROUND_TOKENS <= 0) {
|
|
4391
|
-
|
|
4391
|
+
fail2(
|
|
4392
4392
|
`CONTEXT_BUDGET_REMINDER_ROUND_TOKENS 必须为正整数,得到 ${CONTEXT_BUDGET_REMINDER_ROUND_TOKENS}`
|
|
4393
4393
|
);
|
|
4394
4394
|
}
|
|
4395
4395
|
if (!Number.isInteger(CONTEXT_BUDGET_REMINDER_MIN_BUDGET_TOKENS) || CONTEXT_BUDGET_REMINDER_MIN_BUDGET_TOKENS < CONTEXT_BUDGET_REMINDER_ROUND_TOKENS) {
|
|
4396
|
-
|
|
4396
|
+
fail2(
|
|
4397
4397
|
`CONTEXT_BUDGET_REMINDER_MIN_BUDGET_TOKENS(${CONTEXT_BUDGET_REMINDER_MIN_BUDGET_TOKENS})必须为 ≥ 取整粒度 ${CONTEXT_BUDGET_REMINDER_ROUND_TOKENS} 的整数`
|
|
4398
4398
|
);
|
|
4399
4399
|
}
|
|
4400
4400
|
if (EMPTY_TOOL_RESULT_TEXT.length === 0 || EMPTY_TOOL_ERROR_TEXT.length === 0) {
|
|
4401
|
-
|
|
4401
|
+
fail2("空结果短桩短语不得为空串");
|
|
4402
4402
|
}
|
|
4403
4403
|
}
|
|
4404
4404
|
assertEntryBudgetInvariants();
|
|
@@ -4602,74 +4602,74 @@ var LIFECYCLE_REPLAY_ORDER = [
|
|
|
4602
4602
|
"evict"
|
|
4603
4603
|
];
|
|
4604
4604
|
function assertHistoryLifecycleInvariants() {
|
|
4605
|
-
const
|
|
4605
|
+
const fail2 = (detail) => {
|
|
4606
4606
|
throw new Error(`历史生命周期状态机不变式违约(装载期 fail-fast):${detail}`);
|
|
4607
4607
|
};
|
|
4608
4608
|
if (new Set(HISTORY_LIFECYCLE_STATES).size !== HISTORY_LIFECYCLE_STATES.length) {
|
|
4609
|
-
|
|
4609
|
+
fail2("态集成员必须互异");
|
|
4610
4610
|
}
|
|
4611
4611
|
if (!PRUNE_MARKER_PREFIX.startsWith("[") || PRUNE_MARKER_PREFIX.length < 8) {
|
|
4612
|
-
|
|
4612
|
+
fail2(`PRUNE_MARKER_PREFIX 词形异常(冻结字节被改动):${JSON.stringify(PRUNE_MARKER_PREFIX)}`);
|
|
4613
4613
|
}
|
|
4614
4614
|
const ids = /* @__PURE__ */ new Set();
|
|
4615
4615
|
let lastStage = -1;
|
|
4616
4616
|
const stateIndex = (s) => HISTORY_LIFECYCLE_STATES.indexOf(s);
|
|
4617
4617
|
for (const transition of LIFECYCLE_TRANSITIONS) {
|
|
4618
|
-
if (ids.has(transition.id))
|
|
4618
|
+
if (ids.has(transition.id)) fail2(`迁移 id 重复:${transition.id}`);
|
|
4619
4619
|
ids.add(transition.id);
|
|
4620
4620
|
if (transition.stage <= lastStage) {
|
|
4621
|
-
|
|
4621
|
+
fail2(`迁移 stage 必须严格递增(全序仲裁的根):${transition.id} stage=${transition.stage}`);
|
|
4622
4622
|
}
|
|
4623
4623
|
lastStage = transition.stage;
|
|
4624
4624
|
const toIndex = stateIndex(transition.to);
|
|
4625
|
-
if (toIndex < 0)
|
|
4625
|
+
if (toIndex < 0) fail2(`迁移 ${transition.id} 目标态不在态集:${transition.to}`);
|
|
4626
4626
|
if (transition.from !== "ingestion") {
|
|
4627
4627
|
for (const from of transition.from) {
|
|
4628
|
-
if (stateIndex(from) < 0)
|
|
4628
|
+
if (stateIndex(from) < 0) fail2(`迁移 ${transition.id} 源态不在态集:${from}`);
|
|
4629
4629
|
if (stateIndex(from) >= toIndex) {
|
|
4630
|
-
|
|
4630
|
+
fail2(`迁移 ${transition.id} 违反单向前进(${from} → ${transition.to}):恒无复活迁移`);
|
|
4631
4631
|
}
|
|
4632
4632
|
}
|
|
4633
4633
|
}
|
|
4634
4634
|
}
|
|
4635
4635
|
for (const id of [...LIFECYCLE_BARRIER_ORDER, ...LIFECYCLE_REPLAY_ORDER]) {
|
|
4636
|
-
if (!ids.has(id))
|
|
4636
|
+
if (!ids.has(id)) fail2(`仲裁/重演序引用未登记迁移:${id}`);
|
|
4637
4637
|
}
|
|
4638
4638
|
if (!Number.isInteger(DEFAULT_KEEP_RECENT_ROUNDS) || DEFAULT_KEEP_RECENT_ROUNDS < 1) {
|
|
4639
|
-
|
|
4639
|
+
fail2(`DEFAULT_KEEP_RECENT_ROUNDS 必须为 ≥1 整数,得到 ${DEFAULT_KEEP_RECENT_ROUNDS}`);
|
|
4640
4640
|
}
|
|
4641
4641
|
if (!Number.isInteger(DEFAULT_MICROCOMPACT_KEEP_RECENT_TOOL_RESULTS) || DEFAULT_MICROCOMPACT_KEEP_RECENT_TOOL_RESULTS < 1) {
|
|
4642
|
-
|
|
4642
|
+
fail2(
|
|
4643
4643
|
`DEFAULT_MICROCOMPACT_KEEP_RECENT_TOOL_RESULTS 必须为 ≥1 整数,得到 ${DEFAULT_MICROCOMPACT_KEEP_RECENT_TOOL_RESULTS}`
|
|
4644
4644
|
);
|
|
4645
4645
|
}
|
|
4646
4646
|
if (!Number.isInteger(DEFAULT_MICROCOMPACT_MIN_RESULT_CHARS) || DEFAULT_MICROCOMPACT_MIN_RESULT_CHARS <= 0) {
|
|
4647
|
-
|
|
4647
|
+
fail2(
|
|
4648
4648
|
`DEFAULT_MICROCOMPACT_MIN_RESULT_CHARS 必须为正整数,得到 ${DEFAULT_MICROCOMPACT_MIN_RESULT_CHARS}`
|
|
4649
4649
|
);
|
|
4650
4650
|
}
|
|
4651
4651
|
if (PRUNE_MIN_RESULT_CHARS <= DEFAULT_MICROCOMPACT_MIN_RESULT_CHARS) {
|
|
4652
|
-
|
|
4652
|
+
fail2(
|
|
4653
4653
|
`剪枝门槛(${PRUNE_MIN_RESULT_CHARS})必须严格大于淘汰门槛(${DEFAULT_MICROCOMPACT_MIN_RESULT_CHARS}):prune 是 evict 的前置温和档`
|
|
4654
4654
|
);
|
|
4655
4655
|
}
|
|
4656
4656
|
if (PRUNE_HEAD_CHARS + PRUNE_TAIL_CHARS < DEFAULT_MICROCOMPACT_MIN_RESULT_CHARS) {
|
|
4657
|
-
|
|
4657
|
+
fail2(
|
|
4658
4658
|
`剪后产物下界(head ${PRUNE_HEAD_CHARS} + tail ${PRUNE_TAIL_CHARS})必须 ≥ 淘汰门槛 ${DEFAULT_MICROCOMPACT_MIN_RESULT_CHARS}——否则 pruned 态脱离 evict 候选域,两档接力断链`
|
|
4659
4659
|
);
|
|
4660
4660
|
}
|
|
4661
4661
|
if (DEFAULT_MICROCOMPACT_MIN_RESULT_CHARS >= DEFAULT_TOOL_RESULT_BUDGET_CHARS) {
|
|
4662
|
-
|
|
4662
|
+
fail2(
|
|
4663
4663
|
`淘汰门槛(${DEFAULT_MICROCOMPACT_MIN_RESULT_CHARS})必须小于入史 clamp 预算(${DEFAULT_TOOL_RESULT_BUDGET_CHARS}):淘汰重演的文件在场性判据依赖两带不重叠`
|
|
4664
4664
|
);
|
|
4665
4665
|
}
|
|
4666
4666
|
if (DEGRADED_ESCALATED_KEEP_RECENT_TOOL_RESULTS > DEFAULT_MICROCOMPACT_KEEP_RECENT_TOOL_RESULTS) {
|
|
4667
|
-
|
|
4667
|
+
fail2(
|
|
4668
4668
|
`劣化升级保护窗(${DEGRADED_ESCALATED_KEEP_RECENT_TOOL_RESULTS})不得大于默认窗(${DEFAULT_MICROCOMPACT_KEEP_RECENT_TOOL_RESULTS}):升级恒收窄`
|
|
4669
4669
|
);
|
|
4670
4670
|
}
|
|
4671
4671
|
if (DEGRADED_ESCALATED_MIN_RESULT_CHARS > DEFAULT_MICROCOMPACT_MIN_RESULT_CHARS) {
|
|
4672
|
-
|
|
4672
|
+
fail2(
|
|
4673
4673
|
`劣化升级淘汰门槛(${DEGRADED_ESCALATED_MIN_RESULT_CHARS})不得大于默认门槛(${DEFAULT_MICROCOMPACT_MIN_RESULT_CHARS}):升级恒收窄`
|
|
4674
4674
|
);
|
|
4675
4675
|
}
|
|
@@ -11845,10 +11845,10 @@ function matchHardSafety(input, config = {}) {
|
|
|
11845
11845
|
if (typeof command !== "string") return null;
|
|
11846
11846
|
const structure = parseCommandStructure(command);
|
|
11847
11847
|
if (!structure.ok) return null;
|
|
11848
|
-
for (const
|
|
11849
|
-
const hasEgress =
|
|
11848
|
+
for (const pipeline3 of structure.pipelines) {
|
|
11849
|
+
const hasEgress = pipeline3.commands.some((c) => isEgressCommand(c, extraEgress));
|
|
11850
11850
|
if (!hasEgress) continue;
|
|
11851
|
-
const refsSecret =
|
|
11851
|
+
const refsSecret = pipeline3.commands.some((c) => nodeReferencesSecret(c, input.cwd));
|
|
11852
11852
|
if (refsSecret) {
|
|
11853
11853
|
return {
|
|
11854
11854
|
label: "secret-egress",
|
|
@@ -11942,13 +11942,13 @@ function canonicalCwd(cwd) {
|
|
|
11942
11942
|
const resolved = resolveForPermission(cwd, ".");
|
|
11943
11943
|
return resolved.kind === "ok" ? resolved.path : null;
|
|
11944
11944
|
}
|
|
11945
|
-
function canonicalPathEligible(cwdCanon,
|
|
11946
|
-
if (hasWindowsDeviceSegment(
|
|
11947
|
-
if (
|
|
11948
|
-
if (!
|
|
11949
|
-
if (guard.isProtectedContentPath(
|
|
11950
|
-
if (matchProtectedPath(
|
|
11951
|
-
const segments =
|
|
11945
|
+
function canonicalPathEligible(cwdCanon, path29, policy, guard) {
|
|
11946
|
+
if (hasWindowsDeviceSegment(path29)) return false;
|
|
11947
|
+
if (path29 === cwdCanon) return !policy.strict;
|
|
11948
|
+
if (!path29.startsWith(`${cwdCanon}/`)) return false;
|
|
11949
|
+
if (guard.isProtectedContentPath(path29)) return false;
|
|
11950
|
+
if (matchProtectedPath(path29, null) !== null) return false;
|
|
11951
|
+
const segments = path29.slice(cwdCanon.length + 1).split("/");
|
|
11952
11952
|
for (const [index, seg] of segments.entries()) {
|
|
11953
11953
|
const hasGlob = GLOB_CHAR_RE.test(seg);
|
|
11954
11954
|
if (hasGlob && seg.startsWith(".")) return false;
|
|
@@ -12132,9 +12132,9 @@ function verifyBundle(bundle, opts = {}) {
|
|
|
12132
12132
|
// ../kernel/src/permissions/soften-radius.ts
|
|
12133
12133
|
var PROTECTED_PATH_RULE_PREFIX = "protected-path(";
|
|
12134
12134
|
var MCP_TOOL_PREFIX = "mcp__";
|
|
12135
|
-
function pathInsideRoot(
|
|
12135
|
+
function pathInsideRoot(path29, root) {
|
|
12136
12136
|
if (root === null) return false;
|
|
12137
|
-
return
|
|
12137
|
+
return path29 === root || path29.startsWith(root.endsWith("/") ? root : `${root}/`);
|
|
12138
12138
|
}
|
|
12139
12139
|
function classifySoftenFace(input, base) {
|
|
12140
12140
|
if (base.matchedRule?.startsWith(PROTECTED_PATH_RULE_PREFIX) === true) return "F6";
|
|
@@ -14096,14 +14096,14 @@ var DispatchingToolExecutor = class _DispatchingToolExecutor {
|
|
|
14096
14096
|
let internalError;
|
|
14097
14097
|
const onCtxAbort = () => wake.wake();
|
|
14098
14098
|
ctx.signal.addEventListener("abort", onCtxAbort, { once: true });
|
|
14099
|
-
const
|
|
14099
|
+
const drain2 = function* () {
|
|
14100
14100
|
while (queued.length > 0) {
|
|
14101
14101
|
yield { t: "event", body: queued.shift() };
|
|
14102
14102
|
}
|
|
14103
14103
|
};
|
|
14104
14104
|
const waitUntil = async function* (cond) {
|
|
14105
14105
|
for (; ; ) {
|
|
14106
|
-
yield*
|
|
14106
|
+
yield* drain2();
|
|
14107
14107
|
if (cond()) return;
|
|
14108
14108
|
await wake.wait();
|
|
14109
14109
|
}
|
|
@@ -14131,7 +14131,7 @@ var DispatchingToolExecutor = class _DispatchingToolExecutor {
|
|
|
14131
14131
|
};
|
|
14132
14132
|
for (let i = 0; i < calls.length; i++) {
|
|
14133
14133
|
let call = calls[i];
|
|
14134
|
-
yield*
|
|
14134
|
+
yield* drain2();
|
|
14135
14135
|
if (ctx.signal.aborted) {
|
|
14136
14136
|
sawAbort = true;
|
|
14137
14137
|
fillSyntheticFrom(i);
|
|
@@ -14558,7 +14558,7 @@ var DispatchingToolExecutor = class _DispatchingToolExecutor {
|
|
|
14558
14558
|
}
|
|
14559
14559
|
try {
|
|
14560
14560
|
yield* waitUntil(() => inFlight === 0);
|
|
14561
|
-
yield*
|
|
14561
|
+
yield* drain2();
|
|
14562
14562
|
} finally {
|
|
14563
14563
|
ctx.signal.removeEventListener("abort", onCtxAbort);
|
|
14564
14564
|
}
|
|
@@ -14838,10 +14838,10 @@ async function renameWithRetry(from, to, deps = defaultRenameDeps) {
|
|
|
14838
14838
|
await deps.rename(from, to);
|
|
14839
14839
|
return;
|
|
14840
14840
|
} catch (err) {
|
|
14841
|
-
const
|
|
14841
|
+
const delay2 = RENAME_RETRY_DELAYS_MS[attempt];
|
|
14842
14842
|
const transient = deps.platform === "win32" && isErrnoException(err) && err.code !== void 0 && RENAME_TRANSIENT_CODES.has(err.code);
|
|
14843
|
-
if (!transient ||
|
|
14844
|
-
await deps.sleep(
|
|
14843
|
+
if (!transient || delay2 === void 0) throw err;
|
|
14844
|
+
await deps.sleep(delay2);
|
|
14845
14845
|
}
|
|
14846
14846
|
}
|
|
14847
14847
|
}
|
|
@@ -15110,13 +15110,13 @@ async function externalizeOne(block, sessionDir) {
|
|
|
15110
15110
|
const bytes = Buffer.from(block["data"], "base64");
|
|
15111
15111
|
const hex = createHash6("sha256").update(bytes).digest("hex");
|
|
15112
15112
|
const file = attachmentFilePath(sessionDir, hex);
|
|
15113
|
-
let
|
|
15113
|
+
let exists2 = true;
|
|
15114
15114
|
try {
|
|
15115
15115
|
await access(file);
|
|
15116
15116
|
} catch {
|
|
15117
|
-
|
|
15117
|
+
exists2 = false;
|
|
15118
15118
|
}
|
|
15119
|
-
if (!
|
|
15119
|
+
if (!exists2) {
|
|
15120
15120
|
await mkdir2(attachmentsDirPath(sessionDir), { recursive: true });
|
|
15121
15121
|
await writeFileDurable(file, bytes);
|
|
15122
15122
|
}
|
|
@@ -15416,13 +15416,13 @@ async function effectiveSegments(sessionDir, manifest) {
|
|
|
15416
15416
|
out.push(segment);
|
|
15417
15417
|
continue;
|
|
15418
15418
|
}
|
|
15419
|
-
let
|
|
15419
|
+
let exists2 = true;
|
|
15420
15420
|
try {
|
|
15421
15421
|
await stat2(segmentFilePath(sessionDir, segment.n));
|
|
15422
15422
|
} catch {
|
|
15423
|
-
|
|
15423
|
+
exists2 = false;
|
|
15424
15424
|
}
|
|
15425
|
-
if (isSegmentEffective(segment,
|
|
15425
|
+
if (isSegmentEffective(segment, exists2)) out.push(segment);
|
|
15426
15426
|
}
|
|
15427
15427
|
return out;
|
|
15428
15428
|
}
|
|
@@ -15699,9 +15699,9 @@ async function settleManifest(sessionDir) {
|
|
|
15699
15699
|
segments.push(segment);
|
|
15700
15700
|
continue;
|
|
15701
15701
|
}
|
|
15702
|
-
const
|
|
15702
|
+
const exists2 = await fileExists(segmentFilePath(sessionDir, segment.n));
|
|
15703
15703
|
changed = true;
|
|
15704
|
-
if (isSegmentEffective(segment,
|
|
15704
|
+
if (isSegmentEffective(segment, exists2)) segments.push({ ...segment, status: "sealed" });
|
|
15705
15705
|
}
|
|
15706
15706
|
const last = segments[segments.length - 1];
|
|
15707
15707
|
const liveFirstSeq = last === void 0 ? 0 : last.lastSeq + 1;
|
|
@@ -16343,8 +16343,8 @@ var VERSIONED_MANIFEST = /\/manifest\.g\d{9}-[0-9a-f]+\.json$/;
|
|
|
16343
16343
|
function isManifestObjectKey(key2) {
|
|
16344
16344
|
return key2.endsWith(`/${MANIFEST_OBJECT_NAME}`) || VERSIONED_MANIFEST.test(key2);
|
|
16345
16345
|
}
|
|
16346
|
-
function segmentKey(ref, segment, codec,
|
|
16347
|
-
return `${volumePrefix(ref)}seg-${String(segment.n).padStart(6, "0")}-${segment.firstSeq}-${segment.lastSeq}-${
|
|
16346
|
+
function segmentKey(ref, segment, codec, sha2562) {
|
|
16347
|
+
return `${volumePrefix(ref)}seg-${String(segment.n).padStart(6, "0")}-${segment.firstSeq}-${segment.lastSeq}-${sha2562.slice(0, 12)}.${codecFileExtension(codec)}`;
|
|
16348
16348
|
}
|
|
16349
16349
|
function deriveVolumeId(segments) {
|
|
16350
16350
|
const first = segments.find((s) => s.n === 1);
|
|
@@ -16827,9 +16827,881 @@ function createColdManifestStore(options) {
|
|
|
16827
16827
|
};
|
|
16828
16828
|
}
|
|
16829
16829
|
|
|
16830
|
+
// ../kernel/src/journal/segmented/fs-blob-store.ts
|
|
16831
|
+
import { createHash as createHash9, randomBytes as randomBytes2 } from "node:crypto";
|
|
16832
|
+
import { createReadStream as createReadStream2, createWriteStream as createWriteStream2 } from "node:fs";
|
|
16833
|
+
import { link, mkdir as mkdir4, readdir, readFile as readFile6, rm as rm5, stat as stat3 } from "node:fs/promises";
|
|
16834
|
+
import path8 from "node:path";
|
|
16835
|
+
import { Transform as Transform2 } from "node:stream";
|
|
16836
|
+
import { pipeline as pipeline2 } from "node:stream/promises";
|
|
16837
|
+
var KEY_PART2 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
16838
|
+
function fsKeyParts(key2) {
|
|
16839
|
+
if (key2.length === 0 || key2.includes("\\") || key2.startsWith("/") || key2.endsWith("/")) {
|
|
16840
|
+
throw new StoreError("permanent", `非法对象键 ${JSON.stringify(key2)}:禁空键 / 反斜杠 / 首尾分隔符`);
|
|
16841
|
+
}
|
|
16842
|
+
const parts = key2.split("/");
|
|
16843
|
+
for (const part of parts) {
|
|
16844
|
+
if (!KEY_PART2.test(part) || part === "." || part === "..") {
|
|
16845
|
+
throw new StoreError("permanent", `非法对象键段 ${JSON.stringify(part)}(键 ${JSON.stringify(key2)}):仅允许字母数字与 . _ -,且须以字母数字开头`);
|
|
16846
|
+
}
|
|
16847
|
+
}
|
|
16848
|
+
return parts;
|
|
16849
|
+
}
|
|
16850
|
+
async function exists(p) {
|
|
16851
|
+
try {
|
|
16852
|
+
await stat3(p);
|
|
16853
|
+
return true;
|
|
16854
|
+
} catch {
|
|
16855
|
+
return false;
|
|
16856
|
+
}
|
|
16857
|
+
}
|
|
16858
|
+
var KeyLocks = class {
|
|
16859
|
+
#tails = /* @__PURE__ */ new Map();
|
|
16860
|
+
async run(key2, op) {
|
|
16861
|
+
const prev = this.#tails.get(key2) ?? Promise.resolve();
|
|
16862
|
+
let release;
|
|
16863
|
+
const mine = new Promise((resolve2) => {
|
|
16864
|
+
release = resolve2;
|
|
16865
|
+
});
|
|
16866
|
+
const tail = prev.then(() => mine);
|
|
16867
|
+
this.#tails.set(key2, tail);
|
|
16868
|
+
await prev;
|
|
16869
|
+
try {
|
|
16870
|
+
return await op();
|
|
16871
|
+
} finally {
|
|
16872
|
+
release();
|
|
16873
|
+
if (this.#tails.get(key2) === tail) this.#tails.delete(key2);
|
|
16874
|
+
}
|
|
16875
|
+
}
|
|
16876
|
+
};
|
|
16877
|
+
function createFsBlobStore(options) {
|
|
16878
|
+
const root = path8.resolve(options.dir);
|
|
16879
|
+
const objectsDir = path8.join(root, "objects");
|
|
16880
|
+
const metaDir = path8.join(root, "meta");
|
|
16881
|
+
const tmpDir = path8.join(root, "tmp");
|
|
16882
|
+
const capabilities = {
|
|
16883
|
+
consistency: "strong",
|
|
16884
|
+
conditionalPut: true,
|
|
16885
|
+
rangeGet: true,
|
|
16886
|
+
maxObjectBytes: Number.MAX_SAFE_INTEGER,
|
|
16887
|
+
list: "prefix",
|
|
16888
|
+
batchDelete: 1e3,
|
|
16889
|
+
...options.capabilities
|
|
16890
|
+
};
|
|
16891
|
+
const locks = new KeyLocks();
|
|
16892
|
+
let prepared = null;
|
|
16893
|
+
const prepare = () => {
|
|
16894
|
+
prepared ??= (async () => {
|
|
16895
|
+
await mkdir4(objectsDir, { recursive: true });
|
|
16896
|
+
await mkdir4(metaDir, { recursive: true });
|
|
16897
|
+
await rm5(tmpDir, { recursive: true, force: true }).catch(() => void 0);
|
|
16898
|
+
await mkdir4(tmpDir, { recursive: true });
|
|
16899
|
+
})();
|
|
16900
|
+
return prepared;
|
|
16901
|
+
};
|
|
16902
|
+
const objectPath = (key2) => path8.join(objectsDir, ...fsKeyParts(key2));
|
|
16903
|
+
const metaPath = (key2) => `${path8.join(metaDir, ...fsKeyParts(key2))}.json`;
|
|
16904
|
+
async function readMeta(key2) {
|
|
16905
|
+
let raw;
|
|
16906
|
+
try {
|
|
16907
|
+
raw = await readFile6(metaPath(key2), "utf8");
|
|
16908
|
+
} catch (err) {
|
|
16909
|
+
if (isErrnoException(err) && err.code === "ENOENT") return null;
|
|
16910
|
+
throw toStoreError(err);
|
|
16911
|
+
}
|
|
16912
|
+
try {
|
|
16913
|
+
return JSON.parse(raw);
|
|
16914
|
+
} catch {
|
|
16915
|
+
throw new StoreError("permanent", `对象 meta 损坏:${key2}`);
|
|
16916
|
+
}
|
|
16917
|
+
}
|
|
16918
|
+
async function spool(body, signal) {
|
|
16919
|
+
const tmp = path8.join(tmpDir, `${process.pid.toString(36)}-${randomBytes2(8).toString("hex")}`);
|
|
16920
|
+
const hash = createHash9("sha256");
|
|
16921
|
+
let bytes = 0;
|
|
16922
|
+
const counter = new Transform2({
|
|
16923
|
+
transform(chunk, _enc, callback) {
|
|
16924
|
+
hash.update(chunk);
|
|
16925
|
+
bytes += chunk.length;
|
|
16926
|
+
callback(null, chunk);
|
|
16927
|
+
}
|
|
16928
|
+
});
|
|
16929
|
+
try {
|
|
16930
|
+
await pipeline2(webToNode(body), counter, createWriteStream2(tmp), signal === void 0 ? {} : { signal });
|
|
16931
|
+
} catch (err) {
|
|
16932
|
+
await rm5(tmp, { force: true }).catch(() => void 0);
|
|
16933
|
+
if (signal?.aborted === true || err instanceof Error && err.name === "AbortError") {
|
|
16934
|
+
throw new StoreError("transient", "aborted", { cause: err });
|
|
16935
|
+
}
|
|
16936
|
+
throw toStoreError(err);
|
|
16937
|
+
}
|
|
16938
|
+
return { tmp, sha256: hash.digest("hex"), bytes };
|
|
16939
|
+
}
|
|
16940
|
+
async function writeMeta(key2, meta) {
|
|
16941
|
+
const p = metaPath(key2);
|
|
16942
|
+
await mkdir4(path8.dirname(p), { recursive: true });
|
|
16943
|
+
await writeFileDurable(p, `${JSON.stringify(meta)}
|
|
16944
|
+
`);
|
|
16945
|
+
}
|
|
16946
|
+
async function placeExclusive(tmp, target) {
|
|
16947
|
+
try {
|
|
16948
|
+
await link(tmp, target);
|
|
16949
|
+
await rm5(tmp, { force: true }).catch(() => void 0);
|
|
16950
|
+
return "created";
|
|
16951
|
+
} catch (err) {
|
|
16952
|
+
if (isErrnoException(err)) {
|
|
16953
|
+
if (err.code === "EEXIST") return "exists";
|
|
16954
|
+
if (err.code === "EPERM" || err.code === "ENOSYS" || err.code === "ENOTSUP" || err.code === "EXDEV") {
|
|
16955
|
+
if (await exists(target)) return "exists";
|
|
16956
|
+
await renameWithRetry(tmp, target);
|
|
16957
|
+
return "created";
|
|
16958
|
+
}
|
|
16959
|
+
}
|
|
16960
|
+
throw err;
|
|
16961
|
+
}
|
|
16962
|
+
}
|
|
16963
|
+
const store = {
|
|
16964
|
+
capabilities,
|
|
16965
|
+
async put(key2, body, meta, opts = {}) {
|
|
16966
|
+
const target = objectPath(key2);
|
|
16967
|
+
await prepare();
|
|
16968
|
+
if (opts.signal?.aborted === true) {
|
|
16969
|
+
await body.cancel().catch(() => void 0);
|
|
16970
|
+
throw new StoreError("transient", "aborted", { cause: opts.signal.reason });
|
|
16971
|
+
}
|
|
16972
|
+
const spooled = await spool(body, opts.signal);
|
|
16973
|
+
try {
|
|
16974
|
+
if (spooled.sha256 !== meta.sha256) {
|
|
16975
|
+
throw new StoreError("permanent", `sha256 不符:体 ${spooled.sha256} ≠ meta ${meta.sha256}(${key2})`);
|
|
16976
|
+
}
|
|
16977
|
+
const etag = spooled.sha256;
|
|
16978
|
+
const stored = { ...meta, bytes: spooled.bytes, etag };
|
|
16979
|
+
return await locks.run(key2, async () => {
|
|
16980
|
+
await mkdir4(path8.dirname(target), { recursive: true });
|
|
16981
|
+
if (opts.ifNoneMatch === "*") {
|
|
16982
|
+
const outcome = await placeExclusive(spooled.tmp, target);
|
|
16983
|
+
if (outcome === "exists") {
|
|
16984
|
+
const current = await readMeta(key2);
|
|
16985
|
+
if (current === null || current.etag !== etag) {
|
|
16986
|
+
throw new StoreError("precondition_failed", `ifNoneMatch:* 但对象已存在:${key2}`);
|
|
16987
|
+
}
|
|
16988
|
+
return { etag };
|
|
16989
|
+
}
|
|
16990
|
+
await writeMeta(key2, stored);
|
|
16991
|
+
return { etag };
|
|
16992
|
+
}
|
|
16993
|
+
if (opts.ifMatch !== void 0) {
|
|
16994
|
+
const current = await readMeta(key2);
|
|
16995
|
+
if (current === null || current.etag !== opts.ifMatch) {
|
|
16996
|
+
throw new StoreError("precondition_failed", `ifMatch ${opts.ifMatch} 但现行 etag ${current?.etag ?? "(absent)"}:${key2}`);
|
|
16997
|
+
}
|
|
16998
|
+
}
|
|
16999
|
+
await renameWithRetry(spooled.tmp, target);
|
|
17000
|
+
await writeMeta(key2, stored);
|
|
17001
|
+
return { etag };
|
|
17002
|
+
});
|
|
17003
|
+
} catch (err) {
|
|
17004
|
+
throw toStoreError(err);
|
|
17005
|
+
} finally {
|
|
17006
|
+
await rm5(spooled.tmp, { force: true }).catch(() => void 0);
|
|
17007
|
+
}
|
|
17008
|
+
},
|
|
17009
|
+
async get(key2, range, signal) {
|
|
17010
|
+
const target = objectPath(key2);
|
|
17011
|
+
await prepare();
|
|
17012
|
+
if (signal?.aborted === true) throw new StoreError("transient", "aborted", { cause: signal.reason });
|
|
17013
|
+
const meta = await readMeta(key2);
|
|
17014
|
+
if (meta === null || !await exists(target)) throw new StoreError("not_found", `对象不存在:${key2}`);
|
|
17015
|
+
const streamOptions = { highWaterMark: 256 * 1024 };
|
|
17016
|
+
if (range !== void 0) {
|
|
17017
|
+
if (range.start >= meta.bytes) return new ReadableStream({ start: (c) => c.close() });
|
|
17018
|
+
streamOptions.start = range.start;
|
|
17019
|
+
if (range.end !== void 0) streamOptions.end = Math.min(range.end, meta.bytes - 1);
|
|
17020
|
+
}
|
|
17021
|
+
const source = createReadStream2(target, streamOptions);
|
|
17022
|
+
if (signal !== void 0) {
|
|
17023
|
+
const abort = () => {
|
|
17024
|
+
source.destroy(new StoreError("transient", "aborted", { cause: signal.reason }));
|
|
17025
|
+
};
|
|
17026
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
17027
|
+
source.once("close", () => signal.removeEventListener("abort", abort));
|
|
17028
|
+
}
|
|
17029
|
+
return nodeToWeb(source);
|
|
17030
|
+
},
|
|
17031
|
+
async head(key2) {
|
|
17032
|
+
await prepare();
|
|
17033
|
+
const meta = await readMeta(key2);
|
|
17034
|
+
if (meta === null || !await exists(objectPath(key2))) return null;
|
|
17035
|
+
return { ...meta };
|
|
17036
|
+
},
|
|
17037
|
+
async *list(prefix, cursor, signal) {
|
|
17038
|
+
await prepare();
|
|
17039
|
+
const slash = prefix.lastIndexOf("/");
|
|
17040
|
+
const dirPart = slash === -1 ? "" : prefix.slice(0, slash);
|
|
17041
|
+
const dirParts = dirPart === "" ? [] : fsKeyParts(dirPart);
|
|
17042
|
+
const startDir = path8.join(objectsDir, ...dirParts);
|
|
17043
|
+
if (!await exists(startDir)) return;
|
|
17044
|
+
const keys = [];
|
|
17045
|
+
const walk = async (dir, keyPrefix) => {
|
|
17046
|
+
let entries;
|
|
17047
|
+
try {
|
|
17048
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
17049
|
+
} catch (err) {
|
|
17050
|
+
if (isErrnoException(err) && err.code === "ENOENT") return;
|
|
17051
|
+
throw toStoreError(err);
|
|
17052
|
+
}
|
|
17053
|
+
for (const entry of entries) {
|
|
17054
|
+
const key2 = keyPrefix === "" ? entry.name : `${keyPrefix}/${entry.name}`;
|
|
17055
|
+
if (entry.isDirectory()) await walk(path8.join(dir, entry.name), key2);
|
|
17056
|
+
else if (entry.isFile() && key2.startsWith(prefix)) keys.push(key2);
|
|
17057
|
+
}
|
|
17058
|
+
};
|
|
17059
|
+
await walk(startDir, dirPart);
|
|
17060
|
+
keys.sort();
|
|
17061
|
+
for (const key2 of keys) {
|
|
17062
|
+
if (signal?.aborted === true) throw new StoreError("transient", "aborted", { cause: signal.reason });
|
|
17063
|
+
if (cursor !== void 0 && key2 <= cursor) continue;
|
|
17064
|
+
const meta = await readMeta(key2);
|
|
17065
|
+
if (meta === null) continue;
|
|
17066
|
+
yield { key: key2, meta: { ...meta } };
|
|
17067
|
+
}
|
|
17068
|
+
},
|
|
17069
|
+
async delete(keys) {
|
|
17070
|
+
await prepare();
|
|
17071
|
+
for (const key2 of keys) {
|
|
17072
|
+
await rm5(objectPath(key2), { force: true }).catch((err) => {
|
|
17073
|
+
throw toStoreError(err);
|
|
17074
|
+
});
|
|
17075
|
+
await rm5(metaPath(key2), { force: true }).catch((err) => {
|
|
17076
|
+
throw toStoreError(err);
|
|
17077
|
+
});
|
|
17078
|
+
}
|
|
17079
|
+
}
|
|
17080
|
+
};
|
|
17081
|
+
return store;
|
|
17082
|
+
}
|
|
17083
|
+
|
|
16830
17084
|
// ../kernel/src/journal/segmented/conformance.ts
|
|
17085
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
16831
17086
|
var CHUNK = 256 * 1024;
|
|
16832
17087
|
var MIB = 1024 * 1024;
|
|
17088
|
+
function bytesOf(size, seed) {
|
|
17089
|
+
const out = new Uint8Array(size);
|
|
17090
|
+
let x = seed * 2654435761 + 1 >>> 0;
|
|
17091
|
+
for (let i = 0; i < size; i++) {
|
|
17092
|
+
x = x * 1664525 + 1013904223 >>> 0;
|
|
17093
|
+
out[i] = x >>> 24;
|
|
17094
|
+
}
|
|
17095
|
+
return out;
|
|
17096
|
+
}
|
|
17097
|
+
function sha256(bytes) {
|
|
17098
|
+
return createHash10("sha256").update(bytes).digest("hex");
|
|
17099
|
+
}
|
|
17100
|
+
function toStream(bytes, chunk = CHUNK) {
|
|
17101
|
+
let offset = 0;
|
|
17102
|
+
return new ReadableStream({
|
|
17103
|
+
pull(controller) {
|
|
17104
|
+
if (offset >= bytes.byteLength) {
|
|
17105
|
+
controller.close();
|
|
17106
|
+
return;
|
|
17107
|
+
}
|
|
17108
|
+
const end = Math.min(bytes.byteLength, offset + chunk);
|
|
17109
|
+
controller.enqueue(bytes.subarray(offset, end));
|
|
17110
|
+
offset = end;
|
|
17111
|
+
}
|
|
17112
|
+
});
|
|
17113
|
+
}
|
|
17114
|
+
async function collect(body) {
|
|
17115
|
+
const chunks = [];
|
|
17116
|
+
let total = 0;
|
|
17117
|
+
const reader = body.getReader();
|
|
17118
|
+
try {
|
|
17119
|
+
for (; ; ) {
|
|
17120
|
+
const { done, value } = await reader.read();
|
|
17121
|
+
if (done) break;
|
|
17122
|
+
chunks.push(value);
|
|
17123
|
+
total += value.byteLength;
|
|
17124
|
+
}
|
|
17125
|
+
} finally {
|
|
17126
|
+
reader.releaseLock();
|
|
17127
|
+
}
|
|
17128
|
+
const out = new Uint8Array(total);
|
|
17129
|
+
let o = 0;
|
|
17130
|
+
for (const c of chunks) {
|
|
17131
|
+
out.set(c, o);
|
|
17132
|
+
o += c.byteLength;
|
|
17133
|
+
}
|
|
17134
|
+
return out;
|
|
17135
|
+
}
|
|
17136
|
+
async function drain(body) {
|
|
17137
|
+
let total = 0;
|
|
17138
|
+
const reader = body.getReader();
|
|
17139
|
+
try {
|
|
17140
|
+
for (; ; ) {
|
|
17141
|
+
const { done, value } = await reader.read();
|
|
17142
|
+
if (done) break;
|
|
17143
|
+
total += value.byteLength;
|
|
17144
|
+
}
|
|
17145
|
+
} finally {
|
|
17146
|
+
reader.releaseLock();
|
|
17147
|
+
}
|
|
17148
|
+
return total;
|
|
17149
|
+
}
|
|
17150
|
+
function equalBytes(a, b) {
|
|
17151
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
17152
|
+
for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
|
|
17153
|
+
return true;
|
|
17154
|
+
}
|
|
17155
|
+
function metaFor(bytes, contentType = "application/octet-stream") {
|
|
17156
|
+
return { bytes: bytes.byteLength, contentType, sha256: sha256(bytes) };
|
|
17157
|
+
}
|
|
17158
|
+
function largeStream(total, seed) {
|
|
17159
|
+
let produced = 0;
|
|
17160
|
+
let x = seed * 2654435761 + 1 >>> 0;
|
|
17161
|
+
return new ReadableStream({
|
|
17162
|
+
pull(controller) {
|
|
17163
|
+
if (produced >= total) {
|
|
17164
|
+
controller.close();
|
|
17165
|
+
return;
|
|
17166
|
+
}
|
|
17167
|
+
const size = Math.min(CHUNK, total - produced);
|
|
17168
|
+
const chunk = new Uint8Array(size);
|
|
17169
|
+
for (let i = 0; i < size; i++) {
|
|
17170
|
+
x = x * 1664525 + 1013904223 >>> 0;
|
|
17171
|
+
chunk[i] = x >>> 24;
|
|
17172
|
+
}
|
|
17173
|
+
produced += size;
|
|
17174
|
+
controller.enqueue(chunk);
|
|
17175
|
+
}
|
|
17176
|
+
});
|
|
17177
|
+
}
|
|
17178
|
+
async function largeSha256(total, seed) {
|
|
17179
|
+
const hash = createHash10("sha256");
|
|
17180
|
+
const reader = largeStream(total, seed).getReader();
|
|
17181
|
+
for (; ; ) {
|
|
17182
|
+
const { done, value } = await reader.read();
|
|
17183
|
+
if (done) break;
|
|
17184
|
+
hash.update(value);
|
|
17185
|
+
}
|
|
17186
|
+
return hash.digest("hex");
|
|
17187
|
+
}
|
|
17188
|
+
function sampleRss() {
|
|
17189
|
+
const base = process.memoryUsage().rss;
|
|
17190
|
+
let peak = base;
|
|
17191
|
+
const timer = setInterval(() => {
|
|
17192
|
+
peak = Math.max(peak, process.memoryUsage().rss);
|
|
17193
|
+
}, 20);
|
|
17194
|
+
return {
|
|
17195
|
+
stop() {
|
|
17196
|
+
clearInterval(timer);
|
|
17197
|
+
return Math.max(peak, process.memoryUsage().rss) - base;
|
|
17198
|
+
}
|
|
17199
|
+
};
|
|
17200
|
+
}
|
|
17201
|
+
var fmtMiB = (n) => `${(n / MIB).toFixed(1)} MiB`;
|
|
17202
|
+
var CaseFailure = class extends Error {
|
|
17203
|
+
};
|
|
17204
|
+
var fail = (detail) => {
|
|
17205
|
+
throw new CaseFailure(detail);
|
|
17206
|
+
};
|
|
17207
|
+
var ensure = (cond, detail) => {
|
|
17208
|
+
if (!cond) fail(detail);
|
|
17209
|
+
};
|
|
17210
|
+
var isStoreErrorOf = (err, kind) => err instanceof StoreError && err.kind === kind;
|
|
17211
|
+
async function runStorageConformance(store, options = {}) {
|
|
17212
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17213
|
+
const t0 = performance.now();
|
|
17214
|
+
const caps = store.capabilities;
|
|
17215
|
+
const largeBytes = options.largeObjectBytes ?? 16 * MIB;
|
|
17216
|
+
const rssBudget = options.rssBudgetBytes ?? 64 * MIB;
|
|
17217
|
+
const eventualMs = options.eventualConsistencyMs ?? 0;
|
|
17218
|
+
const prefix = options.keyPrefix ?? `conformance-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
17219
|
+
const concurrency = Math.max(2, options.concurrency ?? 16);
|
|
17220
|
+
const created = /* @__PURE__ */ new Set();
|
|
17221
|
+
const key2 = (name) => {
|
|
17222
|
+
const k = `${prefix}/${name}`;
|
|
17223
|
+
created.add(k);
|
|
17224
|
+
return k;
|
|
17225
|
+
};
|
|
17226
|
+
async function waitVisible(listPrefix, expected) {
|
|
17227
|
+
const deadline = performance.now() + eventualMs;
|
|
17228
|
+
for (; ; ) {
|
|
17229
|
+
for await (const entry of store.list(listPrefix, void 0, options.signal)) {
|
|
17230
|
+
if (entry.key === expected) return true;
|
|
17231
|
+
}
|
|
17232
|
+
if (performance.now() >= deadline) return false;
|
|
17233
|
+
await new Promise((r) => setTimeout(r, Math.min(50, Math.max(5, eventualMs / 20))));
|
|
17234
|
+
}
|
|
17235
|
+
}
|
|
17236
|
+
const cases = [];
|
|
17237
|
+
async function run(id, fn) {
|
|
17238
|
+
const start = performance.now();
|
|
17239
|
+
try {
|
|
17240
|
+
const detail = await fn();
|
|
17241
|
+
const skipped = detail.startsWith("skipped:");
|
|
17242
|
+
cases.push({ id, passed: true, detail, ...skipped ? { skipped: true } : {}, durationMs: Math.round(performance.now() - start) });
|
|
17243
|
+
} catch (err) {
|
|
17244
|
+
const detail = err instanceof CaseFailure ? err.message : `异常:${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`;
|
|
17245
|
+
cases.push({ id, passed: false, detail, durationMs: Math.round(performance.now() - start) });
|
|
17246
|
+
}
|
|
17247
|
+
}
|
|
17248
|
+
await run("idempotent_put", async () => {
|
|
17249
|
+
const k = key2("idempotent");
|
|
17250
|
+
const bytes = bytesOf(7e4, 1);
|
|
17251
|
+
const meta = metaFor(bytes);
|
|
17252
|
+
const r1 = await store.put(k, toStream(bytes), meta);
|
|
17253
|
+
const r2 = await store.put(k, toStream(bytes), meta);
|
|
17254
|
+
ensure(r1.etag === void 0 || r2.etag === void 0 || r1.etag === r2.etag, `同内容重复 put 的 etag 不同:${r1.etag} / ${r2.etag}`);
|
|
17255
|
+
const head = await store.head(k);
|
|
17256
|
+
ensure(head !== null, "put 后 head 为 null");
|
|
17257
|
+
ensure(head.bytes === bytes.byteLength, `head.bytes ${head.bytes} ≠ ${bytes.byteLength}`);
|
|
17258
|
+
ensure(head.sha256 === meta.sha256, "head.sha256 与 meta 不符");
|
|
17259
|
+
ensure(head.contentType === meta.contentType, "head.contentType 与 meta 不符");
|
|
17260
|
+
const back = await collect(await store.get(k));
|
|
17261
|
+
ensure(equalBytes(back, bytes), "get 回读与 put 体不同");
|
|
17262
|
+
return `2 次 put 成功,head/get 一致(${bytes.byteLength} B)`;
|
|
17263
|
+
});
|
|
17264
|
+
await run("range_get", async () => {
|
|
17265
|
+
if (!caps.rangeGet) return "skipped: capabilities.rangeGet:false";
|
|
17266
|
+
const k = key2("range");
|
|
17267
|
+
const bytes = bytesOf(1e5, 2);
|
|
17268
|
+
await store.put(k, toStream(bytes), metaFor(bytes));
|
|
17269
|
+
const mid = await collect(await store.get(k, { start: 10, end: 19 }));
|
|
17270
|
+
ensure(equalBytes(mid, bytes.subarray(10, 20)), `Range [10,19] 回读 ${mid.byteLength} B 与切片不同`);
|
|
17271
|
+
const tail = await collect(await store.get(k, { start: bytes.byteLength - 5 }));
|
|
17272
|
+
ensure(equalBytes(tail, bytes.subarray(bytes.byteLength - 5)), "Range 缺省 end 到尾不符");
|
|
17273
|
+
const clamped = await collect(await store.get(k, { start: bytes.byteLength - 3, end: bytes.byteLength + 1e3 }));
|
|
17274
|
+
ensure(equalBytes(clamped, bytes.subarray(bytes.byteLength - 3)), "Range end 越界未钳到尾");
|
|
17275
|
+
const first = await collect(await store.get(k, { start: 0, end: 0 }));
|
|
17276
|
+
ensure(first.byteLength === 1 && first[0] === bytes[0], "Range [0,0] 应回 1 字节");
|
|
17277
|
+
return "中段 / 到尾 / 越界钳 / 单字节 Range 皆正确";
|
|
17278
|
+
});
|
|
17279
|
+
await run("concurrent_put", async () => {
|
|
17280
|
+
const items = Array.from({ length: concurrency }, (_, i) => ({ k: key2(`concurrent/${String(i).padStart(3, "0")}`), bytes: bytesOf(2e4 + i * 7, 100 + i) }));
|
|
17281
|
+
await Promise.all(items.map((it) => store.put(it.k, toStream(it.bytes), metaFor(it.bytes))));
|
|
17282
|
+
for (const it of items) {
|
|
17283
|
+
const head = await store.head(it.k);
|
|
17284
|
+
ensure(head !== null && head.sha256 === sha256(it.bytes), `并发 put 后 ${it.k} head 缺失或哈希不符`);
|
|
17285
|
+
ensure(equalBytes(await collect(await store.get(it.k)), it.bytes), `并发 put 后 ${it.k} 内容不符`);
|
|
17286
|
+
}
|
|
17287
|
+
return `${concurrency} 键并发 put 全部一致`;
|
|
17288
|
+
});
|
|
17289
|
+
await run("partial_put_invisible", async () => {
|
|
17290
|
+
const k = key2("partial");
|
|
17291
|
+
const bytes = bytesOf(3e5, 4);
|
|
17292
|
+
const meta = metaFor(bytes);
|
|
17293
|
+
let sent = 0;
|
|
17294
|
+
const broken = new ReadableStream({
|
|
17295
|
+
pull(controller) {
|
|
17296
|
+
if (sent >= 2) {
|
|
17297
|
+
controller.error(new Error("conformance: connection dropped"));
|
|
17298
|
+
return;
|
|
17299
|
+
}
|
|
17300
|
+
controller.enqueue(bytes.subarray(sent * CHUNK, Math.min(bytes.byteLength, (sent + 1) * CHUNK)));
|
|
17301
|
+
sent++;
|
|
17302
|
+
}
|
|
17303
|
+
});
|
|
17304
|
+
let threw = false;
|
|
17305
|
+
try {
|
|
17306
|
+
await store.put(k, broken, meta);
|
|
17307
|
+
} catch {
|
|
17308
|
+
threw = true;
|
|
17309
|
+
}
|
|
17310
|
+
ensure(threw, "体流中断的 put 未失败");
|
|
17311
|
+
ensure(await store.head(k) === null, "体流中断后留下半对象(head 非 null)");
|
|
17312
|
+
let threw2 = null;
|
|
17313
|
+
try {
|
|
17314
|
+
await store.put(k, toStream(bytes), { ...meta, sha256: "f".repeat(64) });
|
|
17315
|
+
} catch (err) {
|
|
17316
|
+
threw2 = err;
|
|
17317
|
+
}
|
|
17318
|
+
ensure(threw2 !== null, "sha256 不符的 put 未失败");
|
|
17319
|
+
ensure(await store.head(k) === null, "sha256 不符后留下对象");
|
|
17320
|
+
return `体流中断 / sha256 不符均失败且无半对象${isStoreErrorOf(threw2, "permanent") ? "(sha256 不符归 permanent)" : ""}`;
|
|
17321
|
+
});
|
|
17322
|
+
await run("list_prefix_cursor", async () => {
|
|
17323
|
+
if (caps.list === "none") return "skipped: capabilities.list:'none'";
|
|
17324
|
+
const names = ["a-000", "a-001", "a-002", "b-000", "b-001"];
|
|
17325
|
+
const keys = names.map((n) => key2(`listing/${n}`));
|
|
17326
|
+
for (const [i, k] of keys.entries()) {
|
|
17327
|
+
const bytes = bytesOf(1e3 + i, 200 + i);
|
|
17328
|
+
await store.put(k, toStream(bytes), metaFor(bytes));
|
|
17329
|
+
}
|
|
17330
|
+
const listPrefix = `${prefix}/listing/`;
|
|
17331
|
+
for (const k of keys) ensure(await waitVisible(listPrefix, k), `${k} 在 ${eventualMs} ms 内未经 list 可见`);
|
|
17332
|
+
const all = [];
|
|
17333
|
+
for await (const e of store.list(listPrefix)) {
|
|
17334
|
+
all.push(e.key);
|
|
17335
|
+
ensure(e.meta.sha256.length === 64, `list 条目 meta.sha256 形状不对:${e.key}`);
|
|
17336
|
+
}
|
|
17337
|
+
ensure(all.length === keys.length, `list 返回 ${all.length} 项 ≠ ${keys.length}`);
|
|
17338
|
+
for (let i = 1; i < all.length; i++) ensure(all[i - 1] < all[i], "list 非字典升序");
|
|
17339
|
+
const sub = [];
|
|
17340
|
+
for await (const e of store.list(`${prefix}/listing/a-`)) sub.push(e.key);
|
|
17341
|
+
ensure(sub.length === 3 && sub.every((k) => k.includes("/a-")), `名前缀 list 应 3 项,得 ${sub.length}`);
|
|
17342
|
+
const rest = [];
|
|
17343
|
+
for await (const e of store.list(listPrefix, all[1])) rest.push(e.key);
|
|
17344
|
+
ensure(rest.length === all.length - 2 && rest[0] === all[2], `cursor 分页不正确:${rest.join(",")}`);
|
|
17345
|
+
const none = [];
|
|
17346
|
+
for await (const e of store.list(`${prefix}/nothing-here/`)) none.push(e.key);
|
|
17347
|
+
ensure(none.length === 0, "空前缀 list 应无项");
|
|
17348
|
+
return `${keys.length} 键前缀 / 名前缀 / cursor 分页 / 空前缀正确(可见窗口 ${eventualMs} ms)`;
|
|
17349
|
+
});
|
|
17350
|
+
await run("large_object_streaming", async () => {
|
|
17351
|
+
const k = key2("large");
|
|
17352
|
+
const digest = await largeSha256(largeBytes, 6);
|
|
17353
|
+
const meta = { bytes: largeBytes, contentType: "application/octet-stream", sha256: digest };
|
|
17354
|
+
const putSampler = sampleRss();
|
|
17355
|
+
await store.put(k, largeStream(largeBytes, 6), meta);
|
|
17356
|
+
const putDelta = putSampler.stop();
|
|
17357
|
+
const head = await store.head(k);
|
|
17358
|
+
ensure(head !== null && head.bytes === largeBytes, "large head 缺失或字节数不符");
|
|
17359
|
+
const getSampler = sampleRss();
|
|
17360
|
+
const drained = await drain(await store.get(k));
|
|
17361
|
+
const getDelta = getSampler.stop();
|
|
17362
|
+
ensure(drained === largeBytes, `get 回读 ${drained} B ≠ ${largeBytes}`);
|
|
17363
|
+
ensure(putDelta < rssBudget, `put RSS 峰值增量 ${fmtMiB(putDelta)} ≥ 预算 ${fmtMiB(rssBudget)}`);
|
|
17364
|
+
ensure(getDelta < rssBudget, `get RSS 峰值增量 ${fmtMiB(getDelta)} ≥ 预算 ${fmtMiB(rssBudget)}`);
|
|
17365
|
+
return `${fmtMiB(largeBytes)}:put RSS Δ ${fmtMiB(putDelta)} / get RSS Δ ${fmtMiB(getDelta)} < ${fmtMiB(rssBudget)}`;
|
|
17366
|
+
});
|
|
17367
|
+
await run("abort_signal", async () => {
|
|
17368
|
+
const k = key2("abort");
|
|
17369
|
+
const bytes = bytesOf(4e5, 7);
|
|
17370
|
+
const meta = metaFor(bytes);
|
|
17371
|
+
const pre = new AbortController();
|
|
17372
|
+
pre.abort(new Error("conformance: pre-aborted"));
|
|
17373
|
+
let threw = false;
|
|
17374
|
+
try {
|
|
17375
|
+
await store.put(k, toStream(bytes), meta, { signal: pre.signal });
|
|
17376
|
+
} catch {
|
|
17377
|
+
threw = true;
|
|
17378
|
+
}
|
|
17379
|
+
ensure(threw, "预中止 signal 的 put 未失败");
|
|
17380
|
+
ensure(await store.head(k) === null, "预中止 put 留下对象");
|
|
17381
|
+
const mid = new AbortController();
|
|
17382
|
+
let pulls = 0;
|
|
17383
|
+
const midBody = new ReadableStream({
|
|
17384
|
+
pull(controller) {
|
|
17385
|
+
if (pulls === 1) mid.abort(new Error("conformance: mid abort"));
|
|
17386
|
+
if (pulls * CHUNK >= bytes.byteLength) {
|
|
17387
|
+
controller.close();
|
|
17388
|
+
return;
|
|
17389
|
+
}
|
|
17390
|
+
controller.enqueue(bytes.subarray(pulls * CHUNK, Math.min(bytes.byteLength, (pulls + 1) * CHUNK)));
|
|
17391
|
+
pulls++;
|
|
17392
|
+
}
|
|
17393
|
+
});
|
|
17394
|
+
let threwMid = false;
|
|
17395
|
+
try {
|
|
17396
|
+
await store.put(k, midBody, meta, { signal: mid.signal });
|
|
17397
|
+
} catch {
|
|
17398
|
+
threwMid = true;
|
|
17399
|
+
}
|
|
17400
|
+
ensure(threwMid, "中途中止的 put 未失败");
|
|
17401
|
+
ensure(await store.head(k) === null, "中途中止 put 留下半对象");
|
|
17402
|
+
await store.put(k, toStream(bytes), meta);
|
|
17403
|
+
const getAbort = new AbortController();
|
|
17404
|
+
getAbort.abort(new Error("conformance: get aborted"));
|
|
17405
|
+
let getThrew = false;
|
|
17406
|
+
try {
|
|
17407
|
+
const body = await store.get(k, void 0, getAbort.signal);
|
|
17408
|
+
await drain(body);
|
|
17409
|
+
} catch {
|
|
17410
|
+
getThrew = true;
|
|
17411
|
+
}
|
|
17412
|
+
ensure(getThrew, "预中止 signal 的 get 未失败(既未拒绝也未以流出错)");
|
|
17413
|
+
return "预中止 / 中途中止 put 干净失败无半对象;预中止 get 失败";
|
|
17414
|
+
});
|
|
17415
|
+
await run("manifest_cas", async () => {
|
|
17416
|
+
if (!caps.conditionalPut) return "skipped: capabilities.conditionalPut:false(须挂 SessionIndexStore)";
|
|
17417
|
+
const k = key2("manifest.json");
|
|
17418
|
+
const v1 = new TextEncoder().encode('{"generation":1}\n');
|
|
17419
|
+
const v2 = new TextEncoder().encode('{"generation":2}\n');
|
|
17420
|
+
const v3 = new TextEncoder().encode('{"generation":3}\n');
|
|
17421
|
+
const r1 = await store.put(k, toStream(v1), metaFor(v1, "application/json"), { ifNoneMatch: "*" });
|
|
17422
|
+
ensure(r1.etag !== void 0, "conditionalPut:true 的 put 须返回 etag");
|
|
17423
|
+
let conflict = null;
|
|
17424
|
+
try {
|
|
17425
|
+
await store.put(k, toStream(v2), metaFor(v2, "application/json"), { ifNoneMatch: "*" });
|
|
17426
|
+
} catch (err) {
|
|
17427
|
+
conflict = err;
|
|
17428
|
+
}
|
|
17429
|
+
ensure(isStoreErrorOf(conflict, "precondition_failed"), `ifNoneMatch:* 对已存在键应 precondition_failed,实为 ${String(conflict)}`);
|
|
17430
|
+
ensure(equalBytes(await collect(await store.get(k)), v1), "ifNoneMatch 冲突后内容被改");
|
|
17431
|
+
const head1 = await store.head(k);
|
|
17432
|
+
ensure(head1 !== null && head1.etag === r1.etag, `head.etag(${head1?.etag})应等于 put 返回 etag(${r1.etag})`);
|
|
17433
|
+
const r2 = await store.put(k, toStream(v2), metaFor(v2, "application/json"), { ifMatch: r1.etag });
|
|
17434
|
+
ensure(r2.etag !== void 0 && r2.etag !== r1.etag, "ifMatch 改写后 etag 应变化");
|
|
17435
|
+
let stale = null;
|
|
17436
|
+
try {
|
|
17437
|
+
await store.put(k, toStream(v3), metaFor(v3, "application/json"), { ifMatch: r1.etag });
|
|
17438
|
+
} catch (err) {
|
|
17439
|
+
stale = err;
|
|
17440
|
+
}
|
|
17441
|
+
ensure(isStoreErrorOf(stale, "precondition_failed"), `过期 etag 的 ifMatch 应 precondition_failed,实为 ${String(stale)}`);
|
|
17442
|
+
ensure(equalBytes(await collect(await store.get(k)), v2), "过期 ifMatch 冲突后内容被改");
|
|
17443
|
+
let missing = null;
|
|
17444
|
+
try {
|
|
17445
|
+
await store.put(key2("manifest-missing.json"), toStream(v1), metaFor(v1, "application/json"), { ifMatch: "nope" });
|
|
17446
|
+
} catch (err) {
|
|
17447
|
+
missing = err;
|
|
17448
|
+
}
|
|
17449
|
+
ensure(isStoreErrorOf(missing, "precondition_failed"), "ifMatch 对不存在键应 precondition_failed");
|
|
17450
|
+
return "ifNoneMatch:* 首写 / 冲突 412;ifMatch etag 改写 / 过期 412 / 缺席 412;head 回带 etag";
|
|
17451
|
+
});
|
|
17452
|
+
await run("delete_idempotent", async () => {
|
|
17453
|
+
const k = key2("delete");
|
|
17454
|
+
const bytes = bytesOf(1234, 9);
|
|
17455
|
+
await store.put(k, toStream(bytes), metaFor(bytes));
|
|
17456
|
+
await store.delete([k]);
|
|
17457
|
+
ensure(await store.head(k) === null, "delete 后 head 仍非 null");
|
|
17458
|
+
let notFound = null;
|
|
17459
|
+
try {
|
|
17460
|
+
await drain(await store.get(k));
|
|
17461
|
+
} catch (err) {
|
|
17462
|
+
notFound = err;
|
|
17463
|
+
}
|
|
17464
|
+
ensure(notFound !== null, "delete 后 get 未失败");
|
|
17465
|
+
await store.delete([k]);
|
|
17466
|
+
await store.delete([key2("never-existed")]);
|
|
17467
|
+
const batch = Math.max(1, Math.min(caps.batchDelete ?? 1, 8));
|
|
17468
|
+
const many = Array.from({ length: batch }, (_, i) => key2(`delete-batch/${i}`));
|
|
17469
|
+
for (const [i, mk] of many.entries()) {
|
|
17470
|
+
const b = bytesOf(100 + i, 300 + i);
|
|
17471
|
+
await store.put(mk, toStream(b), metaFor(b));
|
|
17472
|
+
}
|
|
17473
|
+
await store.delete(many);
|
|
17474
|
+
for (const mk of many) ensure(await store.head(mk) === null, `批删后 ${mk} 仍在`);
|
|
17475
|
+
return `在场 / 重复 / 不存在 / 批量 ${batch} 键删除皆幂等`;
|
|
17476
|
+
});
|
|
17477
|
+
await run("capabilities_truthful", async () => {
|
|
17478
|
+
const notes = [];
|
|
17479
|
+
const missing = key2("missing");
|
|
17480
|
+
ensure(await store.head(missing) === null, "head 不存在键应返 null 而非抛");
|
|
17481
|
+
let getErr = null;
|
|
17482
|
+
try {
|
|
17483
|
+
await drain(await store.get(missing));
|
|
17484
|
+
} catch (err) {
|
|
17485
|
+
getErr = err;
|
|
17486
|
+
}
|
|
17487
|
+
ensure(getErr !== null, "get 不存在键应失败");
|
|
17488
|
+
notes.push(isStoreErrorOf(getErr, "not_found") ? "get 缺席 → not_found" : `get 缺席 → ${toStoreError(getErr).kind}(建议 not_found)`);
|
|
17489
|
+
ensure(caps.consistency === "strong" || caps.consistency === "eventual", `consistency 值非法:${String(caps.consistency)}`);
|
|
17490
|
+
ensure(Number.isFinite(caps.maxObjectBytes) && caps.maxObjectBytes > 0, "maxObjectBytes 须为正有限数");
|
|
17491
|
+
ensure(caps.list === "prefix" || caps.list === "none", `list 值非法:${String(caps.list)}`);
|
|
17492
|
+
ensure(caps.batchDelete === void 0 || Number.isInteger(caps.batchDelete) && caps.batchDelete >= 1, "batchDelete 须为 ≥1 整数");
|
|
17493
|
+
const bytes = bytesOf(5e4, 10);
|
|
17494
|
+
const k = key2("truthful");
|
|
17495
|
+
const r = await store.put(k, toStream(bytes), metaFor(bytes));
|
|
17496
|
+
if (caps.rangeGet) {
|
|
17497
|
+
const part = await collect(await store.get(k, { start: 100, end: 199 }));
|
|
17498
|
+
ensure(equalBytes(part, bytes.subarray(100, 200)), "声明 rangeGet:true 但 Range 读不正确");
|
|
17499
|
+
notes.push("rangeGet 属实");
|
|
17500
|
+
}
|
|
17501
|
+
if (caps.conditionalPut) {
|
|
17502
|
+
ensure(r.etag !== void 0, "声明 conditionalPut:true 但 put 未返回 etag");
|
|
17503
|
+
const head = await store.head(k);
|
|
17504
|
+
ensure(head?.etag === r.etag, "声明 conditionalPut:true 但 head 未回带 etag");
|
|
17505
|
+
let conflict = null;
|
|
17506
|
+
try {
|
|
17507
|
+
const other = bytesOf(5e4, 11);
|
|
17508
|
+
await store.put(k, toStream(other), metaFor(other), { ifNoneMatch: "*" });
|
|
17509
|
+
} catch (err) {
|
|
17510
|
+
conflict = err;
|
|
17511
|
+
}
|
|
17512
|
+
ensure(isStoreErrorOf(conflict, "precondition_failed"), "声明 conditionalPut:true 但 ifNoneMatch:* 未拒绝");
|
|
17513
|
+
notes.push("conditionalPut 属实");
|
|
17514
|
+
}
|
|
17515
|
+
if (caps.list === "prefix") {
|
|
17516
|
+
ensure(await waitVisible(`${prefix}/truthful`, k), "声明 list:prefix 但 put 后 list 不见");
|
|
17517
|
+
notes.push("list:prefix 属实");
|
|
17518
|
+
}
|
|
17519
|
+
if (caps.consistency === "strong") {
|
|
17520
|
+
ensure(await store.head(k) !== null, "声明 strong 但写后 head 不可见");
|
|
17521
|
+
notes.push("strong 写后即读属实");
|
|
17522
|
+
}
|
|
17523
|
+
return notes.join(";");
|
|
17524
|
+
});
|
|
17525
|
+
if (options.keepObjects !== true) {
|
|
17526
|
+
const keys = [...created];
|
|
17527
|
+
const batch = Math.max(1, caps.batchDelete ?? 1);
|
|
17528
|
+
for (let i = 0; i < keys.length; i += batch) {
|
|
17529
|
+
await store.delete(keys.slice(i, i + batch)).catch(() => void 0);
|
|
17530
|
+
}
|
|
17531
|
+
}
|
|
17532
|
+
return {
|
|
17533
|
+
cases,
|
|
17534
|
+
passed: cases.every((c) => c.passed),
|
|
17535
|
+
capabilities: { ...caps },
|
|
17536
|
+
startedAt,
|
|
17537
|
+
durationMs: Math.round(performance.now() - t0)
|
|
17538
|
+
};
|
|
17539
|
+
}
|
|
17540
|
+
|
|
17541
|
+
// ../kernel/src/journal/segmented/chaos-blob-store.ts
|
|
17542
|
+
function matches(pred, key2) {
|
|
17543
|
+
if (pred === void 0) return false;
|
|
17544
|
+
return typeof pred === "boolean" ? pred : pred(key2);
|
|
17545
|
+
}
|
|
17546
|
+
var delay = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
17547
|
+
function createChaosBlobStore(inner, faults = {}, now = Date.now) {
|
|
17548
|
+
const putAt = /* @__PURE__ */ new Map();
|
|
17549
|
+
let putsSeen = 0;
|
|
17550
|
+
const stats = {
|
|
17551
|
+
putFailures: 0,
|
|
17552
|
+
putTruncated: 0,
|
|
17553
|
+
listHidden: 0,
|
|
17554
|
+
listDropped: 0,
|
|
17555
|
+
corruptedGets: 0,
|
|
17556
|
+
abortedGets: 0,
|
|
17557
|
+
calls: { put: 0, get: 0, head: 0, list: 0, delete: 0 }
|
|
17558
|
+
};
|
|
17559
|
+
const latency = async () => {
|
|
17560
|
+
const l = faults.latencyMs;
|
|
17561
|
+
if (l === void 0) return;
|
|
17562
|
+
const ms = typeof l === "number" ? l : l.min + Math.random() * Math.max(0, l.max - l.min);
|
|
17563
|
+
if (ms > 0) await delay(ms);
|
|
17564
|
+
};
|
|
17565
|
+
const injectedPutError = (key2, detail) => {
|
|
17566
|
+
const kind = faults.putErrorKind ?? "transient";
|
|
17567
|
+
return new StoreError(kind, `chaos: ${detail} (${key2})`, kind === "transient" && faults.retryAfterMs !== void 0 ? { retryAfterMs: faults.retryAfterMs } : {});
|
|
17568
|
+
};
|
|
17569
|
+
const shouldFailPut = (key2) => {
|
|
17570
|
+
const f = faults.failPut;
|
|
17571
|
+
if (f === void 0) return false;
|
|
17572
|
+
if (typeof f === "number") return putsSeen <= f;
|
|
17573
|
+
return matches(f, key2);
|
|
17574
|
+
};
|
|
17575
|
+
const truncateBody = (body, afterBytes, key2) => {
|
|
17576
|
+
let seen = 0;
|
|
17577
|
+
return body.pipeThrough(
|
|
17578
|
+
new TransformStream({
|
|
17579
|
+
transform(chunk, controller) {
|
|
17580
|
+
if (seen >= afterBytes) {
|
|
17581
|
+
controller.error(injectedPutError(key2, `connection dropped after ${afterBytes} bytes`));
|
|
17582
|
+
return;
|
|
17583
|
+
}
|
|
17584
|
+
const take = Math.min(chunk.byteLength, afterBytes - seen);
|
|
17585
|
+
seen += take;
|
|
17586
|
+
controller.enqueue(chunk.subarray(0, take));
|
|
17587
|
+
if (seen >= afterBytes) controller.error(injectedPutError(key2, `connection dropped after ${afterBytes} bytes`));
|
|
17588
|
+
}
|
|
17589
|
+
})
|
|
17590
|
+
);
|
|
17591
|
+
};
|
|
17592
|
+
const capabilities = {
|
|
17593
|
+
...inner.capabilities,
|
|
17594
|
+
get consistency() {
|
|
17595
|
+
return (faults.eventualListLagMs ?? 0) > 0 || faults.dropListEntries !== void 0 ? "eventual" : inner.capabilities.consistency;
|
|
17596
|
+
}
|
|
17597
|
+
};
|
|
17598
|
+
return {
|
|
17599
|
+
faults,
|
|
17600
|
+
stats,
|
|
17601
|
+
inner,
|
|
17602
|
+
capabilities,
|
|
17603
|
+
async put(key2, body, meta, opts) {
|
|
17604
|
+
stats.calls.put++;
|
|
17605
|
+
putsSeen++;
|
|
17606
|
+
await latency();
|
|
17607
|
+
if (shouldFailPut(key2)) {
|
|
17608
|
+
stats.putFailures++;
|
|
17609
|
+
await body.cancel().catch(() => void 0);
|
|
17610
|
+
throw injectedPutError(key2, "put rejected");
|
|
17611
|
+
}
|
|
17612
|
+
const after = faults.failPutAfterBytes;
|
|
17613
|
+
if (after !== void 0) {
|
|
17614
|
+
stats.putTruncated++;
|
|
17615
|
+
try {
|
|
17616
|
+
return await inner.put(key2, truncateBody(body, after, key2), meta, opts);
|
|
17617
|
+
} catch (err) {
|
|
17618
|
+
if (err instanceof StoreError && err.kind === faults.putErrorKind) throw err;
|
|
17619
|
+
throw injectedPutError(key2, `connection dropped after ${after} bytes`);
|
|
17620
|
+
}
|
|
17621
|
+
}
|
|
17622
|
+
const result = await inner.put(key2, body, meta, opts);
|
|
17623
|
+
putAt.set(key2, now());
|
|
17624
|
+
return result;
|
|
17625
|
+
},
|
|
17626
|
+
async get(key2, range, signal) {
|
|
17627
|
+
stats.calls.get++;
|
|
17628
|
+
await latency();
|
|
17629
|
+
const body = await inner.get(key2, range, signal);
|
|
17630
|
+
if (matches(faults.corruptSegmentKeys, key2)) {
|
|
17631
|
+
stats.corruptedGets++;
|
|
17632
|
+
let flipped = false;
|
|
17633
|
+
return body.pipeThrough(
|
|
17634
|
+
new TransformStream({
|
|
17635
|
+
transform(chunk, controller) {
|
|
17636
|
+
if (!flipped && chunk.byteLength > 0) {
|
|
17637
|
+
const copy = new Uint8Array(chunk);
|
|
17638
|
+
copy[0] = copy[0] ^ 255;
|
|
17639
|
+
flipped = true;
|
|
17640
|
+
controller.enqueue(copy);
|
|
17641
|
+
return;
|
|
17642
|
+
}
|
|
17643
|
+
controller.enqueue(chunk);
|
|
17644
|
+
}
|
|
17645
|
+
})
|
|
17646
|
+
);
|
|
17647
|
+
}
|
|
17648
|
+
if (matches(faults.abortMidStream, key2)) {
|
|
17649
|
+
stats.abortedGets++;
|
|
17650
|
+
let passed = 0;
|
|
17651
|
+
return body.pipeThrough(
|
|
17652
|
+
new TransformStream({
|
|
17653
|
+
transform(chunk, controller) {
|
|
17654
|
+
if (passed === 0) {
|
|
17655
|
+
controller.enqueue(chunk);
|
|
17656
|
+
passed++;
|
|
17657
|
+
return;
|
|
17658
|
+
}
|
|
17659
|
+
controller.error(new StoreError("transient", `chaos: download aborted mid-stream (${key2})`));
|
|
17660
|
+
},
|
|
17661
|
+
flush(controller) {
|
|
17662
|
+
if (passed <= 1) controller.error(new StoreError("transient", `chaos: download aborted mid-stream (${key2})`));
|
|
17663
|
+
}
|
|
17664
|
+
})
|
|
17665
|
+
);
|
|
17666
|
+
}
|
|
17667
|
+
return body;
|
|
17668
|
+
},
|
|
17669
|
+
async head(key2, signal) {
|
|
17670
|
+
stats.calls.head++;
|
|
17671
|
+
await latency();
|
|
17672
|
+
return inner.head(key2, signal);
|
|
17673
|
+
},
|
|
17674
|
+
async *list(prefix, cursor, signal) {
|
|
17675
|
+
stats.calls.list++;
|
|
17676
|
+
await latency();
|
|
17677
|
+
const lag = faults.eventualListLagMs ?? 0;
|
|
17678
|
+
const drop = faults.dropListEntries;
|
|
17679
|
+
for await (const entry of inner.list(prefix, cursor, signal)) {
|
|
17680
|
+
if (lag > 0) {
|
|
17681
|
+
const at = putAt.get(entry.key);
|
|
17682
|
+
if (at !== void 0 && now() - at < lag) {
|
|
17683
|
+
stats.listHidden++;
|
|
17684
|
+
continue;
|
|
17685
|
+
}
|
|
17686
|
+
}
|
|
17687
|
+
if (drop !== void 0) {
|
|
17688
|
+
const dropped = typeof drop === "number" ? Math.random() < drop : matches(drop, entry.key);
|
|
17689
|
+
if (dropped) {
|
|
17690
|
+
stats.listDropped++;
|
|
17691
|
+
continue;
|
|
17692
|
+
}
|
|
17693
|
+
}
|
|
17694
|
+
yield entry;
|
|
17695
|
+
}
|
|
17696
|
+
},
|
|
17697
|
+
async delete(keys, signal) {
|
|
17698
|
+
stats.calls.delete++;
|
|
17699
|
+
await latency();
|
|
17700
|
+
await inner.delete(keys, signal);
|
|
17701
|
+
for (const key2 of keys) putAt.delete(key2);
|
|
17702
|
+
}
|
|
17703
|
+
};
|
|
17704
|
+
}
|
|
16833
17705
|
|
|
16834
17706
|
// ../kernel/src/journal/segmented/store-metrics.ts
|
|
16835
17707
|
var STORE_BUCKETS_MS = DURATION_BUCKETS_MS.filter((b) => b >= 5);
|
|
@@ -16861,8 +17733,8 @@ function storeMetricsFor(registry = defaultMetrics) {
|
|
|
16861
17733
|
}
|
|
16862
17734
|
|
|
16863
17735
|
// ../kernel/src/journal/segmented/cold-tier.ts
|
|
16864
|
-
import { mkdir as
|
|
16865
|
-
import
|
|
17736
|
+
import { mkdir as mkdir5, rm as rm6, stat as stat4 } from "node:fs/promises";
|
|
17737
|
+
import path9 from "node:path";
|
|
16866
17738
|
async function observed(metrics, op, fn, bytes) {
|
|
16867
17739
|
const t0 = performance.now();
|
|
16868
17740
|
try {
|
|
@@ -17089,13 +17961,13 @@ function createColdTier(options) {
|
|
|
17089
17961
|
return state.sessionMeta;
|
|
17090
17962
|
}
|
|
17091
17963
|
async function uploadOne(state, segment) {
|
|
17092
|
-
const filePath =
|
|
17964
|
+
const filePath = path9.join(state.ref.sessionDir, segment.file);
|
|
17093
17965
|
const volume = await currentVolume(state);
|
|
17094
17966
|
const stillHot = volume?.manifest.segments.find((s) => s.n === segment.n && s.lastHash === segment.lastHash);
|
|
17095
17967
|
if (volume === null || stillHot === void 0) return;
|
|
17096
17968
|
let rawBytes;
|
|
17097
17969
|
try {
|
|
17098
|
-
rawBytes = (await
|
|
17970
|
+
rawBytes = (await stat4(filePath)).size;
|
|
17099
17971
|
} catch (err) {
|
|
17100
17972
|
if (isErrnoException(err) && err.code === "ENOENT") {
|
|
17101
17973
|
const laterHot = volume.manifest.segments.some((s) => s.n > segment.n && s.status === "sealed");
|
|
@@ -17161,7 +18033,7 @@ function createColdTier(options) {
|
|
|
17161
18033
|
emit({ type: "store.segment_committed", session: state.ref, segment, key: key2, bytes: entry.bytes, generation: snapshot.manifest.generation });
|
|
17162
18034
|
} finally {
|
|
17163
18035
|
state.inflight.delete(key2);
|
|
17164
|
-
await
|
|
18036
|
+
await rm6(spoolPath, { force: true }).catch(() => void 0);
|
|
17165
18037
|
}
|
|
17166
18038
|
}
|
|
17167
18039
|
async function evictIfNeeded() {
|
|
@@ -17187,7 +18059,7 @@ function createColdTier(options) {
|
|
|
17187
18059
|
continue;
|
|
17188
18060
|
}
|
|
17189
18061
|
try {
|
|
17190
|
-
await
|
|
18062
|
+
await rm6(path9.join(c.state.ref.sessionDir, segment.file), { force: true });
|
|
17191
18063
|
} catch {
|
|
17192
18064
|
continue;
|
|
17193
18065
|
}
|
|
@@ -17217,7 +18089,7 @@ function createColdTier(options) {
|
|
|
17217
18089
|
if (segment.status !== "sealed") continue;
|
|
17218
18090
|
let size;
|
|
17219
18091
|
try {
|
|
17220
|
-
size = (await
|
|
18092
|
+
size = (await stat4(path9.join(state.ref.sessionDir, segment.file))).size;
|
|
17221
18093
|
} catch {
|
|
17222
18094
|
continue;
|
|
17223
18095
|
}
|
|
@@ -17244,7 +18116,7 @@ function createColdTier(options) {
|
|
|
17244
18116
|
while (active < upload.concurrency && runQueue.length > 0) {
|
|
17245
18117
|
const state = runQueue.shift();
|
|
17246
18118
|
active++;
|
|
17247
|
-
void
|
|
18119
|
+
void drain2(state).finally(() => {
|
|
17248
18120
|
active--;
|
|
17249
18121
|
state.scheduled = false;
|
|
17250
18122
|
if (!closed && state.pending.length > 0 && !state.stopped && !state.paused && states.has(state.ref.sessionDir)) schedule(state);
|
|
@@ -17256,7 +18128,7 @@ function createColdTier(options) {
|
|
|
17256
18128
|
});
|
|
17257
18129
|
}
|
|
17258
18130
|
}
|
|
17259
|
-
async function
|
|
18131
|
+
async function drain2(state) {
|
|
17260
18132
|
try {
|
|
17261
18133
|
if (!state.reconciled) await reconcile(state);
|
|
17262
18134
|
while (state.pending.length > 0 && !state.stopped && !closed) {
|
|
@@ -17399,7 +18271,7 @@ function createColdTier(options) {
|
|
|
17399
18271
|
const last = manifest.segments[manifest.segments.length - 1];
|
|
17400
18272
|
const base = { meta, segments: manifest.segments.length, lastSeq: last?.lastSeq ?? -1, volumeId: manifest.volumeId, generation: manifest.generation };
|
|
17401
18273
|
if (hot !== null) return { ...base, restored: false };
|
|
17402
|
-
await
|
|
18274
|
+
await mkdir5(session.sessionDir, { recursive: true });
|
|
17403
18275
|
await writeManifest(session.sessionDir, coldManifestToHot(manifest));
|
|
17404
18276
|
const state = states.get(session.sessionDir);
|
|
17405
18277
|
if (state !== void 0) {
|
|
@@ -17458,11 +18330,11 @@ function createColdTier(options) {
|
|
|
17458
18330
|
}
|
|
17459
18331
|
|
|
17460
18332
|
// ../kernel/src/journal/session-index.ts
|
|
17461
|
-
import { appendFile as appendFile2, readFile as
|
|
17462
|
-
import
|
|
18333
|
+
import { appendFile as appendFile2, readFile as readFile7, readdir as readdir2, rm as rm7, stat as stat5 } from "node:fs/promises";
|
|
18334
|
+
import path10 from "node:path";
|
|
17463
18335
|
var SESSION_INDEX_FILE_NAME = "index.jsonl";
|
|
17464
18336
|
function sessionIndexFilePath(storageRoot) {
|
|
17465
|
-
return
|
|
18337
|
+
return path10.join(storageRoot, SESSION_INDEX_FILE_NAME);
|
|
17466
18338
|
}
|
|
17467
18339
|
var COMPACT_SLACK_ROWS = 256;
|
|
17468
18340
|
var COMPACT_RATIO = 4;
|
|
@@ -17477,7 +18349,7 @@ var SessionIndex = class {
|
|
|
17477
18349
|
constructor(options) {
|
|
17478
18350
|
this.#root = options.storageRoot;
|
|
17479
18351
|
this.#file = sessionIndexFilePath(options.storageRoot);
|
|
17480
|
-
this.#sessionsDir =
|
|
18352
|
+
this.#sessionsDir = path10.join(options.storageRoot, "sessions");
|
|
17481
18353
|
this.#readMeta = options.readMeta;
|
|
17482
18354
|
}
|
|
17483
18355
|
/** 全部存活条目(无序;调用方自行排序/截断) */
|
|
@@ -17541,14 +18413,14 @@ var SessionIndex = class {
|
|
|
17541
18413
|
}
|
|
17542
18414
|
async #dirMtime() {
|
|
17543
18415
|
try {
|
|
17544
|
-
return (await
|
|
18416
|
+
return (await stat5(this.#sessionsDir)).mtimeMs;
|
|
17545
18417
|
} catch {
|
|
17546
18418
|
return -1;
|
|
17547
18419
|
}
|
|
17548
18420
|
}
|
|
17549
18421
|
async #fileSize() {
|
|
17550
18422
|
try {
|
|
17551
|
-
return (await
|
|
18423
|
+
return (await stat5(this.#file)).size;
|
|
17552
18424
|
} catch {
|
|
17553
18425
|
return -1;
|
|
17554
18426
|
}
|
|
@@ -17585,7 +18457,7 @@ var SessionIndex = class {
|
|
|
17585
18457
|
async #loadFile() {
|
|
17586
18458
|
let raw;
|
|
17587
18459
|
try {
|
|
17588
|
-
raw = await
|
|
18460
|
+
raw = await readFile7(this.#file, "utf8");
|
|
17589
18461
|
} catch (err) {
|
|
17590
18462
|
if (isErrnoException(err) && err.code === "ENOENT") return null;
|
|
17591
18463
|
throw err;
|
|
@@ -17626,13 +18498,13 @@ var SessionIndex = class {
|
|
|
17626
18498
|
const entries = /* @__PURE__ */ new Map();
|
|
17627
18499
|
let names = [];
|
|
17628
18500
|
try {
|
|
17629
|
-
names = await
|
|
18501
|
+
names = await readdir2(this.#sessionsDir, { withFileTypes: true });
|
|
17630
18502
|
} catch {
|
|
17631
18503
|
names = [];
|
|
17632
18504
|
}
|
|
17633
18505
|
for (const entry of names) {
|
|
17634
18506
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
17635
|
-
const meta = await this.#readMeta(
|
|
18507
|
+
const meta = await this.#readMeta(path10.join(this.#sessionsDir, entry.name), entry.name);
|
|
17636
18508
|
if (meta !== null) entries.set(meta.sessionId, meta);
|
|
17637
18509
|
}
|
|
17638
18510
|
const state = { entries, fileSize: -1, dirMtimeMs, rows: 0 };
|
|
@@ -17652,7 +18524,7 @@ var SessionIndex = class {
|
|
|
17652
18524
|
` : "";
|
|
17653
18525
|
try {
|
|
17654
18526
|
if (state.entries.size === 0 && state.dirMtimeMs === -1) {
|
|
17655
|
-
await
|
|
18527
|
+
await rm7(this.#file, { force: true }).catch(() => void 0);
|
|
17656
18528
|
state.fileSize = -1;
|
|
17657
18529
|
state.rows = 0;
|
|
17658
18530
|
return;
|
|
@@ -17683,9 +18555,9 @@ var SessionIndex = class {
|
|
|
17683
18555
|
};
|
|
17684
18556
|
|
|
17685
18557
|
// ../kernel/src/journal/checkpoints.ts
|
|
17686
|
-
import { createHash as
|
|
17687
|
-
import { access as access3, mkdir as
|
|
17688
|
-
import
|
|
18558
|
+
import { createHash as createHash11, randomBytes as randomBytes3 } from "node:crypto";
|
|
18559
|
+
import { access as access3, mkdir as mkdir6, readdir as readdir3, readFile as readFile8, rm as rm8 } from "node:fs/promises";
|
|
18560
|
+
import path11 from "node:path";
|
|
17689
18561
|
import { z as z17 } from "zod";
|
|
17690
18562
|
var ContextCheckpointTriggerSchema = z17.enum([
|
|
17691
18563
|
"manual",
|
|
@@ -17739,7 +18611,7 @@ function newCheckpointId(now = Date.now()) {
|
|
|
17739
18611
|
ulidLastRandom[i] = 0;
|
|
17740
18612
|
}
|
|
17741
18613
|
} else {
|
|
17742
|
-
ulidLastRandom = Array.from(
|
|
18614
|
+
ulidLastRandom = Array.from(randomBytes3(16), (b) => b & 31);
|
|
17743
18615
|
ulidLastTime = now;
|
|
17744
18616
|
}
|
|
17745
18617
|
let timePart = "";
|
|
@@ -17762,13 +18634,13 @@ var META_SUFFIX = ".meta.json";
|
|
|
17762
18634
|
var BODY_SUFFIX = ".json";
|
|
17763
18635
|
function checkpointsDirPath(root, sessionId) {
|
|
17764
18636
|
assertValidSessionId(sessionId);
|
|
17765
|
-
return
|
|
18637
|
+
return path11.join(root, sessionId);
|
|
17766
18638
|
}
|
|
17767
18639
|
function bodyFilePath(dir, checkpointId) {
|
|
17768
|
-
return
|
|
18640
|
+
return path11.join(dir, `${checkpointId}${BODY_SUFFIX}`);
|
|
17769
18641
|
}
|
|
17770
18642
|
function metaFilePath2(dir, checkpointId) {
|
|
17771
|
-
return
|
|
18643
|
+
return path11.join(dir, `${checkpointId}${META_SUFFIX}`);
|
|
17772
18644
|
}
|
|
17773
18645
|
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
17774
18646
|
var REF_PATTERN2 = /^sha256:([0-9a-f]{64})$/;
|
|
@@ -17800,7 +18672,7 @@ async function storeImages(messages, cpDir, attachmentsFrom) {
|
|
|
17800
18672
|
`messages 含 ${refs.length} 个已外置($ref)image 块,须以 opts.attachmentsFrom 指明附件来源会话目录`
|
|
17801
18673
|
);
|
|
17802
18674
|
}
|
|
17803
|
-
await
|
|
18675
|
+
await mkdir6(attachmentsDirPath(cpDir), { recursive: true });
|
|
17804
18676
|
for (const hex of refs) {
|
|
17805
18677
|
const target = attachmentFilePath(cpDir, hex);
|
|
17806
18678
|
let targetExists = true;
|
|
@@ -17812,7 +18684,7 @@ async function storeImages(messages, cpDir, attachmentsFrom) {
|
|
|
17812
18684
|
if (targetExists) continue;
|
|
17813
18685
|
let bytes;
|
|
17814
18686
|
try {
|
|
17815
|
-
bytes = await
|
|
18687
|
+
bytes = await readFile8(attachmentFilePath(attachmentsFrom, hex));
|
|
17816
18688
|
} catch (err) {
|
|
17817
18689
|
if (isErrnoException(err) && err.code === "ENOENT") continue;
|
|
17818
18690
|
throw err;
|
|
@@ -17829,7 +18701,7 @@ async function storeImages(messages, cpDir, attachmentsFrom) {
|
|
|
17829
18701
|
async function writeCheckpoint(root, cp, opts = {}) {
|
|
17830
18702
|
assertValidCheckpointId(cp.checkpointId);
|
|
17831
18703
|
const dir = checkpointsDirPath(root, cp.sessionId);
|
|
17832
|
-
await
|
|
18704
|
+
await mkdir6(dir, { recursive: true });
|
|
17833
18705
|
const { messages } = repairHistoryPairing(cp.messages);
|
|
17834
18706
|
const { messages: _ignored, ...head } = cp;
|
|
17835
18707
|
const metaCandidate = { ...head, schemaVersion: 1, messageCount: messages.length };
|
|
@@ -17852,7 +18724,7 @@ async function writeCheckpoint(root, cp, opts = {}) {
|
|
|
17852
18724
|
async function readJsonFile(filePath) {
|
|
17853
18725
|
let raw;
|
|
17854
18726
|
try {
|
|
17855
|
-
raw = await
|
|
18727
|
+
raw = await readFile8(filePath, "utf8");
|
|
17856
18728
|
} catch (err) {
|
|
17857
18729
|
if (isErrnoException(err) && err.code === "ENOENT") return null;
|
|
17858
18730
|
throw err;
|
|
@@ -17904,7 +18776,7 @@ async function listCheckpoints(root, sessionId) {
|
|
|
17904
18776
|
const dir = checkpointsDirPath(root, sessionId);
|
|
17905
18777
|
let names;
|
|
17906
18778
|
try {
|
|
17907
|
-
names = await
|
|
18779
|
+
names = await readdir3(dir);
|
|
17908
18780
|
} catch (err) {
|
|
17909
18781
|
if (isErrnoException(err) && err.code === "ENOENT") return [];
|
|
17910
18782
|
throw err;
|
|
@@ -17944,12 +18816,12 @@ async function deleteCheckpoint(root, sessionId, checkpointId, opts = {}) {
|
|
|
17944
18816
|
const refs = opts.gcAttachments === false ? [] : await collectBodyRefs(bodyFilePath(dir, checkpointId));
|
|
17945
18817
|
let existed = true;
|
|
17946
18818
|
try {
|
|
17947
|
-
await
|
|
18819
|
+
await rm8(bodyFilePath(dir, checkpointId));
|
|
17948
18820
|
} catch (err) {
|
|
17949
18821
|
if (!isErrnoException(err) || err.code !== "ENOENT") throw err;
|
|
17950
18822
|
existed = false;
|
|
17951
18823
|
}
|
|
17952
|
-
await
|
|
18824
|
+
await rm8(metaFilePath2(dir, checkpointId), { force: true });
|
|
17953
18825
|
if (refs.length > 0) await gcAttachments(dir, refs);
|
|
17954
18826
|
return existed;
|
|
17955
18827
|
}
|
|
@@ -17967,7 +18839,7 @@ async function gcAttachments(dir, candidates) {
|
|
|
17967
18839
|
const pending = new Set(candidates);
|
|
17968
18840
|
let names;
|
|
17969
18841
|
try {
|
|
17970
|
-
names = await
|
|
18842
|
+
names = await readdir3(dir);
|
|
17971
18843
|
} catch (err) {
|
|
17972
18844
|
if (isErrnoException(err) && err.code === "ENOENT") return;
|
|
17973
18845
|
throw err;
|
|
@@ -17977,10 +18849,10 @@ async function gcAttachments(dir, candidates) {
|
|
|
17977
18849
|
if (!name.endsWith(BODY_SUFFIX) || name.endsWith(META_SUFFIX)) continue;
|
|
17978
18850
|
const id = name.slice(0, -BODY_SUFFIX.length);
|
|
17979
18851
|
if (!CHECKPOINT_ID_PATTERN.test(id)) continue;
|
|
17980
|
-
for (const hex of await collectBodyRefs(
|
|
18852
|
+
for (const hex of await collectBodyRefs(path11.join(dir, name))) pending.delete(hex);
|
|
17981
18853
|
}
|
|
17982
18854
|
for (const hex of pending) {
|
|
17983
|
-
await
|
|
18855
|
+
await rm8(attachmentFilePath(dir, hex), { force: true });
|
|
17984
18856
|
}
|
|
17985
18857
|
}
|
|
17986
18858
|
async function applyRetention(root, sessionId, reference, retention) {
|
|
@@ -18026,7 +18898,7 @@ function detachInlineImage(block, attachments) {
|
|
|
18026
18898
|
if (!isRecord3(block)) return block;
|
|
18027
18899
|
if (block["t"] === "image" && typeof block["data"] === "string" && block["$ref"] === void 0) {
|
|
18028
18900
|
const bytes = Buffer.from(block["data"], "base64");
|
|
18029
|
-
const hex =
|
|
18901
|
+
const hex = createHash11("sha256").update(bytes).digest("hex");
|
|
18030
18902
|
attachments[hex] = bytes.toString("base64");
|
|
18031
18903
|
return { t: "image", mime: block["mime"], $ref: `sha256:${hex}` };
|
|
18032
18904
|
}
|
|
@@ -18087,9 +18959,9 @@ function decodeCheckpointImport(bytes, target) {
|
|
|
18087
18959
|
);
|
|
18088
18960
|
}
|
|
18089
18961
|
for (const [hex, b64] of Object.entries(bundle.attachments)) {
|
|
18090
|
-
const
|
|
18091
|
-
if (
|
|
18092
|
-
const digest =
|
|
18962
|
+
const bytesOf2 = Buffer.from(b64, "base64");
|
|
18963
|
+
if (bytesOf2.toString("base64") !== b64.replace(/\s+/g, "")) throw importInvalid(`附件 ${hex} 不是合法 base64`);
|
|
18964
|
+
const digest = createHash11("sha256").update(bytesOf2).digest("hex");
|
|
18093
18965
|
if (digest !== hex) throw importInvalid(`附件 ${hex} 字节摘要不符(实为 ${digest})`);
|
|
18094
18966
|
}
|
|
18095
18967
|
const messages = [];
|
|
@@ -18120,11 +18992,11 @@ function decodeCheckpointImport(bytes, target) {
|
|
|
18120
18992
|
}
|
|
18121
18993
|
|
|
18122
18994
|
// ../kernel/src/journal/rotate.ts
|
|
18123
|
-
import { access as access4, mkdir as
|
|
18124
|
-
import
|
|
18995
|
+
import { access as access4, mkdir as mkdir7, readdir as readdir4, readFile as readFile9, rm as rm9, stat as stat6 } from "node:fs/promises";
|
|
18996
|
+
import path12 from "node:path";
|
|
18125
18997
|
var ROTATION_NEXT_FILE_NAME = "journal.next.jsonl";
|
|
18126
18998
|
function rotationNextPath(sessionDir) {
|
|
18127
|
-
return
|
|
18999
|
+
return path12.join(sessionDir, ROTATION_NEXT_FILE_NAME);
|
|
18128
19000
|
}
|
|
18129
19001
|
async function fileExists2(filePath) {
|
|
18130
19002
|
try {
|
|
@@ -18159,28 +19031,28 @@ async function moveTempAttachments(tempDir, sessionDir) {
|
|
|
18159
19031
|
const tempAttachments = attachmentsDirPath(tempDir);
|
|
18160
19032
|
let names = [];
|
|
18161
19033
|
try {
|
|
18162
|
-
names = await
|
|
19034
|
+
names = await readdir4(tempAttachments);
|
|
18163
19035
|
} catch {
|
|
18164
19036
|
names = [];
|
|
18165
19037
|
}
|
|
18166
19038
|
if (names.length === 0) return;
|
|
18167
19039
|
const target = attachmentsDirPath(sessionDir);
|
|
18168
|
-
await
|
|
19040
|
+
await mkdir7(target, { recursive: true });
|
|
18169
19041
|
for (const name of names) {
|
|
18170
|
-
await renameWithRetry(
|
|
19042
|
+
await renameWithRetry(path12.join(tempAttachments, name), path12.join(target, name));
|
|
18171
19043
|
}
|
|
18172
19044
|
}
|
|
18173
19045
|
async function copyReferencedAttachments(tempDir, sessionDir, from) {
|
|
18174
19046
|
const { records } = await readAll(tempDir, { rehydrateImages: false });
|
|
18175
19047
|
const refs = collectImageRefs2(records.map((r) => r.payload));
|
|
18176
19048
|
if (refs.length === 0) return;
|
|
18177
|
-
await
|
|
19049
|
+
await mkdir7(attachmentsDirPath(sessionDir), { recursive: true });
|
|
18178
19050
|
for (const hex of refs) {
|
|
18179
19051
|
const target = attachmentFilePath(sessionDir, hex);
|
|
18180
19052
|
if (await fileExists2(target)) continue;
|
|
18181
19053
|
let bytes;
|
|
18182
19054
|
try {
|
|
18183
|
-
bytes = await
|
|
19055
|
+
bytes = await readFile9(attachmentFilePath(from, hex));
|
|
18184
19056
|
} catch (err) {
|
|
18185
19057
|
if (isErrnoException(err) && err.code === "ENOENT") continue;
|
|
18186
19058
|
throw err;
|
|
@@ -18196,8 +19068,8 @@ async function settleVolume(sessionDir) {
|
|
|
18196
19068
|
return true;
|
|
18197
19069
|
}
|
|
18198
19070
|
async function rotateVolume(sessionDir, build, opts = {}) {
|
|
18199
|
-
const tempDir =
|
|
18200
|
-
await
|
|
19071
|
+
const tempDir = path12.join(sessionDir, `.rotate-${process.pid}-${Date.now().toString(36)}`);
|
|
19072
|
+
await mkdir7(tempDir, { recursive: true });
|
|
18201
19073
|
try {
|
|
18202
19074
|
const writer = await JournalWriter.open(tempDir);
|
|
18203
19075
|
let cursor;
|
|
@@ -18213,16 +19085,16 @@ async function rotateVolume(sessionDir, build, opts = {}) {
|
|
|
18213
19085
|
}
|
|
18214
19086
|
await renameWithRetry(journalFilePath(tempDir), rotationNextPath(sessionDir));
|
|
18215
19087
|
await settleVolume(sessionDir);
|
|
18216
|
-
const { mtimeMs } = await
|
|
19088
|
+
const { mtimeMs } = await stat6(journalFilePath(sessionDir));
|
|
18217
19089
|
return { ...cursor, mtimeMs };
|
|
18218
19090
|
} finally {
|
|
18219
|
-
await
|
|
19091
|
+
await rm9(tempDir, { recursive: true, force: true }).catch(() => void 0);
|
|
18220
19092
|
}
|
|
18221
19093
|
}
|
|
18222
19094
|
|
|
18223
19095
|
// ../kernel/src/journal/fork.ts
|
|
18224
19096
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
18225
|
-
import { access as access5, mkdir as
|
|
19097
|
+
import { access as access5, mkdir as mkdir8, readFile as readFile10, stat as stat7 } from "node:fs/promises";
|
|
18226
19098
|
var FORK_DEFAULT_PRODUCER = "kernel";
|
|
18227
19099
|
function journalKindOf(message) {
|
|
18228
19100
|
if (message.role === "assistant") return "assistant_message";
|
|
@@ -18258,13 +19130,13 @@ async function pathExists(filePath) {
|
|
|
18258
19130
|
}
|
|
18259
19131
|
async function copyRefAttachments(refs, from, sessionDir) {
|
|
18260
19132
|
if (refs.length === 0) return;
|
|
18261
|
-
await
|
|
19133
|
+
await mkdir8(attachmentsDirPath(sessionDir), { recursive: true });
|
|
18262
19134
|
for (const hex of refs) {
|
|
18263
19135
|
const target = attachmentFilePath(sessionDir, hex);
|
|
18264
19136
|
if (await pathExists(target)) continue;
|
|
18265
19137
|
let bytes;
|
|
18266
19138
|
try {
|
|
18267
|
-
bytes = await
|
|
19139
|
+
bytes = await readFile10(attachmentFilePath(from, hex));
|
|
18268
19140
|
} catch (err) {
|
|
18269
19141
|
if (isErrnoException(err) && err.code === "ENOENT") continue;
|
|
18270
19142
|
throw err;
|
|
@@ -18359,27 +19231,27 @@ async function forkSessionFromCheckpoint(input) {
|
|
|
18359
19231
|
} finally {
|
|
18360
19232
|
await lock.release();
|
|
18361
19233
|
}
|
|
18362
|
-
const { mtimeMs } = await
|
|
19234
|
+
const { mtimeMs } = await stat7(journalFilePath(sessionDir));
|
|
18363
19235
|
return { sessionId, sessionDir, cursor: { ...cursor, mtimeMs } };
|
|
18364
19236
|
}
|
|
18365
19237
|
|
|
18366
19238
|
// ../kernel/src/session/cwd.ts
|
|
18367
|
-
import { realpath, stat as
|
|
18368
|
-
import
|
|
19239
|
+
import { realpath, stat as stat8 } from "node:fs/promises";
|
|
19240
|
+
import path13 from "node:path";
|
|
18369
19241
|
var CASE_INSENSITIVE = process.platform === "win32";
|
|
18370
19242
|
function insideRealSubtree(child, base) {
|
|
18371
19243
|
const c = CASE_INSENSITIVE ? child.toLowerCase() : child;
|
|
18372
19244
|
const b = CASE_INSENSITIVE ? base.toLowerCase() : base;
|
|
18373
|
-
const rel =
|
|
18374
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
19245
|
+
const rel = path13.relative(b, c);
|
|
19246
|
+
return rel === "" || !rel.startsWith("..") && !path13.isAbsolute(rel);
|
|
18375
19247
|
}
|
|
18376
19248
|
async function probeDirectory(candidate) {
|
|
18377
|
-
if (!
|
|
19249
|
+
if (!path13.isAbsolute(candidate)) {
|
|
18378
19250
|
return { ok: false, reason: "not_absolute", detail: `cwd 须为绝对路径(不展开 ~):${candidate}` };
|
|
18379
19251
|
}
|
|
18380
19252
|
let isDirectory;
|
|
18381
19253
|
try {
|
|
18382
|
-
isDirectory = (await
|
|
19254
|
+
isDirectory = (await stat8(candidate)).isDirectory();
|
|
18383
19255
|
} catch (err) {
|
|
18384
19256
|
const code = isErrnoException(err) ? err.code : void 0;
|
|
18385
19257
|
return { ok: false, reason: "not_found", detail: `cwd 不可访问(${code ?? "io error"}):${candidate}` };
|
|
@@ -18407,7 +19279,7 @@ async function resolveSessionCwd(input) {
|
|
|
18407
19279
|
};
|
|
18408
19280
|
}
|
|
18409
19281
|
if ("resolve" in policy) {
|
|
18410
|
-
if (!
|
|
19282
|
+
if (!path13.isAbsolute(requested)) {
|
|
18411
19283
|
return { ok: false, reason: "not_absolute", detail: `cwd 须为绝对路径(不展开 ~):${requested}` };
|
|
18412
19284
|
}
|
|
18413
19285
|
let resolved;
|
|
@@ -18422,7 +19294,7 @@ async function resolveSessionCwd(input) {
|
|
|
18422
19294
|
}
|
|
18423
19295
|
const probed2 = await probeDirectory(resolved);
|
|
18424
19296
|
if (!probed2.ok) return probed2;
|
|
18425
|
-
return { ok: true, cwd:
|
|
19297
|
+
return { ok: true, cwd: path13.resolve(resolved) };
|
|
18426
19298
|
}
|
|
18427
19299
|
const probed = await probeDirectory(requested);
|
|
18428
19300
|
if (!probed.ok) return probed;
|
|
@@ -18433,7 +19305,7 @@ async function resolveSessionCwd(input) {
|
|
|
18433
19305
|
} catch {
|
|
18434
19306
|
continue;
|
|
18435
19307
|
}
|
|
18436
|
-
if (insideRealSubtree(probed.real, realRoot)) return { ok: true, cwd:
|
|
19308
|
+
if (insideRealSubtree(probed.real, realRoot)) return { ok: true, cwd: path13.resolve(requested) };
|
|
18437
19309
|
}
|
|
18438
19310
|
return {
|
|
18439
19311
|
ok: false,
|
|
@@ -18443,7 +19315,7 @@ async function resolveSessionCwd(input) {
|
|
|
18443
19315
|
}
|
|
18444
19316
|
|
|
18445
19317
|
// ../kernel/src/tools/search/path-utils.ts
|
|
18446
|
-
import * as
|
|
19318
|
+
import * as path14 from "node:path";
|
|
18447
19319
|
function toPosixPath(p) {
|
|
18448
19320
|
return p.replaceAll("\\", "/");
|
|
18449
19321
|
}
|
|
@@ -18451,10 +19323,10 @@ function normalizeDriveLetter(p) {
|
|
|
18451
19323
|
return /^[A-Za-z]:/.test(p) ? p.charAt(0).toLowerCase() + p.slice(1) : p;
|
|
18452
19324
|
}
|
|
18453
19325
|
function isAbsolutePath(p) {
|
|
18454
|
-
return
|
|
19326
|
+
return path14.win32.isAbsolute(p) || path14.posix.isAbsolute(p);
|
|
18455
19327
|
}
|
|
18456
19328
|
function canonicalize(p) {
|
|
18457
|
-
return normalizeDriveLetter(toPosixPath(
|
|
19329
|
+
return normalizeDriveLetter(toPosixPath(path14.resolve(p)));
|
|
18458
19330
|
}
|
|
18459
19331
|
function resolvePathArg(p, cwd) {
|
|
18460
19332
|
const target = p ?? cwd;
|
|
@@ -18836,7 +19708,7 @@ var SESSION_EVENT_VOCABULARY = {
|
|
|
18836
19708
|
var SESSION_EVENT_KINDS = Object.keys(SESSION_EVENT_VOCABULARY);
|
|
18837
19709
|
|
|
18838
19710
|
// ../kernel/src/tools/files/read.ts
|
|
18839
|
-
import
|
|
19711
|
+
import path15 from "node:path";
|
|
18840
19712
|
import { z as z18 } from "zod";
|
|
18841
19713
|
|
|
18842
19714
|
// ../kernel/src/tools/files/encoding.ts
|
|
@@ -18918,19 +19790,19 @@ function truncateLine(line) {
|
|
|
18918
19790
|
return `${line.slice(0, cut)}…[line truncated: ${line.length} chars total; use Grep to inspect the rest]`;
|
|
18919
19791
|
}
|
|
18920
19792
|
async function findSimilarFiles(fs3, filePath) {
|
|
18921
|
-
const dir =
|
|
18922
|
-
const targetBase =
|
|
18923
|
-
const targetName =
|
|
19793
|
+
const dir = path15.dirname(filePath);
|
|
19794
|
+
const targetBase = path15.basename(filePath);
|
|
19795
|
+
const targetName = path15.parse(filePath).name.toLowerCase();
|
|
18924
19796
|
try {
|
|
18925
19797
|
const entries = await fs3.readdir(dir);
|
|
18926
19798
|
return entries.filter((entry) => entry.isFile()).map((entry) => entry.name).filter(
|
|
18927
|
-
(name) => name.toLowerCase() !== targetBase.toLowerCase() &&
|
|
18928
|
-
).slice(0, 3).map((name) =>
|
|
19799
|
+
(name) => name.toLowerCase() !== targetBase.toLowerCase() && path15.parse(name).name.toLowerCase() === targetName
|
|
19800
|
+
).slice(0, 3).map((name) => path15.join(dir, name));
|
|
18929
19801
|
} catch {
|
|
18930
19802
|
return [];
|
|
18931
19803
|
}
|
|
18932
19804
|
}
|
|
18933
|
-
async function executeImageRead(resolved, args, ctx, fs3,
|
|
19805
|
+
async function executeImageRead(resolved, args, ctx, fs3, stat10, options) {
|
|
18934
19806
|
let mode;
|
|
18935
19807
|
try {
|
|
18936
19808
|
mode = options.imageInput?.();
|
|
@@ -18949,9 +19821,9 @@ async function executeImageRead(resolved, args, ctx, fs3, stat9, options) {
|
|
|
18949
19821
|
"offset/limit apply to text files only. Call Read again without offset/limit to read this image file."
|
|
18950
19822
|
);
|
|
18951
19823
|
}
|
|
18952
|
-
if (
|
|
19824
|
+
if (stat10.size > READ_IMAGE_RAW_MAX_BYTES) {
|
|
18953
19825
|
return errorResult(
|
|
18954
|
-
`Image file is ${
|
|
19826
|
+
`Image file is ${stat10.size} bytes, which exceeds the per-image limit of ${READ_IMAGE_RAW_MAX_BYTES} bytes (5 MiB base64-encoded, aligned with the platform cap). Downscale or compress the image, then retry.`
|
|
18955
19827
|
);
|
|
18956
19828
|
}
|
|
18957
19829
|
let buf;
|
|
@@ -18966,10 +19838,10 @@ async function executeImageRead(resolved, args, ctx, fs3, stat9, options) {
|
|
|
18966
19838
|
const probed = probeImage(buf);
|
|
18967
19839
|
if (probed === null) {
|
|
18968
19840
|
return errorResult(
|
|
18969
|
-
`File has an image extension but its content is not a readable PNG/JPEG/GIF/WebP image (bad or truncated header): ${
|
|
19841
|
+
`File has an image extension but its content is not a readable PNG/JPEG/GIF/WebP image (bad or truncated header): ${path15.basename(resolved)}. If it is actually a text file, rename it; otherwise re-export the image and retry.`
|
|
18970
19842
|
);
|
|
18971
19843
|
}
|
|
18972
|
-
registerFileRead(ctx, normalizeFileKey(resolved),
|
|
19844
|
+
registerFileRead(ctx, normalizeFileKey(resolved), stat10.mtimeMs);
|
|
18973
19845
|
const data = {
|
|
18974
19846
|
path: resolved,
|
|
18975
19847
|
mime: probed.mime,
|
|
@@ -18977,7 +19849,7 @@ async function executeImageRead(resolved, args, ctx, fs3, stat9, options) {
|
|
|
18977
19849
|
height: probed.height,
|
|
18978
19850
|
bytes: buf.length
|
|
18979
19851
|
};
|
|
18980
|
-
const meta = `Image file: ${
|
|
19852
|
+
const meta = `Image file: ${path15.basename(resolved)}
|
|
18981
19853
|
Format: ${probed.mime}
|
|
18982
19854
|
Dimensions: ${probed.width}x${probed.height} px
|
|
18983
19855
|
Size: ${buf.length} bytes`;
|
|
@@ -19000,8 +19872,8 @@ function createReadTool(options = {}) {
|
|
|
19000
19872
|
isConcurrencySafe: true,
|
|
19001
19873
|
touchedPathsOf(args) {
|
|
19002
19874
|
const parsed = ReadArgsSchema.safeParse(args);
|
|
19003
|
-
if (!parsed.success || !
|
|
19004
|
-
return [
|
|
19875
|
+
if (!parsed.success || !path15.isAbsolute(parsed.data.file_path)) return [];
|
|
19876
|
+
return [path15.resolve(parsed.data.file_path)];
|
|
19005
19877
|
},
|
|
19006
19878
|
async execute(args, ctx) {
|
|
19007
19879
|
if (ctx.signal.aborted) {
|
|
@@ -19015,15 +19887,15 @@ function createReadTool(options = {}) {
|
|
|
19015
19887
|
if (invalid) {
|
|
19016
19888
|
return invalid;
|
|
19017
19889
|
}
|
|
19018
|
-
const resolved =
|
|
19890
|
+
const resolved = path15.resolve(args.file_path);
|
|
19019
19891
|
const fs3 = fsOf(ctx);
|
|
19020
19892
|
const realTarget = await checkRealTarget("Read", resolved, { cwd: ctx.cwd, access: "read", fs: fs3 });
|
|
19021
19893
|
if (!realTarget.ok) {
|
|
19022
19894
|
return errorResult(realTarget.reason);
|
|
19023
19895
|
}
|
|
19024
|
-
let
|
|
19896
|
+
let stat10;
|
|
19025
19897
|
try {
|
|
19026
|
-
|
|
19898
|
+
stat10 = await fs3.stat(resolved);
|
|
19027
19899
|
} catch (err) {
|
|
19028
19900
|
if (fsErrorCode(err) === "ENOENT") {
|
|
19029
19901
|
const similar = await findSimilarFiles(fs3, resolved);
|
|
@@ -19032,11 +19904,11 @@ function createReadTool(options = {}) {
|
|
|
19032
19904
|
}
|
|
19033
19905
|
return errorResult(`Failed to read file: ${errorMessageOf(err)}`);
|
|
19034
19906
|
}
|
|
19035
|
-
if (
|
|
19907
|
+
if (stat10.isDirectory()) {
|
|
19036
19908
|
return errorResult(`Path is a directory, not a file: ${resolved}.`);
|
|
19037
19909
|
}
|
|
19038
|
-
if (isImageFileExtension(
|
|
19039
|
-
return executeImageRead(resolved, args, ctx, fs3,
|
|
19910
|
+
if (isImageFileExtension(path15.extname(resolved))) {
|
|
19911
|
+
return executeImageRead(resolved, args, ctx, fs3, stat10, options);
|
|
19040
19912
|
}
|
|
19041
19913
|
const fileKey = normalizeFileKey(resolved);
|
|
19042
19914
|
const effectiveOffset = args.offset ?? 1;
|
|
@@ -19044,7 +19916,7 @@ function createReadTool(options = {}) {
|
|
|
19044
19916
|
const dupWindow = takeDuplicateReadWindow(
|
|
19045
19917
|
ctx,
|
|
19046
19918
|
fileKey,
|
|
19047
|
-
|
|
19919
|
+
stat10.mtimeMs,
|
|
19048
19920
|
effectiveOffset,
|
|
19049
19921
|
effectiveLimit
|
|
19050
19922
|
);
|
|
@@ -19061,9 +19933,9 @@ function createReadTool(options = {}) {
|
|
|
19061
19933
|
data2
|
|
19062
19934
|
);
|
|
19063
19935
|
}
|
|
19064
|
-
if (
|
|
19936
|
+
if (stat10.size > READ_MAX_FULL_BYTES && args.offset === void 0 && args.limit === void 0) {
|
|
19065
19937
|
return errorResult(
|
|
19066
|
-
`File is ${
|
|
19938
|
+
`File is ${stat10.size} bytes, which exceeds the ${READ_MAX_FULL_BYTES}-byte limit for reading the whole file at once (large files inflate context: this one would be roughly ${Math.ceil(stat10.size / 4)}+ tokens). Use the offset and limit parameters to read it in pages, or use Grep to locate the relevant sections first.`
|
|
19067
19939
|
);
|
|
19068
19940
|
}
|
|
19069
19941
|
let buf;
|
|
@@ -19082,7 +19954,7 @@ function createReadTool(options = {}) {
|
|
|
19082
19954
|
);
|
|
19083
19955
|
}
|
|
19084
19956
|
if (decoded.text.length === 0) {
|
|
19085
|
-
registerFileRead(ctx, fileKey,
|
|
19957
|
+
registerFileRead(ctx, fileKey, stat10.mtimeMs, {
|
|
19086
19958
|
offset: effectiveOffset,
|
|
19087
19959
|
limit: effectiveLimit,
|
|
19088
19960
|
totalLines: 0,
|
|
@@ -19096,7 +19968,7 @@ function createReadTool(options = {}) {
|
|
|
19096
19968
|
const startLine = effectiveOffset;
|
|
19097
19969
|
const maxLines = effectiveLimit;
|
|
19098
19970
|
if (startLine > totalLines) {
|
|
19099
|
-
registerFileRead(ctx, fileKey,
|
|
19971
|
+
registerFileRead(ctx, fileKey, stat10.mtimeMs, {
|
|
19100
19972
|
offset: effectiveOffset,
|
|
19101
19973
|
limit: effectiveLimit,
|
|
19102
19974
|
totalLines,
|
|
@@ -19115,7 +19987,7 @@ function createReadTool(options = {}) {
|
|
|
19115
19987
|
text += `
|
|
19116
19988
|
… (showing lines ${startLine}-${endLine} of ${totalLines}; use offset=${endLine + 1} to continue)`;
|
|
19117
19989
|
}
|
|
19118
|
-
registerFileRead(ctx, fileKey,
|
|
19990
|
+
registerFileRead(ctx, fileKey, stat10.mtimeMs, {
|
|
19119
19991
|
offset: effectiveOffset,
|
|
19120
19992
|
limit: effectiveLimit,
|
|
19121
19993
|
totalLines,
|
|
@@ -19129,7 +20001,7 @@ function createReadTool(options = {}) {
|
|
|
19129
20001
|
var ReadTool = createReadTool();
|
|
19130
20002
|
|
|
19131
20003
|
// ../kernel/src/tools/files/write.ts
|
|
19132
|
-
import
|
|
20004
|
+
import path16 from "node:path";
|
|
19133
20005
|
import { z as z19 } from "zod";
|
|
19134
20006
|
var WriteArgsSchema = z19.object({
|
|
19135
20007
|
file_path: z19.string().describe("Absolute path to the file to write. Relative paths are rejected."),
|
|
@@ -19148,8 +20020,8 @@ var WriteTool = {
|
|
|
19148
20020
|
isConcurrencySafe: false,
|
|
19149
20021
|
mutatedPathsOf(args) {
|
|
19150
20022
|
const parsed = WriteArgsSchema.safeParse(args);
|
|
19151
|
-
if (!parsed.success || !
|
|
19152
|
-
return [
|
|
20023
|
+
if (!parsed.success || !path16.isAbsolute(parsed.data.file_path)) return [];
|
|
20024
|
+
return [path16.resolve(parsed.data.file_path)];
|
|
19153
20025
|
},
|
|
19154
20026
|
async execute(args, ctx) {
|
|
19155
20027
|
if (ctx.signal.aborted) {
|
|
@@ -19163,7 +20035,7 @@ var WriteTool = {
|
|
|
19163
20035
|
if (invalid) {
|
|
19164
20036
|
return invalid;
|
|
19165
20037
|
}
|
|
19166
|
-
const resolved =
|
|
20038
|
+
const resolved = path16.resolve(args.file_path);
|
|
19167
20039
|
const key2 = normalizeFileKey(resolved);
|
|
19168
20040
|
const fs3 = fsOf(ctx);
|
|
19169
20041
|
const realTarget = await checkRealTarget("Write", resolved, {
|
|
@@ -19205,7 +20077,7 @@ var WriteTool = {
|
|
|
19205
20077
|
}
|
|
19206
20078
|
} else {
|
|
19207
20079
|
try {
|
|
19208
|
-
await fs3.mkdir(
|
|
20080
|
+
await fs3.mkdir(path16.dirname(resolved), { recursive: true });
|
|
19209
20081
|
} catch (err) {
|
|
19210
20082
|
return errorResult(`Failed to create parent directories: ${errorMessageOf(err)}`);
|
|
19211
20083
|
}
|
|
@@ -19261,7 +20133,7 @@ ${memoryNearLimitNote(health)}`, data);
|
|
|
19261
20133
|
};
|
|
19262
20134
|
|
|
19263
20135
|
// ../kernel/src/tools/files/edit.ts
|
|
19264
|
-
import
|
|
20136
|
+
import path17 from "node:path";
|
|
19265
20137
|
import { z as z20 } from "zod";
|
|
19266
20138
|
var EditArgsSchema = z20.object({
|
|
19267
20139
|
file_path: z20.string().describe("Absolute path to the file to edit. Relative paths are rejected."),
|
|
@@ -19304,8 +20176,8 @@ var EditTool = {
|
|
|
19304
20176
|
isConcurrencySafe: false,
|
|
19305
20177
|
mutatedPathsOf(args) {
|
|
19306
20178
|
const parsed = EditArgsSchema.safeParse(args);
|
|
19307
|
-
if (!parsed.success || !
|
|
19308
|
-
return [
|
|
20179
|
+
if (!parsed.success || !path17.isAbsolute(parsed.data.file_path)) return [];
|
|
20180
|
+
return [path17.resolve(parsed.data.file_path)];
|
|
19309
20181
|
},
|
|
19310
20182
|
async execute(args, ctx) {
|
|
19311
20183
|
if (ctx.signal.aborted) {
|
|
@@ -19319,7 +20191,7 @@ var EditTool = {
|
|
|
19319
20191
|
if (invalid) {
|
|
19320
20192
|
return invalid;
|
|
19321
20193
|
}
|
|
19322
|
-
const resolved =
|
|
20194
|
+
const resolved = path17.resolve(args.file_path);
|
|
19323
20195
|
const key2 = normalizeFileKey(resolved);
|
|
19324
20196
|
const fs3 = fsOf(ctx);
|
|
19325
20197
|
const realTarget = await checkRealTarget("Edit", resolved, {
|
|
@@ -19330,19 +20202,19 @@ var EditTool = {
|
|
|
19330
20202
|
if (!realTarget.ok) {
|
|
19331
20203
|
return errorResult(realTarget.reason);
|
|
19332
20204
|
}
|
|
19333
|
-
let
|
|
20205
|
+
let stat10;
|
|
19334
20206
|
try {
|
|
19335
|
-
|
|
20207
|
+
stat10 = await fs3.stat(resolved);
|
|
19336
20208
|
} catch (err) {
|
|
19337
20209
|
if (fsErrorCode(err) === "ENOENT") {
|
|
19338
20210
|
return errorResult(`File does not exist: ${resolved}. Use the Write tool to create a new file.`);
|
|
19339
20211
|
}
|
|
19340
20212
|
return errorResult(`Failed to access file: ${errorMessageOf(err)}`);
|
|
19341
20213
|
}
|
|
19342
|
-
if (
|
|
20214
|
+
if (stat10.isDirectory()) {
|
|
19343
20215
|
return errorResult(`Path is a directory, not a file: ${resolved}.`);
|
|
19344
20216
|
}
|
|
19345
|
-
const guard = checkStaleWriteGuard(ctx, key2,
|
|
20217
|
+
const guard = checkStaleWriteGuard(ctx, key2, stat10.mtimeMs, "editing");
|
|
19346
20218
|
if (guard) {
|
|
19347
20219
|
return guard;
|
|
19348
20220
|
}
|
|
@@ -19447,7 +20319,7 @@ ${memoryNearLimitNote(health)}`, data);
|
|
|
19447
20319
|
|
|
19448
20320
|
// ../kernel/src/tools/search/glob-tool.ts
|
|
19449
20321
|
import { z as z21 } from "zod";
|
|
19450
|
-
import
|
|
20322
|
+
import path18 from "node:path";
|
|
19451
20323
|
|
|
19452
20324
|
// ../kernel/src/tools/search/walker.ts
|
|
19453
20325
|
var DEFAULT_IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -19541,7 +20413,7 @@ var globTool = {
|
|
|
19541
20413
|
return errorResult3(`path does not exist: ${root}`);
|
|
19542
20414
|
}
|
|
19543
20415
|
if (!rootStat.isDirectory()) return errorResult3(`path is not a directory: ${root}`);
|
|
19544
|
-
const realRoot = await checkRealTarget("Glob",
|
|
20416
|
+
const realRoot = await checkRealTarget("Glob", path18.resolve(root), { cwd: ctx.cwd, access: "read", fs: fs3 });
|
|
19545
20417
|
if (!realRoot.ok) return errorResult3(realRoot.reason);
|
|
19546
20418
|
let matcher;
|
|
19547
20419
|
try {
|
|
@@ -19580,7 +20452,7 @@ var globTool = {
|
|
|
19580
20452
|
|
|
19581
20453
|
// ../kernel/src/tools/search/grep-tool.ts
|
|
19582
20454
|
import { z as z22 } from "zod";
|
|
19583
|
-
import
|
|
20455
|
+
import path19 from "node:path";
|
|
19584
20456
|
|
|
19585
20457
|
// ../kernel/src/tools/search/js-engine.ts
|
|
19586
20458
|
var GREP_MAX_FILE_SIZE = 4 * 1024 * 1024;
|
|
@@ -20054,7 +20926,7 @@ var grepTool = {
|
|
|
20054
20926
|
return finish(errorResult4(`path does not exist: ${root}`));
|
|
20055
20927
|
}
|
|
20056
20928
|
if (!rootStat.isDirectory()) return finish(errorResult4(`path is not a directory: ${root}`));
|
|
20057
|
-
const realRoot = await checkRealTarget("Grep",
|
|
20929
|
+
const realRoot = await checkRealTarget("Grep", path19.resolve(root), { cwd: ctx.cwd, access: "read", fs: fs3 });
|
|
20058
20930
|
if (!realRoot.ok) return finish(errorResult4(realRoot.reason));
|
|
20059
20931
|
try {
|
|
20060
20932
|
new RegExp(args.pattern, args.case_insensitive ? "i" : "");
|
|
@@ -20139,7 +21011,7 @@ var grepTool = {
|
|
|
20139
21011
|
|
|
20140
21012
|
// ../kernel/src/tools/search/list-tool.ts
|
|
20141
21013
|
import { z as z23 } from "zod";
|
|
20142
|
-
import
|
|
21014
|
+
import path20 from "node:path";
|
|
20143
21015
|
var LIST_MAX_ENTRIES = 500;
|
|
20144
21016
|
var ListArgsSchema = z23.object({
|
|
20145
21017
|
path: z23.string().min(1),
|
|
@@ -20169,7 +21041,7 @@ var listTool = {
|
|
|
20169
21041
|
touchedPathsOf(args) {
|
|
20170
21042
|
const parsed = ListArgsSchema.safeParse(args);
|
|
20171
21043
|
if (!parsed.success || !isAbsolutePath(parsed.data.path)) return [];
|
|
20172
|
-
return [
|
|
21044
|
+
return [path20.resolve(parsed.data.path)];
|
|
20173
21045
|
},
|
|
20174
21046
|
async execute(args, ctx) {
|
|
20175
21047
|
if (ctx.signal.aborted) return errorResult5("aborted");
|
|
@@ -20183,7 +21055,7 @@ var listTool = {
|
|
|
20183
21055
|
return errorResult5(`path does not exist: ${root}`);
|
|
20184
21056
|
}
|
|
20185
21057
|
if (!rootStat.isDirectory()) return errorResult5(`path is not a directory: ${root}`);
|
|
20186
|
-
const realRoot = await checkRealTarget("List",
|
|
21058
|
+
const realRoot = await checkRealTarget("List", path20.resolve(root), { cwd: ctx.cwd, access: "read", fs: fs3 });
|
|
20187
21059
|
if (!realRoot.ok) return errorResult5(realRoot.reason);
|
|
20188
21060
|
let ignoreMatchers = [];
|
|
20189
21061
|
try {
|
|
@@ -20713,8 +21585,8 @@ ${tail}`;
|
|
|
20713
21585
|
};
|
|
20714
21586
|
|
|
20715
21587
|
// ../kernel/src/tools/shell/output-file.ts
|
|
20716
|
-
import { createWriteStream as
|
|
20717
|
-
import * as
|
|
21588
|
+
import { createWriteStream as createWriteStream3, mkdirSync } from "node:fs";
|
|
21589
|
+
import * as path21 from "node:path";
|
|
20718
21590
|
var SHELL_OUTPUT_SPILL_THRESHOLD_CHARS = 5e4;
|
|
20719
21591
|
var SHELL_OUTPUT_DIR_NAME = "shell-output";
|
|
20720
21592
|
function sanitizeFileStem(stem) {
|
|
@@ -20723,7 +21595,7 @@ function sanitizeFileStem(stem) {
|
|
|
20723
21595
|
}
|
|
20724
21596
|
function shellOutputFilePath(sessionCwd, toolCallId) {
|
|
20725
21597
|
const stem = toolCallId !== void 0 && toolCallId.trim().length > 0 ? sanitizeFileStem(toolCallId) : `shell-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
20726
|
-
return
|
|
21598
|
+
return path21.join(sessionCwd, ".tansr", SHELL_OUTPUT_DIR_NAME, `${stem}.log`);
|
|
20727
21599
|
}
|
|
20728
21600
|
var ShellOutputSink = class {
|
|
20729
21601
|
filePath;
|
|
@@ -20759,8 +21631,8 @@ var ShellOutputSink = class {
|
|
|
20759
21631
|
forceOpen() {
|
|
20760
21632
|
if (this.#stream !== void 0 || this.#writeError !== void 0 || this.#closed) return;
|
|
20761
21633
|
try {
|
|
20762
|
-
mkdirSync(
|
|
20763
|
-
const stream =
|
|
21634
|
+
mkdirSync(path21.dirname(this.filePath), { recursive: true });
|
|
21635
|
+
const stream = createWriteStream3(this.filePath, { encoding: "utf8" });
|
|
20764
21636
|
stream.on("error", (err) => {
|
|
20765
21637
|
this.#writeError = err.message;
|
|
20766
21638
|
this.#stream = void 0;
|
|
@@ -23188,7 +24060,7 @@ function applyToolOverrides(filtered, overrides) {
|
|
|
23188
24060
|
}
|
|
23189
24061
|
|
|
23190
24062
|
// ../kernel/src/agents/subagent-memory-fence.ts
|
|
23191
|
-
import
|
|
24063
|
+
import path22 from "node:path";
|
|
23192
24064
|
function subagentMemoryFenceOf(ctx) {
|
|
23193
24065
|
if (ctx === void 0 || ctx.dirs.length === 0) return void 0;
|
|
23194
24066
|
return {
|
|
@@ -23203,7 +24075,7 @@ function writeTargetOf(args, cwd) {
|
|
|
23203
24075
|
if (args === null || typeof args !== "object") return null;
|
|
23204
24076
|
const raw = args.file_path;
|
|
23205
24077
|
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
23206
|
-
return
|
|
24078
|
+
return path22.isAbsolute(raw) ? path22.resolve(raw) : path22.resolve(cwd, raw);
|
|
23207
24079
|
}
|
|
23208
24080
|
function fencedSubagentWriteTool(tool, fence) {
|
|
23209
24081
|
return {
|
|
@@ -23507,10 +24379,10 @@ async function runSubagent(options) {
|
|
|
23507
24379
|
}
|
|
23508
24380
|
|
|
23509
24381
|
// ../kernel/src/tools/task/background.ts
|
|
23510
|
-
import * as
|
|
24382
|
+
import * as path23 from "node:path";
|
|
23511
24383
|
var TASK_AGENT_OUTPUT_DIR_NAME = "agent-output";
|
|
23512
24384
|
function taskAgentOutputFilePath(sessionCwd, agentId) {
|
|
23513
|
-
return
|
|
24385
|
+
return path23.join(sessionCwd, ".tansr", TASK_AGENT_OUTPUT_DIR_NAME, `${sanitizeFileStem(agentId)}.md`);
|
|
23514
24386
|
}
|
|
23515
24387
|
function taskSettlementStopReason(reason, aborted) {
|
|
23516
24388
|
if (aborted) return "aborted";
|
|
@@ -23646,35 +24518,35 @@ function resolveSubagentDispatch(input) {
|
|
|
23646
24518
|
return { kind: "inherit", reason: "economy_unavailable" };
|
|
23647
24519
|
}
|
|
23648
24520
|
function assertDispatchPolicyInvariants() {
|
|
23649
|
-
const
|
|
24521
|
+
const fail2 = (detail) => {
|
|
23650
24522
|
throw new Error(`dispatch-policy 不变式违约(装载期 fail-fast):${detail}`);
|
|
23651
24523
|
};
|
|
23652
24524
|
const tierList = SUBAGENT_DISPATCH_TIERS;
|
|
23653
24525
|
const defaultTier = DEFAULT_SUBAGENT_DISPATCH_TIER;
|
|
23654
|
-
if (tierList.length === 0)
|
|
24526
|
+
if (tierList.length === 0) fail2("档位词表不得为空");
|
|
23655
24527
|
if (new Set(tierList).size !== tierList.length) {
|
|
23656
|
-
|
|
24528
|
+
fail2("档位词表存在重复项");
|
|
23657
24529
|
}
|
|
23658
24530
|
if (!tierList.includes("inherit")) {
|
|
23659
|
-
|
|
24531
|
+
fail2("档位词表必含 'inherit'(fail-safe 锚:未知类型/回落链尽头的现状档)");
|
|
23660
24532
|
}
|
|
23661
24533
|
if (defaultTier !== "inherit") {
|
|
23662
|
-
|
|
24534
|
+
fail2(
|
|
23663
24535
|
`缺省档位必须为 'inherit'(新增/未知类型恒不降档的 fail-safe 语义),得到 '${defaultTier}'`
|
|
23664
24536
|
);
|
|
23665
24537
|
}
|
|
23666
24538
|
const tiers = new Set(SUBAGENT_DISPATCH_TIERS);
|
|
23667
24539
|
for (const [name, tier] of Object.entries(SUBAGENT_TYPE_TIER_MAP)) {
|
|
23668
24540
|
if (!AGENT_NAME_RE.test(name)) {
|
|
23669
|
-
|
|
24541
|
+
fail2(`映射表键 '${name}' 不符合代理名约束(${String(AGENT_NAME_RE)})`);
|
|
23670
24542
|
}
|
|
23671
24543
|
if (!tiers.has(tier)) {
|
|
23672
|
-
|
|
24544
|
+
fail2(`映射表值 '${tier}'(键 '${name}')不在档位词表内`);
|
|
23673
24545
|
}
|
|
23674
24546
|
}
|
|
23675
24547
|
for (const template of BUILTIN_AGENT_TEMPLATES) {
|
|
23676
24548
|
if (SUBAGENT_TYPE_TIER_MAP[template.name] === void 0) {
|
|
23677
|
-
|
|
24549
|
+
fail2(
|
|
23678
24550
|
`内置模板 '${template.name}' 未在 SUBAGENT_TYPE_TIER_MAP 显式登记档位——新增内置类型必须做一次显式档位决策(fail-safe 建制)`
|
|
23679
24551
|
);
|
|
23680
24552
|
}
|
|
@@ -25294,8 +26166,8 @@ function suggestedAudioName(audio) {
|
|
|
25294
26166
|
async function sinkInlineAudio(sink, audio) {
|
|
25295
26167
|
try {
|
|
25296
26168
|
const bytes = new Uint8Array(Buffer.from(audio.b64, "base64"));
|
|
25297
|
-
const
|
|
25298
|
-
return { path:
|
|
26169
|
+
const path29 = await sink(bytes, audio.mime, suggestedAudioName(audio));
|
|
26170
|
+
return { path: path29 };
|
|
25299
26171
|
} catch (err) {
|
|
25300
26172
|
return { error: err instanceof Error ? err.message : String(err) };
|
|
25301
26173
|
}
|
|
@@ -25407,7 +26279,7 @@ import { spawnSync as spawnSync2 } from "node:child_process";
|
|
|
25407
26279
|
|
|
25408
26280
|
// ../kernel/src/tools/mcp/win32-spawn.ts
|
|
25409
26281
|
import fs2 from "node:fs";
|
|
25410
|
-
import
|
|
26282
|
+
import path24 from "node:path";
|
|
25411
26283
|
var CMD_META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
|
|
25412
26284
|
var SPAWNABLE_EXTS = [".COM", ".EXE", ".BAT", ".CMD"];
|
|
25413
26285
|
function escapeCmdCommand(command) {
|
|
@@ -25446,7 +26318,7 @@ function defaultFileExists(filePath) {
|
|
|
25446
26318
|
}
|
|
25447
26319
|
}
|
|
25448
26320
|
function resolveCommandFile(command, env, cwd, fileExists3) {
|
|
25449
|
-
const w =
|
|
26321
|
+
const w = path24.win32;
|
|
25450
26322
|
const exts = spawnableExts(envLookup(env, "PATHEXT"));
|
|
25451
26323
|
const bases = [];
|
|
25452
26324
|
if (command.includes("/") || command.includes("\\")) {
|
|
@@ -25480,7 +26352,7 @@ function wrapWithComSpec(target, args, env) {
|
|
|
25480
26352
|
function planStdioSpawn(command, args, options) {
|
|
25481
26353
|
const platform = options.platform ?? process.platform;
|
|
25482
26354
|
if (platform !== "win32") return { file: command, args };
|
|
25483
|
-
const ext =
|
|
26355
|
+
const ext = path24.win32.extname(command).toUpperCase();
|
|
25484
26356
|
if (ext === ".EXE" || ext === ".COM") return { file: command, args };
|
|
25485
26357
|
if (ext === ".CMD" || ext === ".BAT") return wrapWithComSpec(command, args, options.env);
|
|
25486
26358
|
if (ext !== "") return { file: command, args };
|
|
@@ -25488,7 +26360,7 @@ function planStdioSpawn(command, args, options) {
|
|
|
25488
26360
|
const fileExists3 = options.fileExists ?? defaultFileExists;
|
|
25489
26361
|
const resolved = resolveCommandFile(command, options.env, cwd, fileExists3);
|
|
25490
26362
|
if (resolved === void 0) return { file: command, args };
|
|
25491
|
-
const resolvedExt =
|
|
26363
|
+
const resolvedExt = path24.win32.extname(resolved).toUpperCase();
|
|
25492
26364
|
if (resolvedExt === ".CMD" || resolvedExt === ".BAT") {
|
|
25493
26365
|
return wrapWithComSpec(resolved, args, options.env);
|
|
25494
26366
|
}
|
|
@@ -26827,7 +27699,7 @@ async function connectHttpMcpServer(config, options) {
|
|
|
26827
27699
|
}
|
|
26828
27700
|
|
|
26829
27701
|
// ../kernel/src/tools/mcp/bridge.ts
|
|
26830
|
-
import { createHash as
|
|
27702
|
+
import { createHash as createHash12 } from "node:crypto";
|
|
26831
27703
|
import { z as z40 } from "zod";
|
|
26832
27704
|
var MCP_TOOL_NAME_PREFIX = "mcp__";
|
|
26833
27705
|
var MCP_REMOTE_TOOL_NAME_RE = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
@@ -26835,7 +27707,7 @@ var MAX_LIST_PAGES = 64;
|
|
|
26835
27707
|
function buildMcpToolName(serverName, toolName2) {
|
|
26836
27708
|
const full = `${MCP_TOOL_NAME_PREFIX}${serverName}__${toolName2}`;
|
|
26837
27709
|
if (full.length <= MCP_BRIDGED_TOOL_NAME_MAX_CHARS) return full;
|
|
26838
|
-
const hash =
|
|
27710
|
+
const hash = createHash12("sha256").update(full, "utf8").digest("hex").slice(0, MCP_NAME_FOLD_HASH_CHARS);
|
|
26839
27711
|
const prefix = `${MCP_TOOL_NAME_PREFIX}${serverName}__`;
|
|
26840
27712
|
const tailBudget = MCP_BRIDGED_TOOL_NAME_MAX_CHARS - prefix.length;
|
|
26841
27713
|
if (tailBudget >= MCP_NAME_FOLD_HASH_CHARS + 1) {
|
|
@@ -27724,15 +28596,15 @@ var McpConnectionPool = class {
|
|
|
27724
28596
|
* `mcp.closed(server_exit|error)` → `mcp.reconnecting`(RFC-SC-3 R4:重连帧
|
|
27725
28597
|
* 不携原因,原因在**前帧** closed / connect_failed;SC-45 直透卷钉住此序)。
|
|
27726
28598
|
*/
|
|
27727
|
-
#observerFor(slot,
|
|
28599
|
+
#observerFor(slot, link2) {
|
|
27728
28600
|
return (event) => {
|
|
27729
28601
|
try {
|
|
27730
28602
|
this.#onEvent?.(event);
|
|
27731
28603
|
} finally {
|
|
27732
28604
|
if (event.type === "mcp.closed" && event.server === this.serverName) {
|
|
27733
|
-
|
|
27734
|
-
if (event.reason !== "closed" &&
|
|
27735
|
-
this.#memberDown(slot,
|
|
28605
|
+
link2.dead = true;
|
|
28606
|
+
if (event.reason !== "closed" && link2.connection !== void 0) {
|
|
28607
|
+
this.#memberDown(slot, link2.connection);
|
|
27736
28608
|
}
|
|
27737
28609
|
}
|
|
27738
28610
|
}
|
|
@@ -27823,12 +28695,12 @@ var McpConnectionPool = class {
|
|
|
27823
28695
|
*/
|
|
27824
28696
|
async #attempt(slot, reason, automatic) {
|
|
27825
28697
|
const attemptStartedAt = this.#now();
|
|
27826
|
-
const
|
|
28698
|
+
const link2 = { connection: void 0, dead: false };
|
|
27827
28699
|
let connection;
|
|
27828
28700
|
try {
|
|
27829
|
-
connection = await this.#connectMember(slot.index, this.#observerFor(slot,
|
|
27830
|
-
|
|
27831
|
-
if (
|
|
28701
|
+
connection = await this.#connectMember(slot.index, this.#observerFor(slot, link2));
|
|
28702
|
+
link2.connection = connection;
|
|
28703
|
+
if (link2.dead || isClosedConnection(connection)) {
|
|
27832
28704
|
throw new McpError("transport_closed", `MCP server "${this.serverName}" closed the connection during setup.`, {
|
|
27833
28705
|
server: this.serverName
|
|
27834
28706
|
});
|
|
@@ -27836,7 +28708,7 @@ var McpConnectionPool = class {
|
|
|
27836
28708
|
if (this.#onMemberUp !== void 0) {
|
|
27837
28709
|
await this.#onMemberUp({ slot: slot.index, connection, reason });
|
|
27838
28710
|
}
|
|
27839
|
-
if (
|
|
28711
|
+
if (link2.dead || isClosedConnection(connection)) {
|
|
27840
28712
|
throw new McpError("transport_closed", `MCP server "${this.serverName}" closed the connection during discovery.`, {
|
|
27841
28713
|
server: this.serverName
|
|
27842
28714
|
});
|
|
@@ -27847,7 +28719,7 @@ var McpConnectionPool = class {
|
|
|
27847
28719
|
return { ok: false, error: this.#closedError() };
|
|
27848
28720
|
}
|
|
27849
28721
|
slot.connection = connection;
|
|
27850
|
-
slot.link =
|
|
28722
|
+
slot.link = link2;
|
|
27851
28723
|
slot.generation += 1;
|
|
27852
28724
|
slot.connectedAt = this.#now();
|
|
27853
28725
|
slot.exhausted = false;
|
|
@@ -27980,7 +28852,7 @@ function resolveMcpServerGovernance(config, layers = {}) {
|
|
|
27980
28852
|
}
|
|
27981
28853
|
|
|
27982
28854
|
// ../kernel/src/tools/mcp/catalog-store.ts
|
|
27983
|
-
import { createHash as
|
|
28855
|
+
import { createHash as createHash13 } from "node:crypto";
|
|
27984
28856
|
var MCP_CATALOG_STORE_VERSION = 1;
|
|
27985
28857
|
function stableStringify2(value) {
|
|
27986
28858
|
if (value === null || typeof value !== "object") {
|
|
@@ -27994,10 +28866,10 @@ function stableStringify2(value) {
|
|
|
27994
28866
|
return `{${parts.join(",")}}`;
|
|
27995
28867
|
}
|
|
27996
28868
|
function computeMcpServerConfigFingerprint(config) {
|
|
27997
|
-
return
|
|
28869
|
+
return createHash13("sha256").update(stableStringify2(config), "utf8").digest("hex");
|
|
27998
28870
|
}
|
|
27999
28871
|
function computeMcpToolDefinitionHash(tool) {
|
|
28000
|
-
return
|
|
28872
|
+
return createHash13("sha256").update(
|
|
28001
28873
|
stableStringify2({
|
|
28002
28874
|
name: tool.name,
|
|
28003
28875
|
description: tool.description,
|
|
@@ -28611,7 +29483,7 @@ async function initMcpLazyManager(toolset, servers, options = {}) {
|
|
|
28611
29483
|
import os3 from "node:os";
|
|
28612
29484
|
|
|
28613
29485
|
// ../kernel/src/hooks/config.ts
|
|
28614
|
-
import { createHash as
|
|
29486
|
+
import { createHash as createHash14 } from "node:crypto";
|
|
28615
29487
|
|
|
28616
29488
|
// ../kernel/src/config/merge.ts
|
|
28617
29489
|
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
@@ -28641,12 +29513,12 @@ function deepMergeRawLayers(rawsAscending) {
|
|
|
28641
29513
|
}
|
|
28642
29514
|
return structuredClone(acc);
|
|
28643
29515
|
}
|
|
28644
|
-
function displayPath2(
|
|
28645
|
-
return
|
|
29516
|
+
function displayPath2(path29) {
|
|
29517
|
+
return path29.map(String).join(".");
|
|
28646
29518
|
}
|
|
28647
|
-
function getAtPath(root,
|
|
29519
|
+
function getAtPath(root, path29) {
|
|
28648
29520
|
let cur = root;
|
|
28649
|
-
for (const seg of
|
|
29521
|
+
for (const seg of path29) {
|
|
28650
29522
|
if (typeof seg === "number") {
|
|
28651
29523
|
if (!Array.isArray(cur) || seg < 0 || seg >= cur.length) return void 0;
|
|
28652
29524
|
cur = cur[seg];
|
|
@@ -28657,11 +29529,11 @@ function getAtPath(root, path28) {
|
|
|
28657
29529
|
}
|
|
28658
29530
|
return cur;
|
|
28659
29531
|
}
|
|
28660
|
-
function hasPath(root,
|
|
28661
|
-
if (
|
|
29532
|
+
function hasPath(root, path29) {
|
|
29533
|
+
if (path29.length === 0) return true;
|
|
28662
29534
|
let cur = root;
|
|
28663
|
-
for (let i = 0; i <
|
|
28664
|
-
const seg =
|
|
29535
|
+
for (let i = 0; i < path29.length; i++) {
|
|
29536
|
+
const seg = path29[i];
|
|
28665
29537
|
if (typeof seg === "number") {
|
|
28666
29538
|
if (!Array.isArray(cur) || seg < 0 || seg >= cur.length) return false;
|
|
28667
29539
|
cur = cur[seg];
|
|
@@ -28672,25 +29544,25 @@ function hasPath(root, path28) {
|
|
|
28672
29544
|
}
|
|
28673
29545
|
return true;
|
|
28674
29546
|
}
|
|
28675
|
-
function sanitizeDropPath(root,
|
|
29547
|
+
function sanitizeDropPath(root, path29) {
|
|
28676
29548
|
let cur = root;
|
|
28677
|
-
for (let i = 0; i <
|
|
28678
|
-
const seg =
|
|
29549
|
+
for (let i = 0; i < path29.length; i++) {
|
|
29550
|
+
const seg = path29[i];
|
|
28679
29551
|
const next = getAtPath(cur, [seg]);
|
|
28680
|
-
if (Array.isArray(next) && i <
|
|
28681
|
-
return
|
|
29552
|
+
if (Array.isArray(next) && i < path29.length - 1) {
|
|
29553
|
+
return path29.slice(0, i + 1);
|
|
28682
29554
|
}
|
|
28683
|
-
if (typeof
|
|
28684
|
-
return
|
|
29555
|
+
if (typeof path29[i + 1] === "number") {
|
|
29556
|
+
return path29.slice(0, i + 1);
|
|
28685
29557
|
}
|
|
28686
29558
|
cur = next;
|
|
28687
29559
|
}
|
|
28688
|
-
return
|
|
29560
|
+
return path29;
|
|
28689
29561
|
}
|
|
28690
|
-
function deletePath(root,
|
|
28691
|
-
if (
|
|
28692
|
-
const parent = getAtPath(root,
|
|
28693
|
-
const leaf =
|
|
29562
|
+
function deletePath(root, path29) {
|
|
29563
|
+
if (path29.length === 0) return;
|
|
29564
|
+
const parent = getAtPath(root, path29.slice(0, -1));
|
|
29565
|
+
const leaf = path29[path29.length - 1];
|
|
28694
29566
|
if (isPlainObject2(parent) && typeof leaf === "string") {
|
|
28695
29567
|
delete parent[leaf];
|
|
28696
29568
|
}
|
|
@@ -28780,16 +29652,16 @@ function canonicalJson(value) {
|
|
|
28780
29652
|
return JSON.stringify(value) ?? "null";
|
|
28781
29653
|
}
|
|
28782
29654
|
function configHash8(dedupeKey) {
|
|
28783
|
-
return
|
|
29655
|
+
return createHash14("sha256").update(dedupeKey, "utf8").digest("hex").slice(0, 8);
|
|
28784
29656
|
}
|
|
28785
|
-
function parseExecutor(value, layer,
|
|
29657
|
+
function parseExecutor(value, layer, path29, file, addDiagnostic) {
|
|
28786
29658
|
const fileRef = file !== void 0 ? { file } : {};
|
|
28787
29659
|
const invalid = (params) => {
|
|
28788
29660
|
addDiagnostic({
|
|
28789
29661
|
severity: "error",
|
|
28790
29662
|
code: "hook_invalid_value",
|
|
28791
29663
|
layer,
|
|
28792
|
-
path:
|
|
29664
|
+
path: path29,
|
|
28793
29665
|
...fileRef,
|
|
28794
29666
|
params
|
|
28795
29667
|
});
|
|
@@ -28837,7 +29709,7 @@ function parseExecutor(value, layer, path28, file, addDiagnostic) {
|
|
|
28837
29709
|
severity: "error",
|
|
28838
29710
|
code: "hook_url_invalid",
|
|
28839
29711
|
layer,
|
|
28840
|
-
path: `${
|
|
29712
|
+
path: `${path29}.url`,
|
|
28841
29713
|
...fileRef,
|
|
28842
29714
|
params: { reason: "unparsable" }
|
|
28843
29715
|
});
|
|
@@ -28848,7 +29720,7 @@ function parseExecutor(value, layer, path28, file, addDiagnostic) {
|
|
|
28848
29720
|
severity: "error",
|
|
28849
29721
|
code: "hook_url_invalid",
|
|
28850
29722
|
layer,
|
|
28851
|
-
path: `${
|
|
29723
|
+
path: `${path29}.url`,
|
|
28852
29724
|
...fileRef,
|
|
28853
29725
|
params: { reason: "unsupported_protocol", protocol: parsed.protocol }
|
|
28854
29726
|
});
|
|
@@ -29439,7 +30311,7 @@ function migrateLayerRaw(raw) {
|
|
|
29439
30311
|
|
|
29440
30312
|
// ../kernel/src/config/paths.ts
|
|
29441
30313
|
import { promises as fsPromises } from "node:fs";
|
|
29442
|
-
import
|
|
30314
|
+
import path25 from "node:path";
|
|
29443
30315
|
var SETTINGS_FILE_NAME = "settings.json";
|
|
29444
30316
|
var LOCAL_SETTINGS_FILE_NAME = "settings.local.json";
|
|
29445
30317
|
var TANSR_DIR_NAME = ".tansr";
|
|
@@ -29466,27 +30338,27 @@ function normalizeForCompare(p, platform) {
|
|
|
29466
30338
|
function defaultManagedPaths(platform, env) {
|
|
29467
30339
|
if (platform === "win32") {
|
|
29468
30340
|
const programData = readTrimmedEnv2(env, "PROGRAMDATA") ?? "C:\\ProgramData";
|
|
29469
|
-
return [
|
|
30341
|
+
return [path25.join(programData, "tansr", MANAGED_SETTINGS_FILE_NAME)];
|
|
29470
30342
|
}
|
|
29471
|
-
return [
|
|
30343
|
+
return [path25.posix.join("/etc", "tansr", MANAGED_SETTINGS_FILE_NAME)];
|
|
29472
30344
|
}
|
|
29473
30345
|
async function discoverLayerFiles(options) {
|
|
29474
30346
|
const { fs: fs3, env, platform } = options;
|
|
29475
30347
|
const configDirOverride = readTrimmedEnv2(env, "TANSR_CONFIG_DIR");
|
|
29476
|
-
const userDir = configDirOverride ??
|
|
29477
|
-
const userFile =
|
|
29478
|
-
const homeKey = normalizeForCompare(
|
|
30348
|
+
const userDir = configDirOverride ?? path25.join(options.homedir, TANSR_DIR_NAME);
|
|
30349
|
+
const userFile = path25.join(userDir, SETTINGS_FILE_NAME);
|
|
30350
|
+
const homeKey = normalizeForCompare(path25.resolve(options.homedir), platform);
|
|
29479
30351
|
let projectRoot;
|
|
29480
|
-
const boundary = options.boundary === void 0 ? void 0 : normalizeForCompare(
|
|
29481
|
-
let cursor =
|
|
30352
|
+
const boundary = options.boundary === void 0 ? void 0 : normalizeForCompare(path25.resolve(options.boundary), platform);
|
|
30353
|
+
let cursor = path25.resolve(options.cwd);
|
|
29482
30354
|
for (let depth = 0; depth < 256; depth++) {
|
|
29483
30355
|
const homeAnchorInvisible = normalizeForCompare(cursor, platform) === homeKey;
|
|
29484
|
-
if (!homeAnchorInvisible && await fs3.directoryExists(
|
|
30356
|
+
if (!homeAnchorInvisible && await fs3.directoryExists(path25.join(cursor, TANSR_DIR_NAME))) {
|
|
29485
30357
|
projectRoot = cursor;
|
|
29486
30358
|
break;
|
|
29487
30359
|
}
|
|
29488
30360
|
if (boundary !== void 0 && normalizeForCompare(cursor, platform) === boundary) break;
|
|
29489
|
-
const parent =
|
|
30361
|
+
const parent = path25.dirname(cursor);
|
|
29490
30362
|
if (parent === cursor) break;
|
|
29491
30363
|
cursor = parent;
|
|
29492
30364
|
}
|
|
@@ -29495,8 +30367,8 @@ async function discoverLayerFiles(options) {
|
|
|
29495
30367
|
managedCandidates,
|
|
29496
30368
|
...projectRoot !== void 0 ? {
|
|
29497
30369
|
projectRoot,
|
|
29498
|
-
projectSharedFile:
|
|
29499
|
-
projectLocalFile:
|
|
30370
|
+
projectSharedFile: path25.join(projectRoot, TANSR_DIR_NAME, SETTINGS_FILE_NAME),
|
|
30371
|
+
projectLocalFile: path25.join(projectRoot, TANSR_DIR_NAME, LOCAL_SETTINGS_FILE_NAME)
|
|
29500
30372
|
} : {},
|
|
29501
30373
|
userFile
|
|
29502
30374
|
};
|
|
@@ -30083,8 +30955,8 @@ function issueExpected(issue) {
|
|
|
30083
30955
|
return issue.code;
|
|
30084
30956
|
}
|
|
30085
30957
|
}
|
|
30086
|
-
function attributeLayer(layersDescending,
|
|
30087
|
-
return layersDescending.find((entry) => hasPath(entry.raw,
|
|
30958
|
+
function attributeLayer(layersDescending, path29) {
|
|
30959
|
+
return layersDescending.find((entry) => hasPath(entry.raw, path29));
|
|
30088
30960
|
}
|
|
30089
30961
|
function scanUnknownKeys(raw, parsed, pathSoFar, layersDescending, addDiagnostic) {
|
|
30090
30962
|
if (!isPlainObject2(raw) || !isPlainObject2(parsed)) return;
|
|
@@ -31182,7 +32054,7 @@ var HookEngine = class {
|
|
|
31182
32054
|
// ../kernel/src/skills/discover.ts
|
|
31183
32055
|
import { promises as fsPromises2 } from "node:fs";
|
|
31184
32056
|
import os4 from "node:os";
|
|
31185
|
-
import
|
|
32057
|
+
import path26 from "node:path";
|
|
31186
32058
|
|
|
31187
32059
|
// ../kernel/src/skills/types.ts
|
|
31188
32060
|
var SKILLS_DIR_NAME = "skills";
|
|
@@ -31402,8 +32274,8 @@ async function scanFileSource(source, skillsDir, fs3, diagnostics) {
|
|
|
31402
32274
|
const seen = /* @__PURE__ */ new Set();
|
|
31403
32275
|
for (const dirName of [...subdirs].sort()) {
|
|
31404
32276
|
if (dirName.startsWith(".") || dirName.startsWith("_")) continue;
|
|
31405
|
-
const dir =
|
|
31406
|
-
const file =
|
|
32277
|
+
const dir = path26.join(skillsDir, dirName);
|
|
32278
|
+
const file = path26.join(dir, SKILL_FILE_NAME);
|
|
31407
32279
|
if (!SKILL_NAME_RE.test(dirName)) {
|
|
31408
32280
|
diagnostics.push({ severity: "warning", code: "name_invalid", source, skill: dirName, file });
|
|
31409
32281
|
continue;
|
|
@@ -31452,7 +32324,7 @@ async function discoverSkills(options = {}) {
|
|
|
31452
32324
|
const platform = options.platform ?? process.platform;
|
|
31453
32325
|
const homedir = options.homedir ?? os4.homedir();
|
|
31454
32326
|
const cwd = options.cwd ?? process.cwd();
|
|
31455
|
-
const explicitRoot = options.projectRoot === void 0 ? void 0 :
|
|
32327
|
+
const explicitRoot = options.projectRoot === void 0 ? void 0 : path26.resolve(options.projectRoot);
|
|
31456
32328
|
const discovered = await discoverLayerFiles({
|
|
31457
32329
|
cwd: explicitRoot ?? cwd,
|
|
31458
32330
|
homedir,
|
|
@@ -31463,13 +32335,13 @@ async function discoverSkills(options = {}) {
|
|
|
31463
32335
|
...explicitRoot !== void 0 ? { boundary: explicitRoot } : options.boundary !== void 0 ? { boundary: options.boundary } : {}
|
|
31464
32336
|
});
|
|
31465
32337
|
const builtinEntries = collectBuiltin(options.builtinSkills ?? [], diagnostics);
|
|
31466
|
-
const userSkillsDir =
|
|
32338
|
+
const userSkillsDir = path26.join(path26.dirname(discovered.userFile), SKILLS_DIR_NAME);
|
|
31467
32339
|
const userOutcome = await scanFileSource("user", userSkillsDir, fs3, diagnostics);
|
|
31468
32340
|
const projectRoot = explicitRoot ?? discovered.projectRoot;
|
|
31469
32341
|
let projectSkillsDir;
|
|
31470
32342
|
let projectOutcome = { status: "missing" };
|
|
31471
32343
|
if (projectRoot !== void 0) {
|
|
31472
|
-
projectSkillsDir =
|
|
32344
|
+
projectSkillsDir = path26.join(projectRoot, TANSR_DIR_NAME, SKILLS_DIR_NAME);
|
|
31473
32345
|
projectOutcome = await scanFileSource("project", projectSkillsDir, fs3, diagnostics);
|
|
31474
32346
|
}
|
|
31475
32347
|
const userEntries = userOutcome.status === "scanned" ? userOutcome.entries : [];
|
|
@@ -31543,7 +32415,7 @@ async function discoverSkills(options = {}) {
|
|
|
31543
32415
|
}
|
|
31544
32416
|
|
|
31545
32417
|
// ../kernel/src/skills/registry.ts
|
|
31546
|
-
import
|
|
32418
|
+
import path27 from "node:path";
|
|
31547
32419
|
function errCode2(err) {
|
|
31548
32420
|
if (err !== null && typeof err === "object" && "code" in err) {
|
|
31549
32421
|
const code = err.code;
|
|
@@ -31606,7 +32478,7 @@ var SkillRegistry = class {
|
|
|
31606
32478
|
/** 技能目录绝对路径(正文相对引用锚点;builtin 无) */
|
|
31607
32479
|
static baseDirOf(entry) {
|
|
31608
32480
|
if (entry.dir !== void 0) return entry.dir;
|
|
31609
|
-
if (entry.file !== void 0) return
|
|
32481
|
+
if (entry.file !== void 0) return path27.dirname(entry.file);
|
|
31610
32482
|
return void 0;
|
|
31611
32483
|
}
|
|
31612
32484
|
};
|
|
@@ -31787,7 +32659,7 @@ var TansrSdkError = class extends Error {
|
|
|
31787
32659
|
};
|
|
31788
32660
|
|
|
31789
32661
|
// src/define-tool.ts
|
|
31790
|
-
function specToZod(spec,
|
|
32662
|
+
function specToZod(spec, path29) {
|
|
31791
32663
|
let schema;
|
|
31792
32664
|
switch (spec.type) {
|
|
31793
32665
|
case "string":
|
|
@@ -31800,24 +32672,24 @@ function specToZod(spec, path28) {
|
|
|
31800
32672
|
schema = z46.boolean();
|
|
31801
32673
|
break;
|
|
31802
32674
|
case "array":
|
|
31803
|
-
schema = z46.array(spec.items !== void 0 ? specToZod(spec.items, `${
|
|
32675
|
+
schema = z46.array(spec.items !== void 0 ? specToZod(spec.items, `${path29}.items`) : z46.unknown());
|
|
31804
32676
|
break;
|
|
31805
32677
|
case "object":
|
|
31806
|
-
schema = spec.properties !== void 0 ? propertiesToZodObject(spec.properties,
|
|
32678
|
+
schema = spec.properties !== void 0 ? propertiesToZodObject(spec.properties, path29) : z46.record(z46.unknown());
|
|
31807
32679
|
break;
|
|
31808
32680
|
default:
|
|
31809
32681
|
throw new TansrSdkError(
|
|
31810
32682
|
"invalid_options",
|
|
31811
|
-
`defineTool: parameter "${
|
|
32683
|
+
`defineTool: parameter "${path29}" has unknown type "${String(spec.type)}"; use one of 'string' | 'number' | 'boolean' | 'array' | 'object'.`
|
|
31812
32684
|
);
|
|
31813
32685
|
}
|
|
31814
32686
|
if (spec.description !== void 0) schema = schema.describe(spec.description);
|
|
31815
32687
|
return schema;
|
|
31816
32688
|
}
|
|
31817
|
-
function propertiesToZodObject(properties,
|
|
32689
|
+
function propertiesToZodObject(properties, path29) {
|
|
31818
32690
|
const shape = {};
|
|
31819
32691
|
for (const [key2, spec] of Object.entries(properties)) {
|
|
31820
|
-
const field = specToZod(spec,
|
|
32692
|
+
const field = specToZod(spec, path29 === "" ? key2 : `${path29}.${key2}`);
|
|
31821
32693
|
shape[key2] = spec.optional === true ? field.optional() : field;
|
|
31822
32694
|
}
|
|
31823
32695
|
return z46.object(shape);
|
|
@@ -31926,7 +32798,7 @@ function defineTool(options) {
|
|
|
31926
32798
|
}
|
|
31927
32799
|
|
|
31928
32800
|
// src/skills.ts
|
|
31929
|
-
import
|
|
32801
|
+
import path28 from "node:path";
|
|
31930
32802
|
function yamlQuote(value) {
|
|
31931
32803
|
return '"' + value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r\n?/g, "\n").replace(/\n/g, "\\n").replace(/\t/g, "\\t") + '"';
|
|
31932
32804
|
}
|
|
@@ -31960,9 +32832,9 @@ function defineSkill(options) {
|
|
|
31960
32832
|
];
|
|
31961
32833
|
return { name: options.name, content: lines.join("\n") };
|
|
31962
32834
|
}
|
|
31963
|
-
var NO_DISCOVERY_ROOT =
|
|
32835
|
+
var NO_DISCOVERY_ROOT = path28.resolve(path28.sep, ".tansr-sdk-no-discovery");
|
|
31964
32836
|
function noDiscoveryFs(inner) {
|
|
31965
|
-
const inVoid = (target) =>
|
|
32837
|
+
const inVoid = (target) => path28.resolve(target).startsWith(NO_DISCOVERY_ROOT);
|
|
31966
32838
|
const enoent = (target) => {
|
|
31967
32839
|
const err = new Error(`ENOENT: no such file or directory ${target}`);
|
|
31968
32840
|
err.code = "ENOENT";
|
|
@@ -31976,7 +32848,7 @@ function noDiscoveryFs(inner) {
|
|
|
31976
32848
|
}
|
|
31977
32849
|
async function assembleSdkSkills(options) {
|
|
31978
32850
|
const fs3 = noDiscoveryFs(options.fs ?? defaultSkillsFileSystem());
|
|
31979
|
-
const dirs = (options.dirs ?? []).map((dir) =>
|
|
32851
|
+
const dirs = (options.dirs ?? []).map((dir) => path28.resolve(dir));
|
|
31980
32852
|
for (const dir of dirs) {
|
|
31981
32853
|
if (!await fs3.directoryExists(dir)) {
|
|
31982
32854
|
throw new TansrSdkError(
|
|
@@ -34175,7 +35047,7 @@ var OpenAIResponsesAdapter = class {
|
|
|
34175
35047
|
};
|
|
34176
35048
|
|
|
34177
35049
|
// ../providers/src/retry/attempt-observer.ts
|
|
34178
|
-
import { createHash as
|
|
35050
|
+
import { createHash as createHash15 } from "node:crypto";
|
|
34179
35051
|
var PROVIDER_CALL_META_FIELD = "callMeta";
|
|
34180
35052
|
function providerCallMetaOf(options) {
|
|
34181
35053
|
if (options === void 0) return void 0;
|
|
@@ -34204,12 +35076,12 @@ function endpointKeyOf(baseUrl) {
|
|
|
34204
35076
|
let normalized;
|
|
34205
35077
|
try {
|
|
34206
35078
|
const url = new URL(baseUrl);
|
|
34207
|
-
const
|
|
34208
|
-
normalized = `${url.protocol}//${url.host}${
|
|
35079
|
+
const path29 = url.pathname.replace(/\/+$/, "");
|
|
35080
|
+
normalized = `${url.protocol}//${url.host}${path29}`.toLowerCase();
|
|
34209
35081
|
} catch {
|
|
34210
35082
|
normalized = baseUrl.trim().replace(/\/+$/, "").toLowerCase();
|
|
34211
35083
|
}
|
|
34212
|
-
return
|
|
35084
|
+
return createHash15("sha256").update(normalized, "utf8").digest("hex").slice(0, 16);
|
|
34213
35085
|
}
|
|
34214
35086
|
function safeAttemptCall(fn) {
|
|
34215
35087
|
if (fn === void 0) return;
|
|
@@ -34362,16 +35234,16 @@ function sendableTwpReasoningContent(features) {
|
|
|
34362
35234
|
}
|
|
34363
35235
|
|
|
34364
35236
|
// ../providers/src/twp/signing.ts
|
|
34365
|
-
import { createHash as
|
|
35237
|
+
import { createHash as createHash16, createHmac, randomBytes as randomBytes4 } from "node:crypto";
|
|
34366
35238
|
var TWP_SIGNING_ALGORITHM = "TWP1-HMAC-SHA256";
|
|
34367
35239
|
var TWP_NONCE_LENGTH = 24;
|
|
34368
35240
|
function deriveTwpSigningKey(secretKey) {
|
|
34369
|
-
return
|
|
35241
|
+
return createHash16("sha256").update(secretKey, "utf8").digest();
|
|
34370
35242
|
}
|
|
34371
35243
|
function sha256Hex2(data) {
|
|
34372
|
-
return
|
|
35244
|
+
return createHash16("sha256").update(typeof data === "string" ? Buffer.from(data, "utf8") : data).digest("hex");
|
|
34373
35245
|
}
|
|
34374
|
-
function generateTwpNonce(random =
|
|
35246
|
+
function generateTwpNonce(random = randomBytes4) {
|
|
34375
35247
|
return random(TWP_NONCE_LENGTH / 2).toString("hex");
|
|
34376
35248
|
}
|
|
34377
35249
|
function buildTwpSigningString(input) {
|
|
@@ -35388,9 +36260,9 @@ var MissingApiKeyError = class extends ProviderRegistryError {
|
|
|
35388
36260
|
var CyclicFallbackError = class extends ProviderRegistryError {
|
|
35389
36261
|
/** 成环路径,末项为再次出现的别名,如 ['main','fast','main'] */
|
|
35390
36262
|
path;
|
|
35391
|
-
constructor(
|
|
35392
|
-
super("cyclic_fallback", `Cyclic fallback chain: ${
|
|
35393
|
-
this.path =
|
|
36263
|
+
constructor(path29) {
|
|
36264
|
+
super("cyclic_fallback", `Cyclic fallback chain: ${path29.join(" -> ")}`);
|
|
36265
|
+
this.path = path29;
|
|
35394
36266
|
}
|
|
35395
36267
|
};
|
|
35396
36268
|
|
|
@@ -35878,9 +36750,9 @@ function pushUnique(out, seen, resolved) {
|
|
|
35878
36750
|
seen.add(dedupeKey);
|
|
35879
36751
|
out.push(resolved);
|
|
35880
36752
|
}
|
|
35881
|
-
function expandInto(config, alias,
|
|
35882
|
-
if (
|
|
35883
|
-
const nextPath = [...
|
|
36753
|
+
function expandInto(config, alias, path29, out, seen) {
|
|
36754
|
+
if (path29.includes(alias)) throw new CyclicFallbackError([...path29, alias]);
|
|
36755
|
+
const nextPath = [...path29, alias];
|
|
35884
36756
|
pushUnique(out, seen, resolveAliasOrRef(config, alias));
|
|
35885
36757
|
for (const entry of config.fallbacks[alias] ?? []) {
|
|
35886
36758
|
if (entry.includes("/")) {
|
|
@@ -37973,7 +38845,7 @@ async function assembleTooling(options) {
|
|
|
37973
38845
|
}
|
|
37974
38846
|
|
|
37975
38847
|
// src/adjudication/prompts.ts
|
|
37976
|
-
import { createHash as
|
|
38848
|
+
import { createHash as createHash17 } from "node:crypto";
|
|
37977
38849
|
var STAGE1_MAX_TOKENS = 64;
|
|
37978
38850
|
var STAGE2_MAX_TOKENS = 1024;
|
|
37979
38851
|
var TWO_STAGE_TIMEOUT_MS = 15e3;
|
|
@@ -38070,7 +38942,7 @@ function serializeTimeoutPolicy(policy) {
|
|
|
38070
38942
|
].join(";");
|
|
38071
38943
|
}
|
|
38072
38944
|
function makeProtocolFingerprint(components) {
|
|
38073
|
-
return
|
|
38945
|
+
return createHash17("sha256").update(components.join("\n\0")).digest("hex").slice(0, 16);
|
|
38074
38946
|
}
|
|
38075
38947
|
var PAYLOAD_VERSION_COMPONENT = "payload:v4-engine-facts-shell";
|
|
38076
38948
|
var SETTLE_COMPONENT = "settle:verdict-first";
|
|
@@ -41137,7 +42009,7 @@ async function createSession(options = {}) {
|
|
|
41137
42009
|
|
|
41138
42010
|
// src/sessions/file-store.ts
|
|
41139
42011
|
import { randomUUID as randomUUID14 } from "node:crypto";
|
|
41140
|
-
import { mkdir as
|
|
42012
|
+
import { mkdir as mkdir9, readFile as readFile11, rm as rm10, stat as stat9 } from "node:fs/promises";
|
|
41141
42013
|
var LOCAL_END_USER_KEY = "local";
|
|
41142
42014
|
var MESSAGE_KINDS2 = /* @__PURE__ */ new Set([
|
|
41143
42015
|
"user_prompt",
|
|
@@ -41160,7 +42032,7 @@ function sessionTitleOf(messages) {
|
|
|
41160
42032
|
}
|
|
41161
42033
|
async function readRawMeta(sessionDir) {
|
|
41162
42034
|
try {
|
|
41163
|
-
const parsed = JSON.parse(await
|
|
42035
|
+
const parsed = JSON.parse(await readFile11(metaFilePath(sessionDir), "utf8"));
|
|
41164
42036
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
41165
42037
|
return parsed;
|
|
41166
42038
|
} catch {
|
|
@@ -41295,7 +42167,7 @@ function createFileSessionStore(options) {
|
|
|
41295
42167
|
cursors.set(sessionId, tail);
|
|
41296
42168
|
}
|
|
41297
42169
|
async function withMtime(sessionDir, cursor) {
|
|
41298
|
-
const { mtimeMs } = await
|
|
42170
|
+
const { mtimeMs } = await stat9(journalFilePath(sessionDir));
|
|
41299
42171
|
return { ...cursor, mtimeMs };
|
|
41300
42172
|
}
|
|
41301
42173
|
async function patchRawMeta(sessionDir, mutate) {
|
|
@@ -41364,20 +42236,20 @@ function createFileSessionStore(options) {
|
|
|
41364
42236
|
if (await readRawMeta(sessionDir) !== null) return false;
|
|
41365
42237
|
let existed = true;
|
|
41366
42238
|
try {
|
|
41367
|
-
await
|
|
42239
|
+
await stat9(sessionDir);
|
|
41368
42240
|
} catch {
|
|
41369
42241
|
existed = false;
|
|
41370
42242
|
}
|
|
41371
|
-
await
|
|
42243
|
+
await mkdir9(sessionDir, { recursive: true });
|
|
41372
42244
|
let restored;
|
|
41373
42245
|
try {
|
|
41374
42246
|
restored = await tier.restore({ sessionId, endUserKey: LOCAL_END_USER_KEY, sessionDir });
|
|
41375
42247
|
} catch (err) {
|
|
41376
|
-
if (!existed) await
|
|
42248
|
+
if (!existed) await rm10(sessionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
41377
42249
|
throw readError(sessionId, err);
|
|
41378
42250
|
}
|
|
41379
42251
|
if (restored === null) {
|
|
41380
|
-
if (!existed) await
|
|
42252
|
+
if (!existed) await rm10(sessionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
41381
42253
|
return false;
|
|
41382
42254
|
}
|
|
41383
42255
|
const cold = restored.meta;
|
|
@@ -41611,7 +42483,7 @@ function createFileSessionStore(options) {
|
|
|
41611
42483
|
tier.unbind(sessionDir);
|
|
41612
42484
|
await tier.remove({ sessionId, endUserKey: LOCAL_END_USER_KEY });
|
|
41613
42485
|
}
|
|
41614
|
-
await
|
|
42486
|
+
await rm10(sessionDir, { recursive: true, force: true });
|
|
41615
42487
|
await index.remove(sessionId);
|
|
41616
42488
|
});
|
|
41617
42489
|
},
|
|
@@ -42670,7 +43542,7 @@ function createNarrator(source, options) {
|
|
|
42670
43542
|
}
|
|
42671
43543
|
|
|
42672
43544
|
// src/platform/bundle-cache.ts
|
|
42673
|
-
import { createHash as
|
|
43545
|
+
import { createHash as createHash18 } from "node:crypto";
|
|
42674
43546
|
var DEFAULT_BUNDLE_CACHE_TTL_MS = 6e4;
|
|
42675
43547
|
var DEFAULT_MAX_BUNDLE_ENTRIES = 16;
|
|
42676
43548
|
var DEFAULT_MAX_FEATURE_ENTRIES = 1e5;
|
|
@@ -42710,7 +43582,7 @@ var Lru = class {
|
|
|
42710
43582
|
}
|
|
42711
43583
|
};
|
|
42712
43584
|
function tokenScope(token) {
|
|
42713
|
-
return
|
|
43585
|
+
return createHash18("sha256").update(token, "utf8").digest("hex");
|
|
42714
43586
|
}
|
|
42715
43587
|
function bundleKey(base, appId) {
|
|
42716
43588
|
return `${base}\0${appId ?? ""}`;
|
|
@@ -42971,9 +43843,11 @@ export {
|
|
|
42971
43843
|
createAppTokenFetch,
|
|
42972
43844
|
createBlockListClassifier,
|
|
42973
43845
|
createBundleCache,
|
|
43846
|
+
createChaosBlobStore,
|
|
42974
43847
|
createClassifierTranscriptRecorder,
|
|
42975
43848
|
createFileCheckpointStore,
|
|
42976
43849
|
createFileSessionStore,
|
|
43850
|
+
createFsBlobStore,
|
|
42977
43851
|
createImageGenTool2 as createImageGenTool,
|
|
42978
43852
|
createMcpHost,
|
|
42979
43853
|
createMemoryBlobStore,
|
|
@@ -43033,6 +43907,7 @@ export {
|
|
|
43033
43907
|
resolveStageHandle,
|
|
43034
43908
|
runAgent,
|
|
43035
43909
|
runIdleCompaction,
|
|
43910
|
+
runStorageConformance,
|
|
43036
43911
|
serializeTimeoutPolicy,
|
|
43037
43912
|
setSessionViewDelivery,
|
|
43038
43913
|
singleStageProtocolFingerprintFor,
|