@kiturone/kapibala 0.0.10 → 0.1.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.cjs +1217 -827
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -22
- package/dist/index.d.ts +82 -22
- package/dist/index.js +1217 -827
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -711,15 +711,50 @@ var import_node_path = __toESM(require("path"), 1);
|
|
|
711
711
|
|
|
712
712
|
// src/context/jsonl-lines.ts
|
|
713
713
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
714
|
-
function
|
|
714
|
+
function assertHistoryReadBudget(bytes, options) {
|
|
715
|
+
const limit = options.maxReadBytes;
|
|
716
|
+
if (limit === void 0) return;
|
|
717
|
+
if (!Number.isSafeInteger(limit) || limit <= 0)
|
|
718
|
+
throw new KapibalaError("Invalid history read budget", { code: "CONFIG_INVALID" });
|
|
719
|
+
if (bytes > limit)
|
|
720
|
+
throw new KapibalaError("History exceeds the configured read budget", {
|
|
721
|
+
code: "HISTORY_READ_LIMIT",
|
|
722
|
+
retryPolicy: "after_user_action",
|
|
723
|
+
safeMessage: "\u5386\u53F2\u6587\u4EF6\u8D85\u8FC7\u8BFB\u53D6\u9884\u7B97\uFF0C\u539F\u6587\u4EF6\u4FDD\u6301\u4E0D\u53D8"
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
function readHistoryBytes(file, options = {}) {
|
|
727
|
+
const fd = import_node_fs.default.openSync(file, "r");
|
|
728
|
+
try {
|
|
729
|
+
assertHistoryReadBudget(import_node_fs.default.fstatSync(fd).size, options);
|
|
730
|
+
const chunks = [];
|
|
731
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
732
|
+
let total = 0;
|
|
733
|
+
while (true) {
|
|
734
|
+
const length = import_node_fs.default.readSync(fd, buffer, 0, buffer.length, null);
|
|
735
|
+
if (!length) break;
|
|
736
|
+
total += length;
|
|
737
|
+
assertHistoryReadBudget(total, options);
|
|
738
|
+
chunks.push(Buffer.from(buffer.subarray(0, length)));
|
|
739
|
+
}
|
|
740
|
+
return Buffer.concat(chunks, total);
|
|
741
|
+
} finally {
|
|
742
|
+
import_node_fs.default.closeSync(fd);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function* readJsonlLines(file, options = {}) {
|
|
715
746
|
const fd = import_node_fs.default.openSync(file, "r");
|
|
716
747
|
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
717
748
|
let pending = Buffer.alloc(0);
|
|
718
749
|
let offset = 0;
|
|
750
|
+
let total = 0;
|
|
719
751
|
try {
|
|
752
|
+
assertHistoryReadBudget(import_node_fs.default.fstatSync(fd).size, options);
|
|
720
753
|
while (true) {
|
|
721
754
|
const length = import_node_fs.default.readSync(fd, buffer, 0, buffer.length, null);
|
|
722
755
|
if (!length) break;
|
|
756
|
+
total += length;
|
|
757
|
+
assertHistoryReadBudget(total, options);
|
|
723
758
|
pending = Buffer.concat([pending, buffer.subarray(0, length)]);
|
|
724
759
|
let start = 0;
|
|
725
760
|
let end = pending.indexOf(10, start);
|
|
@@ -753,7 +788,7 @@ var SESSION_SCHEMA_VERSION_V2 = 2;
|
|
|
753
788
|
var SESSION_SCHEMA_VERSION = SESSION_SCHEMA_VERSION_V2;
|
|
754
789
|
var MAIN_BRANCH_ID = "main";
|
|
755
790
|
function isGlobalControlRecordType(type) {
|
|
756
|
-
return type === "header" || type === "branch_created" || type === "branch_selected" || type === "title" || type === "archive_state" || type === "usage" || type === "legacy_import";
|
|
791
|
+
return type === "header" || type === "branch_created" || type === "branch_selected" || type === "title" || type === "archive_state" || type === "pin_state" || type === "usage" || type === "legacy_import";
|
|
757
792
|
}
|
|
758
793
|
var types = /* @__PURE__ */ new Set([
|
|
759
794
|
"header",
|
|
@@ -772,7 +807,8 @@ var types = /* @__PURE__ */ new Set([
|
|
|
772
807
|
"branch_created",
|
|
773
808
|
"branch_selected",
|
|
774
809
|
"branch_summary",
|
|
775
|
-
"archive_state"
|
|
810
|
+
"archive_state",
|
|
811
|
+
"pin_state"
|
|
776
812
|
]);
|
|
777
813
|
function supportsSessionState(store) {
|
|
778
814
|
return "appendRecord" in store && typeof store.appendRecord === "function" && "loadState" in store && typeof store.loadState === "function";
|
|
@@ -855,6 +891,13 @@ function assertClosedToolTransaction(records, targetRecordId) {
|
|
|
855
891
|
}
|
|
856
892
|
}
|
|
857
893
|
var SessionStore = class _SessionStore {
|
|
894
|
+
constructor(filePath, schemaVersion = SESSION_SCHEMA_VERSION_V2, readOptions = {}) {
|
|
895
|
+
this.readOptions = readOptions;
|
|
896
|
+
assertHistoryReadBudget(0, readOptions);
|
|
897
|
+
this.filePath = import_node_path.default.resolve(filePath);
|
|
898
|
+
this.schemaVersion = schemaVersion;
|
|
899
|
+
}
|
|
900
|
+
readOptions;
|
|
858
901
|
filePath;
|
|
859
902
|
queue = Promise.resolve();
|
|
860
903
|
records = [];
|
|
@@ -864,10 +907,6 @@ var SessionStore = class _SessionStore {
|
|
|
864
907
|
schemaVersion = SESSION_SCHEMA_VERSION_V2;
|
|
865
908
|
activeBranchId = "main";
|
|
866
909
|
branchLeaves = /* @__PURE__ */ new Map();
|
|
867
|
-
constructor(filePath, schemaVersion = SESSION_SCHEMA_VERSION_V2) {
|
|
868
|
-
this.filePath = import_node_path.default.resolve(filePath);
|
|
869
|
-
this.schemaVersion = schemaVersion;
|
|
870
|
-
}
|
|
871
910
|
/** 获取当前会话文件使用的 Schema 版本(1 或 2)。 */
|
|
872
911
|
getSchemaVersion() {
|
|
873
912
|
return this.schemaVersion;
|
|
@@ -890,8 +929,8 @@ var SessionStore = class _SessionStore {
|
|
|
890
929
|
this.onDiagnostic = onDiagnostic;
|
|
891
930
|
}
|
|
892
931
|
/** 以独占创建方式安装新头部(默认生成 Schema v2 格式),已有文件明确失败,绝不覆盖其它会话。 */
|
|
893
|
-
static async create(filePath, header) {
|
|
894
|
-
const store = new _SessionStore(filePath, SESSION_SCHEMA_VERSION_V2);
|
|
932
|
+
static async create(filePath, header, options = {}) {
|
|
933
|
+
const store = new _SessionStore(filePath, SESSION_SCHEMA_VERSION_V2, { ...options });
|
|
895
934
|
import_node_fs2.default.mkdirSync(import_node_path.default.dirname(store.filePath), { recursive: true });
|
|
896
935
|
const record = store.makeRecord("header", {
|
|
897
936
|
...header,
|
|
@@ -912,8 +951,8 @@ var SessionStore = class _SessionStore {
|
|
|
912
951
|
return store;
|
|
913
952
|
}
|
|
914
953
|
/** 打开并验证已有会话;尾部残片先保存到隔离文件,中部损坏与未知版本拒绝恢复。 */
|
|
915
|
-
static async open(filePath) {
|
|
916
|
-
const store = new _SessionStore(filePath);
|
|
954
|
+
static async open(filePath, options = {}) {
|
|
955
|
+
const store = new _SessionStore(filePath, SESSION_SCHEMA_VERSION_V2, { ...options });
|
|
917
956
|
store.records = store.readFile(true);
|
|
918
957
|
store.refreshStamp();
|
|
919
958
|
return store;
|
|
@@ -1007,6 +1046,24 @@ var SessionStore = class _SessionStore {
|
|
|
1007
1046
|
}
|
|
1008
1047
|
return false;
|
|
1009
1048
|
}
|
|
1049
|
+
/**
|
|
1050
|
+
* 追加会话置顶控制记录。
|
|
1051
|
+
*
|
|
1052
|
+
* 约束:
|
|
1053
|
+
* - 置顶状态记录为全局控制记录(parentId 为 null,不推进分支上下文叶子);
|
|
1054
|
+
* - payload 结构为 { pinned: boolean },最后一次写入生效;
|
|
1055
|
+
* - 置顶只影响列表排序,不改变正文活动时间,因此不会让会话挤进「最近」;
|
|
1056
|
+
* - v1 历史会话禁止变更置顶状态。
|
|
1057
|
+
*/
|
|
1058
|
+
async setPinnedState(pinned) {
|
|
1059
|
+
if (this.isReadOnly()) {
|
|
1060
|
+
throw new KapibalaError("Cannot modify pin state of legacy v1 session", {
|
|
1061
|
+
code: "LEGACY_SESSION_READ_ONLY",
|
|
1062
|
+
safeMessage: "\u65E0\u6CD5\u53D8\u66F4\u53EA\u8BFB\u5386\u53F2\u4F1A\u8BDD\u7684\u7F6E\u9876\u72B6\u6001"
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
return this.appendRecord("pin_state", { pinned });
|
|
1066
|
+
}
|
|
1010
1067
|
/**
|
|
1011
1068
|
* 获取当前会话中所有已知分支的元数据与状态列表。
|
|
1012
1069
|
*/
|
|
@@ -1298,14 +1355,15 @@ ${summaryText}` }]
|
|
|
1298
1355
|
}
|
|
1299
1356
|
/** 校验每个完整记录,只有最后非空行的 JSON 残片可隔离,语义损坏绝不作为残片吞掉。 */
|
|
1300
1357
|
readFile(recover) {
|
|
1301
|
-
const iterator = readJsonlLines(this.filePath);
|
|
1358
|
+
const iterator = readJsonlLines(this.filePath, this.readOptions);
|
|
1302
1359
|
let lastTerminated = true;
|
|
1303
1360
|
let blankSeen = false;
|
|
1304
1361
|
const records = [];
|
|
1305
1362
|
const identities = /* @__PURE__ */ new Set();
|
|
1306
1363
|
const messages = /* @__PURE__ */ new Set();
|
|
1307
|
-
|
|
1308
|
-
|
|
1364
|
+
const branchLeaves = /* @__PURE__ */ new Map();
|
|
1365
|
+
let activeBranchId = MAIN_BRANCH_ID;
|
|
1366
|
+
let schemaVersion = this.schemaVersion;
|
|
1309
1367
|
let detectedSchemaVersion = null;
|
|
1310
1368
|
let i = -1;
|
|
1311
1369
|
for (const line of iterator) {
|
|
@@ -1320,7 +1378,7 @@ ${summaryText}` }]
|
|
|
1320
1378
|
try {
|
|
1321
1379
|
record = JSON.parse(line.text);
|
|
1322
1380
|
} catch {
|
|
1323
|
-
if (!recover ||
|
|
1381
|
+
if (!recover || schemaVersion === SESSION_SCHEMA_VERSION_V1 || !records.length || Array.from(iterator).some((next) => next.text.trim()))
|
|
1324
1382
|
throw new Error(`Session corrupt at line ${i + 1}`);
|
|
1325
1383
|
import_node_fs2.default.writeFileSync(
|
|
1326
1384
|
`${this.filePath}.corrupt-${createMessageId()}`,
|
|
@@ -1344,7 +1402,7 @@ ${summaryText}` }]
|
|
|
1344
1402
|
throw new Error("Unsupported session schema version");
|
|
1345
1403
|
}
|
|
1346
1404
|
detectedSchemaVersion = record.schemaVersion;
|
|
1347
|
-
|
|
1405
|
+
schemaVersion = detectedSchemaVersion;
|
|
1348
1406
|
} else if (record.schemaVersion !== detectedSchemaVersion) {
|
|
1349
1407
|
throw new Error(`Session corrupt schema version mismatch at line ${i + 1}`);
|
|
1350
1408
|
}
|
|
@@ -1361,11 +1419,11 @@ ${summaryText}` }]
|
|
|
1361
1419
|
} else if (record.type === "header") {
|
|
1362
1420
|
throw new Error("Session corrupt: repeated header");
|
|
1363
1421
|
}
|
|
1364
|
-
if (
|
|
1422
|
+
if (schemaVersion === SESSION_SCHEMA_VERSION_V1) {
|
|
1365
1423
|
if (record.parentId !== (records.at(-1)?.recordId ?? null)) {
|
|
1366
1424
|
throw new Error(`Session corrupt parent at line ${i + 1}`);
|
|
1367
1425
|
}
|
|
1368
|
-
|
|
1426
|
+
branchLeaves.set(MAIN_BRANCH_ID, record.recordId);
|
|
1369
1427
|
} else {
|
|
1370
1428
|
if (typeof record.seq !== "number" || record.seq !== records.length) {
|
|
1371
1429
|
throw new Error(`Session corrupt seq at line ${i + 1}`);
|
|
@@ -1379,37 +1437,37 @@ ${summaryText}` }]
|
|
|
1379
1437
|
throw new Error(`Session corrupt control record parent at line ${i + 1}`);
|
|
1380
1438
|
}
|
|
1381
1439
|
if (record.type === "header") {
|
|
1382
|
-
|
|
1383
|
-
|
|
1440
|
+
branchLeaves.set(MAIN_BRANCH_ID, record.recordId);
|
|
1441
|
+
activeBranchId = MAIN_BRANCH_ID;
|
|
1384
1442
|
} else if (record.type === "branch_created") {
|
|
1385
1443
|
const bId = record.payload.branchId;
|
|
1386
1444
|
const baseId = record.payload.baseRecordId;
|
|
1387
1445
|
if (typeof bId !== "string" || !bId) {
|
|
1388
1446
|
throw new Error(`Session corrupt branch_created branchId at line ${i + 1}`);
|
|
1389
1447
|
}
|
|
1390
|
-
if (
|
|
1448
|
+
if (branchLeaves.has(bId)) {
|
|
1391
1449
|
throw new Error(`Duplicate branch ID: ${bId}`);
|
|
1392
1450
|
}
|
|
1393
1451
|
if (typeof baseId !== "string" || !identities.has(baseId)) {
|
|
1394
1452
|
throw new Error(`Session corrupt branch_created baseRecordId at line ${i + 1}`);
|
|
1395
1453
|
}
|
|
1396
|
-
|
|
1454
|
+
branchLeaves.set(bId, baseId);
|
|
1397
1455
|
} else if (record.type === "branch_selected") {
|
|
1398
1456
|
const bId = record.payload.branchId;
|
|
1399
|
-
if (typeof bId !== "string" || !
|
|
1457
|
+
if (typeof bId !== "string" || !branchLeaves.has(bId)) {
|
|
1400
1458
|
throw new Error(`Selected branch not found: ${bId}`);
|
|
1401
1459
|
}
|
|
1402
|
-
|
|
1460
|
+
activeBranchId = bId;
|
|
1403
1461
|
}
|
|
1404
1462
|
} else {
|
|
1405
1463
|
const bId = record.branchId;
|
|
1406
|
-
if (typeof bId !== "string" || !
|
|
1464
|
+
if (typeof bId !== "string" || !branchLeaves.has(bId)) {
|
|
1407
1465
|
throw new Error(`Session corrupt branchId at line ${i + 1}`);
|
|
1408
1466
|
}
|
|
1409
1467
|
if (typeof record.parentId !== "string" || !identities.has(record.parentId)) {
|
|
1410
1468
|
throw new Error(`Session corrupt parent at line ${i + 1}`);
|
|
1411
1469
|
}
|
|
1412
|
-
|
|
1470
|
+
branchLeaves.set(bId, record.recordId);
|
|
1413
1471
|
}
|
|
1414
1472
|
}
|
|
1415
1473
|
identities.add(record.recordId);
|
|
@@ -1428,7 +1486,7 @@ ${summaryText}` }]
|
|
|
1428
1486
|
if (!records.length) {
|
|
1429
1487
|
throw new Error("Session corrupt header");
|
|
1430
1488
|
}
|
|
1431
|
-
if (recover && !lastTerminated && records.length === i + 1 &&
|
|
1489
|
+
if (recover && !lastTerminated && records.length === i + 1 && schemaVersion !== SESSION_SCHEMA_VERSION_V1) {
|
|
1432
1490
|
const fd = import_node_fs2.default.openSync(this.filePath, "a");
|
|
1433
1491
|
try {
|
|
1434
1492
|
import_node_fs2.default.writeFileSync(fd, "\n");
|
|
@@ -1437,6 +1495,9 @@ ${summaryText}` }]
|
|
|
1437
1495
|
import_node_fs2.default.closeSync(fd);
|
|
1438
1496
|
}
|
|
1439
1497
|
}
|
|
1498
|
+
this.branchLeaves = branchLeaves;
|
|
1499
|
+
this.activeBranchId = activeBranchId;
|
|
1500
|
+
this.schemaVersion = schemaVersion;
|
|
1440
1501
|
return records;
|
|
1441
1502
|
}
|
|
1442
1503
|
};
|
|
@@ -1724,18 +1785,21 @@ function resolveWorkspaceContext(input) {
|
|
|
1724
1785
|
function normalizedProjectRoot(root) {
|
|
1725
1786
|
return process.platform === "win32" ? root.replaceAll("\\", "/").toLowerCase() : root;
|
|
1726
1787
|
}
|
|
1727
|
-
function resolveSessionProject(cwd) {
|
|
1788
|
+
function resolveSessionProject(cwd, mode = "git") {
|
|
1728
1789
|
const realCwd = toNativePhysicalPath(cwd);
|
|
1729
1790
|
let root = realCwd;
|
|
1730
1791
|
let gitBranch;
|
|
1731
1792
|
let gitWorktree = false;
|
|
1732
1793
|
try {
|
|
1733
|
-
|
|
1794
|
+
const gitRoot = toNativePhysicalPath(
|
|
1734
1795
|
(0, import_node_child_process.execFileSync)("git", ["-C", realCwd, "rev-parse", "--show-toplevel"], {
|
|
1735
1796
|
encoding: "utf8",
|
|
1736
1797
|
stdio: ["ignore", "pipe", "ignore"]
|
|
1737
1798
|
}).trim()
|
|
1738
1799
|
);
|
|
1800
|
+
if (mode === "git") root = gitRoot;
|
|
1801
|
+
if (normalizedProjectRoot(gitRoot) !== normalizedProjectRoot(root))
|
|
1802
|
+
return { projectRoot: root, cwd: realCwd, projectKey: computeProjectKey(root) };
|
|
1739
1803
|
gitWorktree = true;
|
|
1740
1804
|
gitBranch = (0, import_node_child_process.execFileSync)("git", ["-C", realCwd, "symbolic-ref", "--short", "-q", "HEAD"], {
|
|
1741
1805
|
encoding: "utf8",
|
|
@@ -1743,11 +1807,10 @@ function resolveSessionProject(cwd) {
|
|
|
1743
1807
|
}).trim() || void 0;
|
|
1744
1808
|
} catch {
|
|
1745
1809
|
}
|
|
1746
|
-
const normalized = normalizedProjectRoot(root);
|
|
1747
1810
|
return {
|
|
1748
1811
|
projectRoot: root,
|
|
1749
1812
|
cwd: realCwd,
|
|
1750
|
-
projectKey: (
|
|
1813
|
+
projectKey: computeProjectKey(root),
|
|
1751
1814
|
gitBranch,
|
|
1752
1815
|
gitWorktree
|
|
1753
1816
|
};
|
|
@@ -1765,7 +1828,10 @@ var SessionManager = class {
|
|
|
1765
1828
|
directory;
|
|
1766
1829
|
handles = /* @__PURE__ */ new Map();
|
|
1767
1830
|
diagnostic;
|
|
1831
|
+
readOptions;
|
|
1768
1832
|
constructor(options = {}) {
|
|
1833
|
+
this.readOptions = { maxReadBytes: options.maxReadBytes };
|
|
1834
|
+
assertHistoryReadBudget(0, this.readOptions);
|
|
1769
1835
|
const rootCandidate = options.workspaceRoot ?? options.projectRoot ?? options.cwd;
|
|
1770
1836
|
if (!rootCandidate) {
|
|
1771
1837
|
throw new KapibalaError("Workspace root is required for SessionManager", {
|
|
@@ -1773,7 +1839,7 @@ var SessionManager = class {
|
|
|
1773
1839
|
safeMessage: "\u5FC5\u987B\u663E\u5F0F\u6307\u5B9A\u5DE5\u4F5C\u533A\u6839\u8DEF\u5F84\u6216\u9879\u76EE\u76EE\u5F55"
|
|
1774
1840
|
});
|
|
1775
1841
|
}
|
|
1776
|
-
this.project = resolveSessionProject(rootCandidate);
|
|
1842
|
+
this.project = resolveSessionProject(rootCandidate, options.projectRootMode);
|
|
1777
1843
|
this.directory = import_node_path4.default.join(
|
|
1778
1844
|
options.homeDirectory ?? import_node_os.default.homedir(),
|
|
1779
1845
|
".kapibala",
|
|
@@ -1789,12 +1855,16 @@ var SessionManager = class {
|
|
|
1789
1855
|
const file = import_node_path4.default.join(this.directory, `${Date.now()}_${conversationId}.jsonl`);
|
|
1790
1856
|
const lock = await SessionLock.acquire(this.lockPath(conversationId));
|
|
1791
1857
|
try {
|
|
1792
|
-
const store = await SessionStore.create(
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1858
|
+
const store = await SessionStore.create(
|
|
1859
|
+
file,
|
|
1860
|
+
{
|
|
1861
|
+
conversationId,
|
|
1862
|
+
projectRoot: this.project.projectRoot,
|
|
1863
|
+
initialCwd: this.project.cwd,
|
|
1864
|
+
gitBranch: this.project.gitBranch
|
|
1865
|
+
},
|
|
1866
|
+
this.readOptions
|
|
1867
|
+
);
|
|
1798
1868
|
return await this.attach(store, lock);
|
|
1799
1869
|
} catch (error) {
|
|
1800
1870
|
await lock.release();
|
|
@@ -1803,19 +1873,25 @@ var SessionManager = class {
|
|
|
1803
1873
|
}
|
|
1804
1874
|
/**
|
|
1805
1875
|
* 按正文活动排序分页;有效 sidecar 只读元数据,不读取完整 JSONL。默认排除已归档会话。
|
|
1806
|
-
* pageSize 由调用方显式给出时必须在 1–100
|
|
1876
|
+
* pageSize 由调用方显式给出时必须在 1–100 之间;filter 在分页前筛选元数据,调用方须自行处理敏感信息。
|
|
1877
|
+
* 置顶项整体前置、组内仍按活动时间排序;排序只作用于本方法的返回值,不影响 continueRecent。
|
|
1807
1878
|
*/
|
|
1808
1879
|
async list(page = 1, options) {
|
|
1809
1880
|
const pageSize = options?.pageSize ?? 20;
|
|
1810
1881
|
if (!Number.isSafeInteger(page) || page < 1 || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100)
|
|
1811
1882
|
throw createSessionError("HISTORY_PAGE_INVALID", "History page must be a positive integer");
|
|
1812
1883
|
const all = await this.allMetadata();
|
|
1813
|
-
const
|
|
1884
|
+
const visible = options?.includeArchived ? all : all.filter((m) => !m.archived);
|
|
1885
|
+
const filtered = options?.filter ? visible.filter(options.filter) : visible;
|
|
1886
|
+
const ordered = [
|
|
1887
|
+
...filtered.filter((item) => item.pinned),
|
|
1888
|
+
...filtered.filter((item) => !item.pinned)
|
|
1889
|
+
];
|
|
1814
1890
|
return {
|
|
1815
|
-
items:
|
|
1816
|
-
total:
|
|
1891
|
+
items: ordered.slice((page - 1) * pageSize, page * pageSize),
|
|
1892
|
+
total: ordered.length,
|
|
1817
1893
|
page,
|
|
1818
|
-
pages: Math.max(1, Math.ceil(
|
|
1894
|
+
pages: Math.max(1, Math.ceil(ordered.length / pageSize))
|
|
1819
1895
|
};
|
|
1820
1896
|
}
|
|
1821
1897
|
/** 解析完整 ID 或唯一前缀对应的会话文件与 ID。 */
|
|
@@ -1834,60 +1910,109 @@ var SessionManager = class {
|
|
|
1834
1910
|
return matches[0];
|
|
1835
1911
|
}
|
|
1836
1912
|
/**
|
|
1837
|
-
*
|
|
1913
|
+
* 归档、恢复、重命名与置顶都向正文追加一条全局控制记录,共用同一套前置约束。
|
|
1838
1914
|
*
|
|
1839
1915
|
* 约束:
|
|
1840
1916
|
* - 必须取得会话独占锁;
|
|
1841
|
-
* -
|
|
1842
|
-
* -
|
|
1917
|
+
* - 若会话当前正处于活动打开状态,拒绝并抛出 SESSION_IN_USE(调用方先关闭句柄);
|
|
1918
|
+
* - v1 只读历史不能变更,明确以 LEGACY_SESSION_READ_ONLY 失败而不是静默忽略;
|
|
1919
|
+
* - 写入后按正文重建 sidecar,状态只以正文记录为准。
|
|
1843
1920
|
*/
|
|
1844
|
-
async
|
|
1921
|
+
async appendControlRecord(id, subject, append) {
|
|
1845
1922
|
const { fileName, conversationId } = this.resolveSession(id);
|
|
1846
1923
|
if (this.handles.has(conversationId)) {
|
|
1847
|
-
throw new KapibalaError("Session is currently active or in use
|
|
1924
|
+
throw new KapibalaError("Session is currently active or in use", {
|
|
1848
1925
|
code: "SESSION_IN_USE",
|
|
1849
|
-
safeMessage:
|
|
1926
|
+
safeMessage: `\u4F1A\u8BDD\u6B63\u5728\u4F7F\u7528\u4E2D\uFF0C\u65E0\u6CD5${subject}`
|
|
1850
1927
|
});
|
|
1851
1928
|
}
|
|
1852
1929
|
const targetFile = import_node_path4.default.join(this.directory, fileName);
|
|
1853
1930
|
const lock = await SessionLock.acquire(this.lockPath(conversationId));
|
|
1854
1931
|
try {
|
|
1855
|
-
const store = await SessionStore.open(targetFile);
|
|
1932
|
+
const store = await SessionStore.open(targetFile, this.readOptions);
|
|
1856
1933
|
if (store.isReadOnly()) {
|
|
1857
|
-
throw new KapibalaError("Cannot
|
|
1934
|
+
throw new KapibalaError("Cannot modify legacy v1 session state", {
|
|
1858
1935
|
code: "LEGACY_SESSION_READ_ONLY",
|
|
1859
|
-
safeMessage:
|
|
1936
|
+
safeMessage: `\u65E0\u6CD5\u53D8\u66F4\u53EA\u8BFB\u5386\u53F2\u4F1A\u8BDD\u7684${subject}\u72B6\u6001`
|
|
1860
1937
|
});
|
|
1861
1938
|
}
|
|
1862
|
-
await store
|
|
1863
|
-
|
|
1864
|
-
const meta = this.buildMetadata(targetFile, records);
|
|
1865
|
-
meta.archived = true;
|
|
1866
|
-
this.writeMetadata(targetFile, meta);
|
|
1939
|
+
await append(store);
|
|
1940
|
+
this.writeMetadata(targetFile, this.buildMetadata(targetFile, await store.readRecords()));
|
|
1867
1941
|
} finally {
|
|
1868
1942
|
await lock.release();
|
|
1869
1943
|
}
|
|
1870
1944
|
}
|
|
1871
1945
|
/**
|
|
1872
|
-
*
|
|
1946
|
+
* 逻辑归档指定会话:向正文追加 archive_state { archived: true } 并刷新 sidecar 缓存。
|
|
1947
|
+
*/
|
|
1948
|
+
async archiveSession(id) {
|
|
1949
|
+
await this.appendControlRecord(id, "\u5F52\u6863", (store) => store.setArchiveState(true));
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* 恢复已逻辑归档的会话:追加 archive_state { archived: false },正文保持不动。
|
|
1873
1953
|
*/
|
|
1874
1954
|
async restoreSession(id) {
|
|
1955
|
+
await this.appendControlRecord(id, "\u5F52\u6863", (store) => store.setArchiveState(false));
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* 单独重命名指定会话,不保留句柄:追加 title 记录后立即释放写锁。
|
|
1959
|
+
*
|
|
1960
|
+
* 与 `rename` 的分工:本方法面向「没有打开该会话」的宿主(桌面侧栏、设置页),
|
|
1961
|
+
* 因此不接受活动句柄;已打开的会话由宿主经自己的 Store 追加,避免同进程锁冲突。
|
|
1962
|
+
*/
|
|
1963
|
+
async renameSession(id, title) {
|
|
1964
|
+
const value = title.trim();
|
|
1965
|
+
if (!value) throw new Error("Session title cannot be empty");
|
|
1966
|
+
await this.appendControlRecord(
|
|
1967
|
+
id,
|
|
1968
|
+
"\u91CD\u547D\u540D",
|
|
1969
|
+
(store) => store.appendRecord("title", { title: sanitizeSessionTitle(value) })
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* 切换会话置顶:追加 pin_state { pinned } 控制记录,只在列表排序时前置。
|
|
1974
|
+
*
|
|
1975
|
+
* 置顶不是「最近活动」,因此不写 run_started、也不改 lastActivityAt;
|
|
1976
|
+
* 关闭置顶后会话回到按活动时间排序的原有位置。
|
|
1977
|
+
*/
|
|
1978
|
+
async pinSession(id, pinned) {
|
|
1979
|
+
await this.appendControlRecord(id, "\u7F6E\u9876", (store) => store.setPinnedState(pinned));
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* 物理删除指定会话的正文与 sidecar 缓存。
|
|
1983
|
+
*
|
|
1984
|
+
* 约束与边界:
|
|
1985
|
+
* - 删除不可逆,本方法不提供回收站或软删除,调用方必须先取得用户确认;
|
|
1986
|
+
* - 本 Manager 已持有句柄的会话拒绝(SESSION_IN_USE),避免删掉正在追加的文件;
|
|
1987
|
+
* - 其它进程持锁时以 SESSION_LOCKED 失败,不抢占他人写入权,也不做「先等一会儿」的重试;
|
|
1988
|
+
* - 只删该会话自己的 `.jsonl` 与 `.meta.json`;桶目录内其它文件(含 `*.tmp-*` 残片)不动;
|
|
1989
|
+
* - 删除前复核首条头部身份与桶归属:文件名匹配但正文身份不符的残留文件删除会放大错误;
|
|
1990
|
+
* - 缓存先删、正文后删:缓存本可重建,若正文删除失败该会话会在下次浏览时自动重建缓存。
|
|
1991
|
+
*/
|
|
1992
|
+
async deleteSession(id) {
|
|
1875
1993
|
const { fileName, conversationId } = this.resolveSession(id);
|
|
1994
|
+
if (this.handles.has(conversationId)) {
|
|
1995
|
+
throw new KapibalaError("Session is currently active or in use, cannot delete", {
|
|
1996
|
+
code: "SESSION_IN_USE",
|
|
1997
|
+
safeMessage: "\u4F1A\u8BDD\u6B63\u5728\u4F7F\u7528\u4E2D\uFF0C\u65E0\u6CD5\u5220\u9664"
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
1876
2000
|
const targetFile = import_node_path4.default.join(this.directory, fileName);
|
|
1877
2001
|
const lock = await SessionLock.acquire(this.lockPath(conversationId));
|
|
1878
2002
|
try {
|
|
1879
|
-
const
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
2003
|
+
const headerLines = this.readDiscoveryRecords(targetFile);
|
|
2004
|
+
let header;
|
|
2005
|
+
try {
|
|
2006
|
+
header = headerLines.next().value.payload;
|
|
2007
|
+
} finally {
|
|
2008
|
+
headerLines.return(void 0);
|
|
1885
2009
|
}
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
const
|
|
1889
|
-
|
|
1890
|
-
|
|
2010
|
+
if (header.conversationId !== conversationId || normalizedProjectRoot(header.projectRoot) !== normalizedProjectRoot(this.project.projectRoot))
|
|
2011
|
+
throw new Error("Foreign session identity");
|
|
2012
|
+
const metadata = this.metadataPath(targetFile);
|
|
2013
|
+
if (import_node_fs5.default.existsSync(metadata)) import_node_fs5.default.unlinkSync(metadata);
|
|
2014
|
+
import_node_fs5.default.unlinkSync(targetFile);
|
|
2015
|
+
syncSessionDirectory(this.directory);
|
|
1891
2016
|
} finally {
|
|
1892
2017
|
await lock.release();
|
|
1893
2018
|
}
|
|
@@ -1899,6 +2024,11 @@ var SessionManager = class {
|
|
|
1899
2024
|
const file = import_node_path4.default.join(this.directory, fileName);
|
|
1900
2025
|
try {
|
|
1901
2026
|
let meta = this.readMetadata(file);
|
|
2027
|
+
if (this.readOptions.maxReadBytes !== void 0 && import_node_fs5.default.statSync(file).size > this.readOptions.maxReadBytes) {
|
|
2028
|
+
meta ??= this.readOversizedMetadata(file);
|
|
2029
|
+
all.push({ ...meta, unavailableReason: "HISTORY_READ_LIMIT" });
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
1902
2032
|
if (!meta) {
|
|
1903
2033
|
const records = this.readDiscoveryRecords(file);
|
|
1904
2034
|
meta = this.buildMetadata(file, records);
|
|
@@ -1909,7 +2039,17 @@ var SessionManager = class {
|
|
|
1909
2039
|
}
|
|
1910
2040
|
}
|
|
1911
2041
|
all.push(meta);
|
|
1912
|
-
} catch {
|
|
2042
|
+
} catch (error) {
|
|
2043
|
+
if (error instanceof KapibalaError && error.code === "HISTORY_READ_LIMIT") {
|
|
2044
|
+
try {
|
|
2045
|
+
all.push({
|
|
2046
|
+
...this.readOversizedMetadata(file),
|
|
2047
|
+
unavailableReason: "HISTORY_READ_LIMIT"
|
|
2048
|
+
});
|
|
2049
|
+
continue;
|
|
2050
|
+
} catch {
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
1913
2053
|
this.report(`Cannot discover session: ${fileName}`);
|
|
1914
2054
|
}
|
|
1915
2055
|
}
|
|
@@ -1959,7 +2099,10 @@ var SessionManager = class {
|
|
|
1959
2099
|
}
|
|
1960
2100
|
if (header.conversationId !== meta.conversationId || normalizedProjectRoot(header.projectRoot) !== normalizedProjectRoot(this.project.projectRoot))
|
|
1961
2101
|
throw new Error("Foreign session identity");
|
|
1962
|
-
const store = await SessionStore.open(
|
|
2102
|
+
const store = await SessionStore.open(
|
|
2103
|
+
import_node_path4.default.join(this.directory, meta.fileName),
|
|
2104
|
+
this.readOptions
|
|
2105
|
+
);
|
|
1963
2106
|
if ((await store.loadState()).header.conversationId !== meta.conversationId)
|
|
1964
2107
|
throw new Error("Session identity mismatch");
|
|
1965
2108
|
await store.load();
|
|
@@ -1971,7 +2114,9 @@ var SessionManager = class {
|
|
|
1971
2114
|
}
|
|
1972
2115
|
/** 继续最近有正文的会话;没有历史才新建,显式打开失败绝不回退为空会话。 */
|
|
1973
2116
|
async continueRecent(options) {
|
|
1974
|
-
const latest = (await this.allMetadata()).find(
|
|
2117
|
+
const latest = (await this.allMetadata()).find(
|
|
2118
|
+
(m) => m.messageCount > 0 || m.unavailableReason
|
|
2119
|
+
);
|
|
1975
2120
|
if (latest)
|
|
1976
2121
|
return this.open(latest.conversationId, {
|
|
1977
2122
|
autoUpgradeLegacy: options?.autoUpgradeLegacy ?? true
|
|
@@ -1979,11 +2124,21 @@ var SessionManager = class {
|
|
|
1979
2124
|
this.report("No previous session with content; created a new session");
|
|
1980
2125
|
return this.create();
|
|
1981
2126
|
}
|
|
1982
|
-
/**
|
|
2127
|
+
/**
|
|
2128
|
+
* 重命名已打开或待打开的会话;标题作为独立耐久记录,标题或浏览都不改变正文活动排序。
|
|
2129
|
+
*
|
|
2130
|
+
* 未持有句柄时临时打开并在追加后释放:只改标题不该把会话一直挂在写锁上,
|
|
2131
|
+
* 否则同进程后续的归档、删除与置顶都会被自己占住的锁挡掉。
|
|
2132
|
+
*/
|
|
1983
2133
|
async rename(id, title) {
|
|
1984
2134
|
if (!title.trim()) throw new Error("Session title cannot be empty");
|
|
2135
|
+
const alreadyOpen = this.handles.has(this.resolveSession(id).conversationId);
|
|
1985
2136
|
const handle = await this.open(id, { autoUpgradeLegacy: true });
|
|
1986
|
-
|
|
2137
|
+
try {
|
|
2138
|
+
await handle.store.appendRecord("title", { title: sanitizeSessionTitle(title) });
|
|
2139
|
+
} finally {
|
|
2140
|
+
if (!alreadyOpen) await handle.release();
|
|
2141
|
+
}
|
|
1987
2142
|
}
|
|
1988
2143
|
/** 等候所有存储队列后释放句柄;宿主必须先结束 AgentSession 的工具/摘要清理。 */
|
|
1989
2144
|
async close() {
|
|
@@ -2003,7 +2158,7 @@ var SessionManager = class {
|
|
|
2003
2158
|
for (const source of sources) {
|
|
2004
2159
|
if (!import_node_fs5.default.existsSync(source)) continue;
|
|
2005
2160
|
const before = import_node_fs5.default.statSync(source);
|
|
2006
|
-
const bytes =
|
|
2161
|
+
const bytes = readHistoryBytes(source, this.readOptions);
|
|
2007
2162
|
const identity = (0, import_node_crypto6.createHash)("sha256").update(import_node_fs5.default.realpathSync.native(source)).update("\0").update(bytes).digest("hex");
|
|
2008
2163
|
const importLock = await SessionLock.acquire(
|
|
2009
2164
|
import_node_path4.default.join(this.directory, `import-${identity}.lock`)
|
|
@@ -2029,19 +2184,23 @@ var SessionManager = class {
|
|
|
2029
2184
|
const id = createMessageId();
|
|
2030
2185
|
const target = import_node_path4.default.join(this.directory, `${Date.now()}_${id}.jsonl`);
|
|
2031
2186
|
temp = `${target}.tmp-${createMessageId()}`;
|
|
2032
|
-
const store = await SessionStore.create(
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2187
|
+
const store = await SessionStore.create(
|
|
2188
|
+
temp,
|
|
2189
|
+
{
|
|
2190
|
+
conversationId: id,
|
|
2191
|
+
projectRoot: this.project.projectRoot,
|
|
2192
|
+
initialCwd: this.project.cwd,
|
|
2193
|
+
gitBranch: this.project.gitBranch,
|
|
2194
|
+
importIdentity: identity
|
|
2195
|
+
},
|
|
2196
|
+
this.readOptions
|
|
2197
|
+
);
|
|
2039
2198
|
await store.appendRecord("legacy_import", { source, identity });
|
|
2040
2199
|
await store.appendRecord("title", { title: "\u65E7\u5386\u53F2\u5BFC\u5165" });
|
|
2041
2200
|
for (const message of identifyHistory(raw, identity)) await store.append(message);
|
|
2042
2201
|
await store.load();
|
|
2043
2202
|
const after = import_node_fs5.default.statSync(source);
|
|
2044
|
-
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || !
|
|
2203
|
+
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || !readHistoryBytes(source, this.readOptions).equals(bytes)) {
|
|
2045
2204
|
this.report("Legacy history is changing; import postponed");
|
|
2046
2205
|
continue;
|
|
2047
2206
|
}
|
|
@@ -2104,7 +2263,7 @@ var SessionManager = class {
|
|
|
2104
2263
|
}
|
|
2105
2264
|
const nativeSource = toNativePhysicalPath(sourcePath);
|
|
2106
2265
|
const beforeStat = import_node_fs5.default.statSync(nativeSource);
|
|
2107
|
-
const bytes =
|
|
2266
|
+
const bytes = readHistoryBytes(nativeSource, this.readOptions);
|
|
2108
2267
|
const upgradeIdentity = (0, import_node_crypto6.createHash)("sha256").update(nativeSource).update("\0").update(bytes).digest("hex");
|
|
2109
2268
|
const existingMeta = (await this.allMetadata()).find(
|
|
2110
2269
|
(m) => m.upgradeIdentity === upgradeIdentity
|
|
@@ -2123,7 +2282,7 @@ var SessionManager = class {
|
|
|
2123
2282
|
if (doubleCheck) {
|
|
2124
2283
|
return this.open(doubleCheck.conversationId);
|
|
2125
2284
|
}
|
|
2126
|
-
const sourceStore = await SessionStore.open(nativeSource);
|
|
2285
|
+
const sourceStore = await SessionStore.open(nativeSource, this.readOptions);
|
|
2127
2286
|
if (sourceStore.getSchemaVersion() !== SESSION_SCHEMA_VERSION_V1) {
|
|
2128
2287
|
const state = await sourceStore.loadState();
|
|
2129
2288
|
return this.open(state.header.conversationId);
|
|
@@ -2190,7 +2349,7 @@ var SessionManager = class {
|
|
|
2190
2349
|
import_node_fs5.default.closeSync(fd);
|
|
2191
2350
|
}
|
|
2192
2351
|
const afterStat = import_node_fs5.default.statSync(nativeSource);
|
|
2193
|
-
if (beforeStat.size !== afterStat.size || beforeStat.mtimeMs !== afterStat.mtimeMs || !
|
|
2352
|
+
if (beforeStat.size !== afterStat.size || beforeStat.mtimeMs !== afterStat.mtimeMs || !readHistoryBytes(nativeSource, this.readOptions).equals(bytes)) {
|
|
2194
2353
|
throw new Error("Source legacy session changed during upgrade; retry aborted");
|
|
2195
2354
|
}
|
|
2196
2355
|
import_node_fs5.default.renameSync(tempPath, targetPath);
|
|
@@ -2353,7 +2512,7 @@ var SessionManager = class {
|
|
|
2353
2512
|
store.setAppendObserver(
|
|
2354
2513
|
(record) => {
|
|
2355
2514
|
const previous = JSON.parse(
|
|
2356
|
-
|
|
2515
|
+
readHistoryBytes(this.metadataPath(store.filePath), this.readOptions).toString("utf8")
|
|
2357
2516
|
);
|
|
2358
2517
|
const currentPrev = record.prevRecordId ?? record.parentId;
|
|
2359
2518
|
if (previous.conversationId !== header.conversationId || currentPrev !== null && currentPrev !== (previous.logRevision ?? previous.revision))
|
|
@@ -2398,10 +2557,28 @@ var SessionManager = class {
|
|
|
2398
2557
|
this.handles.set(header.conversationId, handle);
|
|
2399
2558
|
return handle;
|
|
2400
2559
|
}
|
|
2560
|
+
/** 超限列表只解析有界头部并核对桶身份,不读取正文、不生成可误用的耐久缓存。 */
|
|
2561
|
+
readOversizedMetadata(file) {
|
|
2562
|
+
const fd = import_node_fs5.default.openSync(file, "r");
|
|
2563
|
+
try {
|
|
2564
|
+
const buffer = Buffer.alloc(Math.min(this.readOptions.maxReadBytes, 64 * 1024));
|
|
2565
|
+
const length = import_node_fs5.default.readSync(fd, buffer, 0, buffer.length, 0);
|
|
2566
|
+
const end = buffer.subarray(0, length).indexOf(10);
|
|
2567
|
+
if (end < 0) throw new Error("Session header exceeds discovery budget");
|
|
2568
|
+
const header = JSON.parse(buffer.subarray(0, end).toString("utf8"));
|
|
2569
|
+
if (header.type !== "header" || ![1, 2].includes(header.schemaVersion))
|
|
2570
|
+
throw new Error("Session corrupt header");
|
|
2571
|
+
return { ...this.buildMetadata(file, [header]), title: "\u5386\u53F2\u8D85\u8FC7\u8BFB\u53D6\u9884\u7B97" };
|
|
2572
|
+
} finally {
|
|
2573
|
+
import_node_fs5.default.closeSync(fd);
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2401
2576
|
/** 缓存通过文件名、身份、统计与正文 stat 校验;失败交由正文重建。 */
|
|
2402
2577
|
readMetadata(file) {
|
|
2403
2578
|
try {
|
|
2404
|
-
const meta = JSON.parse(
|
|
2579
|
+
const meta = JSON.parse(
|
|
2580
|
+
readHistoryBytes(this.metadataPath(file), this.readOptions).toString("utf8")
|
|
2581
|
+
);
|
|
2405
2582
|
const stat = import_node_fs5.default.statSync(file);
|
|
2406
2583
|
if ((meta.schemaVersion === 1 || meta.schemaVersion === 2) && meta.fileName === import_node_path4.default.basename(file) && typeof meta.conversationId === "string" && import_node_path4.default.basename(file).endsWith(`_${meta.conversationId}.jsonl`) && normalizedProjectRoot(meta.projectRoot) === normalizedProjectRoot(this.project.projectRoot) && typeof meta.title === "string" && Number.isFinite(meta.createdAt) && Number.isFinite(meta.lastActivityAt) && Number.isSafeInteger(meta.messageCount) && meta.messageCount >= 0 && typeof meta.revision === "string" && meta.size === stat.size && meta.mtimeMs === stat.mtimeMs)
|
|
2407
2584
|
return meta;
|
|
@@ -2411,7 +2588,7 @@ var SessionManager = class {
|
|
|
2411
2588
|
}
|
|
2412
2589
|
/** 浏览仅解析完整记录;严格打开由 SessionStore 验证,浏览不能改写残片。 */
|
|
2413
2590
|
*readDiscoveryRecords(file) {
|
|
2414
|
-
const iterator = readJsonlLines(file);
|
|
2591
|
+
const iterator = readJsonlLines(file, this.readOptions);
|
|
2415
2592
|
let first = true;
|
|
2416
2593
|
let blankSeen = false;
|
|
2417
2594
|
for (const line of iterator) {
|
|
@@ -2466,7 +2643,8 @@ var SessionManager = class {
|
|
|
2466
2643
|
contentRevision: first.value.recordId,
|
|
2467
2644
|
activeBranchId: MAIN_BRANCH_ID,
|
|
2468
2645
|
activeLeafId: first.value.recordId,
|
|
2469
|
-
archived: false
|
|
2646
|
+
archived: false,
|
|
2647
|
+
pinned: false
|
|
2470
2648
|
};
|
|
2471
2649
|
const branchLeaves = /* @__PURE__ */ new Map();
|
|
2472
2650
|
branchLeaves.set(MAIN_BRANCH_ID, first.value.recordId);
|
|
@@ -2519,6 +2697,8 @@ var SessionManager = class {
|
|
|
2519
2697
|
if (bId) meta.activeBranchId = bId;
|
|
2520
2698
|
} else if (record.type === "archive_state") {
|
|
2521
2699
|
meta.archived = Boolean(record.payload.archived);
|
|
2700
|
+
} else if (record.type === "pin_state") {
|
|
2701
|
+
meta.pinned = Boolean(record.payload.pinned);
|
|
2522
2702
|
}
|
|
2523
2703
|
if (branchLeaves && !isGlobalControlRecordType(record.type)) {
|
|
2524
2704
|
const bId = record.branchId ?? meta.activeBranchId ?? MAIN_BRANCH_ID;
|
|
@@ -3025,6 +3205,7 @@ var CrossProjectSessionCatalog = class {
|
|
|
3025
3205
|
let title = "\u65B0\u4F1A\u8BDD";
|
|
3026
3206
|
let activeBranchId = MAIN_BRANCH_ID;
|
|
3027
3207
|
let archived = false;
|
|
3208
|
+
let pinned = false;
|
|
3028
3209
|
let lastRecordId = headerRecord.recordId;
|
|
3029
3210
|
for (const line of iterator) {
|
|
3030
3211
|
if (!line.text.trim()) continue;
|
|
@@ -3045,6 +3226,8 @@ var CrossProjectSessionCatalog = class {
|
|
|
3045
3226
|
title = String(rec.payload.title);
|
|
3046
3227
|
} else if (rec.type === "archive_state") {
|
|
3047
3228
|
archived = Boolean(rec.payload.archived);
|
|
3229
|
+
} else if (rec.type === "pin_state") {
|
|
3230
|
+
pinned = Boolean(rec.payload.pinned);
|
|
3048
3231
|
} else if (rec.type === "branch_selected") {
|
|
3049
3232
|
activeBranchId = String(rec.payload.branchId);
|
|
3050
3233
|
}
|
|
@@ -3063,6 +3246,7 @@ var CrossProjectSessionCatalog = class {
|
|
|
3063
3246
|
title,
|
|
3064
3247
|
activeBranchId,
|
|
3065
3248
|
archived,
|
|
3249
|
+
pinned,
|
|
3066
3250
|
size: stat.size,
|
|
3067
3251
|
mtimeMs: stat.mtimeMs,
|
|
3068
3252
|
revision: lastRecordId,
|
|
@@ -6200,7 +6384,8 @@ function stopProcessTree(child, nativeGroup) {
|
|
|
6200
6384
|
try {
|
|
6201
6385
|
process.kill(-child.pid, "SIGTERM");
|
|
6202
6386
|
return true;
|
|
6203
|
-
} catch {
|
|
6387
|
+
} catch (error) {
|
|
6388
|
+
if (error.code === "ESRCH") return true;
|
|
6204
6389
|
try {
|
|
6205
6390
|
child.kill("SIGTERM");
|
|
6206
6391
|
} catch {
|
|
@@ -6221,7 +6406,8 @@ function forceProcessTree(child, nativeGroup) {
|
|
|
6221
6406
|
try {
|
|
6222
6407
|
process.kill(-child.pid, "SIGKILL");
|
|
6223
6408
|
return true;
|
|
6224
|
-
} catch {
|
|
6409
|
+
} catch (error) {
|
|
6410
|
+
if (error.code === "ESRCH") return true;
|
|
6225
6411
|
try {
|
|
6226
6412
|
child.kill("SIGKILL");
|
|
6227
6413
|
} catch {
|
|
@@ -6242,13 +6428,31 @@ async function confirmProcessTreeStopped(child, terminationSucceeded, nativeGrou
|
|
|
6242
6428
|
if (process.platform === "win32") return true;
|
|
6243
6429
|
const deadline = Date.now() + 1e3;
|
|
6244
6430
|
while (Date.now() < deadline) {
|
|
6431
|
+
let exists;
|
|
6245
6432
|
try {
|
|
6246
6433
|
process.kill(-child.pid, 0);
|
|
6434
|
+
exists = true;
|
|
6247
6435
|
} catch (error) {
|
|
6248
|
-
|
|
6249
|
-
return
|
|
6436
|
+
const code = error.code;
|
|
6437
|
+
if (code === "ESRCH") return true;
|
|
6438
|
+
if (code !== "EPERM") return false;
|
|
6439
|
+
exists = true;
|
|
6250
6440
|
}
|
|
6251
|
-
|
|
6441
|
+
if (groupHasLiveProcess(child.pid)) {
|
|
6442
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
6443
|
+
continue;
|
|
6444
|
+
}
|
|
6445
|
+
return true;
|
|
6446
|
+
}
|
|
6447
|
+
return false;
|
|
6448
|
+
}
|
|
6449
|
+
function groupHasLiveProcess(pgid) {
|
|
6450
|
+
const result = (0, import_node_child_process3.spawnSync)("ps", ["-axo", "pgid=", "stat="], { timeout: 2e3, encoding: "utf8" });
|
|
6451
|
+
if (result.status !== 0 || result.error) return true;
|
|
6452
|
+
for (const row of result.stdout.split("\n")) {
|
|
6453
|
+
const columns = row.trim().split(/\s+/);
|
|
6454
|
+
if (columns.length < 2 || Number(columns[0]) !== pgid) continue;
|
|
6455
|
+
if (!columns[1].startsWith("Z")) return true;
|
|
6252
6456
|
}
|
|
6253
6457
|
return false;
|
|
6254
6458
|
}
|
|
@@ -7616,133 +7820,462 @@ var HookRegistry = class {
|
|
|
7616
7820
|
};
|
|
7617
7821
|
|
|
7618
7822
|
// src/runtime/executor/index.ts
|
|
7619
|
-
var import_node_crypto16 = require("crypto");
|
|
7620
7823
|
var import_node_path17 = __toESM(require("path"), 1);
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
this.rootDir = options.rootDir;
|
|
7661
|
-
this.signal = options.signal ? AbortSignal.any([options.signal, this.cancellation.signal]) : this.cancellation.signal;
|
|
7662
|
-
this.logger = options.logger;
|
|
7663
|
-
this.toolTimeoutMs = options.toolTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
7664
|
-
this.eventLogger = options.eventLogger;
|
|
7665
|
-
this.sessionId = options.sessionId;
|
|
7666
|
-
this.runId = options.runId;
|
|
7667
|
-
this.branchId = options.branchId;
|
|
7668
|
-
this.rootTaskId = options.rootTaskId;
|
|
7669
|
-
this.depth = options.depth;
|
|
7670
|
-
this.agentId = options.agentId;
|
|
7671
|
-
this.mode = options.mode ?? "Approval";
|
|
7672
|
-
this.permissionRules = options.permissionRules ?? [];
|
|
7673
|
-
this.approvalChannel = options.approvalChannel;
|
|
7674
|
-
this.approvalCache = options.approvalCache ?? new SessionApprovalCache();
|
|
7675
|
-
}
|
|
7676
|
-
/**
|
|
7677
|
-
* 父工具事务提交成功后调用,按注册顺序执行所有提交动作。
|
|
7678
|
-
* 动作执行成功后逐项清理对应回滚动作,若某项失败保留剩余回滚动作。
|
|
7679
|
-
*/
|
|
7680
|
-
async commit() {
|
|
7681
|
-
while (this.committedActions.length > 0) {
|
|
7682
|
-
const action = this.committedActions.shift();
|
|
7683
|
-
await action();
|
|
7684
|
-
this.rollbackActions.shift();
|
|
7685
|
-
}
|
|
7686
|
-
this.rollbackActions.length = 0;
|
|
7687
|
-
}
|
|
7688
|
-
/**
|
|
7689
|
-
* 父工具事务提交失败或取消时调用,按逆序执行所有回滚清理动作。
|
|
7690
|
-
*/
|
|
7691
|
-
async rollback() {
|
|
7692
|
-
const actions = [...this.rollbackActions].reverse();
|
|
7693
|
-
this.committedActions.length = 0;
|
|
7694
|
-
this.rollbackActions.length = 0;
|
|
7695
|
-
for (const action of actions) {
|
|
7824
|
+
|
|
7825
|
+
// src/runtime/executor/approval.ts
|
|
7826
|
+
var import_node_crypto16 = require("crypto");
|
|
7827
|
+
async function resolveToolApproval(deps) {
|
|
7828
|
+
const {
|
|
7829
|
+
call,
|
|
7830
|
+
finalInput,
|
|
7831
|
+
tool,
|
|
7832
|
+
rootDir,
|
|
7833
|
+
executionCwd,
|
|
7834
|
+
mode,
|
|
7835
|
+
permissionRules,
|
|
7836
|
+
approvalChannel,
|
|
7837
|
+
approvalCache,
|
|
7838
|
+
hardDenial,
|
|
7839
|
+
neverFailures,
|
|
7840
|
+
policy,
|
|
7841
|
+
signal,
|
|
7842
|
+
audit,
|
|
7843
|
+
failure
|
|
7844
|
+
} = deps;
|
|
7845
|
+
let shell = deps.shell;
|
|
7846
|
+
const executionIdentity = deps.executionIdentity;
|
|
7847
|
+
let auditFields = {};
|
|
7848
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
7849
|
+
const request = {
|
|
7850
|
+
toolName: call.name,
|
|
7851
|
+
capabilities: tool.metadata?.permissions ?? [],
|
|
7852
|
+
input: finalInput,
|
|
7853
|
+
rootDir,
|
|
7854
|
+
...shell ? { shell } : {},
|
|
7855
|
+
...executionIdentity ? { executionIdentity } : {},
|
|
7856
|
+
// 解释器内建查询与内容已绑定摘要的工作区直引脚本可确认不受外部脚本变更影响,
|
|
7857
|
+
// 允许会话级批准;其余 shell 命令仅允许单次批准。
|
|
7858
|
+
// 非 shell 工具(含深度绑定 executionIdentity 的外部工具)允许会话级批准。
|
|
7859
|
+
sessionAllowed: !shell || /^(?:pwd|get-location)$/i.test(shell.command.trim()) || shell.scriptDigest !== void 0
|
|
7860
|
+
};
|
|
7861
|
+
const repeatKey = (0, import_node_crypto16.createHash)("sha256").update(stableJsonStringify([call.name, finalInput])).digest("hex");
|
|
7862
|
+
if (neverFailures.has(repeatKey)) {
|
|
7696
7863
|
try {
|
|
7697
|
-
await
|
|
7864
|
+
await audit("tool.decided", { decision: "deny", decisionSource: "repeat_block" });
|
|
7865
|
+
await audit("tool.finished", { status: "blocked", errorCode: "REPEAT_BLOCKED" });
|
|
7698
7866
|
} catch {
|
|
7867
|
+
return {
|
|
7868
|
+
result: failure(
|
|
7869
|
+
"Audit unavailable; tool was not started",
|
|
7870
|
+
"AUDIT_UNAVAILABLE",
|
|
7871
|
+
"after_user_action",
|
|
7872
|
+
{
|
|
7873
|
+
version: 1,
|
|
7874
|
+
dispatchOutcome: "not_sent",
|
|
7875
|
+
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7876
|
+
}
|
|
7877
|
+
)
|
|
7878
|
+
};
|
|
7699
7879
|
}
|
|
7880
|
+
return {
|
|
7881
|
+
result: failure(
|
|
7882
|
+
"Identical failed tool call cannot be repeated in this run",
|
|
7883
|
+
"REPEAT_BLOCKED"
|
|
7884
|
+
)
|
|
7885
|
+
};
|
|
7700
7886
|
}
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
for (const call of calls) {
|
|
7714
|
-
const result = await this.executeOne(call, onProgress, requiresUserAction);
|
|
7715
|
-
results.push(result);
|
|
7716
|
-
onResult?.(structuredClone(result));
|
|
7717
|
-
if (result.retryPolicy === "after_user_action") requiresUserAction = true;
|
|
7718
|
-
}
|
|
7719
|
-
return results;
|
|
7720
|
-
}
|
|
7721
|
-
/**
|
|
7722
|
-
* 在最终参数上裁决并审计一次调用,所有拒绝和异常均闭合为工具结果。
|
|
7723
|
-
* 为调用记录本次绝对工具根目录,供跨目录恢复后的文件事实解析;不使用历史目录授权。
|
|
7724
|
-
*/
|
|
7725
|
-
async executeOne(call, onProgress, forcedDeny = false) {
|
|
7726
|
-
call.executionRoot = import_node_path17.default.resolve(this.workspaceRoot);
|
|
7727
|
-
const startedAt = Date.now();
|
|
7728
|
-
const operationId = createLogId();
|
|
7729
|
-
const failure = (message, errorCode, retryPolicy = "never", execution) => ({
|
|
7730
|
-
type: "tool_result",
|
|
7731
|
-
toolUseId: call.id,
|
|
7732
|
-
content: JSON.stringify({ code: errorCode, retryPolicy, message }),
|
|
7733
|
-
isError: true,
|
|
7734
|
-
errorCode,
|
|
7735
|
-
retryPolicy,
|
|
7736
|
-
durationMs: Math.max(0, Date.now() - startedAt),
|
|
7737
|
-
execution: execution ?? {
|
|
7738
|
-
version: 1,
|
|
7739
|
-
dispatchOutcome: "not_sent"
|
|
7740
|
-
}
|
|
7887
|
+
const hardDenied = hardDenial?.(tool, finalInput);
|
|
7888
|
+
const cached = approvalCache.get(request);
|
|
7889
|
+
const decision = policy.decide({
|
|
7890
|
+
hardDenied: hardDenied !== void 0,
|
|
7891
|
+
mode: typeof mode === "function" ? mode() : mode,
|
|
7892
|
+
toolName: call.name,
|
|
7893
|
+
capabilities: request.capabilities,
|
|
7894
|
+
dangerous: tool.metadata?.dangerous,
|
|
7895
|
+
cachedApproval: cached === "allow",
|
|
7896
|
+
cachedDenial: cached === "deny",
|
|
7897
|
+
rules: permissionRules,
|
|
7898
|
+
...shell ? { shell } : {}
|
|
7741
7899
|
});
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7900
|
+
let permitted = decision.decision === "allow";
|
|
7901
|
+
let source = decision.source;
|
|
7902
|
+
let approvalChoice;
|
|
7903
|
+
if (decision.decision === "ask") {
|
|
7904
|
+
let choice = "deny_once";
|
|
7905
|
+
try {
|
|
7906
|
+
choice = await approvalChannel?.requestApproval(request, signal) ?? "deny_once";
|
|
7907
|
+
} catch {
|
|
7908
|
+
choice = "cancel";
|
|
7909
|
+
}
|
|
7910
|
+
approvalChoice = choice;
|
|
7911
|
+
if (choice === "allow_session" && request.sessionAllowed) approvalCache.set(request, "allow");
|
|
7912
|
+
if (choice === "deny_session") approvalCache.set(request, "deny");
|
|
7913
|
+
permitted = choice === "allow_once" || choice === "allow_session" && request.sessionAllowed;
|
|
7914
|
+
source = approvalChannel ? "human" : "non_interactive";
|
|
7915
|
+
}
|
|
7916
|
+
auditFields = shell ? {
|
|
7917
|
+
shell: shell.interpreter,
|
|
7918
|
+
cwdDigest: (0, import_node_crypto16.createHash)("sha256").update(shell.cwd).digest("hex"),
|
|
7919
|
+
executableDigest: shell.executableDigest,
|
|
7920
|
+
executionFingerprint: shell.executionFingerprint,
|
|
7921
|
+
...commandAuditSummary(shell.command)
|
|
7922
|
+
} : executionIdentity ? {
|
|
7923
|
+
serverIdentity: executionIdentity.serverIdentity,
|
|
7924
|
+
connectionGeneration: executionIdentity.connectionGeneration,
|
|
7925
|
+
catalogEpoch: executionIdentity.catalogEpoch,
|
|
7926
|
+
toolDigest: executionIdentity.toolDigest
|
|
7927
|
+
} : {};
|
|
7928
|
+
try {
|
|
7929
|
+
await audit("tool.decided", {
|
|
7930
|
+
decision: permitted ? "allow" : "deny",
|
|
7931
|
+
decisionSource: source,
|
|
7932
|
+
...approvalChoice ? { approvalChoice } : {},
|
|
7933
|
+
...hardDenied?.fields ?? {},
|
|
7934
|
+
...auditFields
|
|
7935
|
+
});
|
|
7936
|
+
} catch {
|
|
7937
|
+
return {
|
|
7938
|
+
result: failure(
|
|
7939
|
+
"Audit unavailable; tool was not started",
|
|
7940
|
+
"AUDIT_UNAVAILABLE",
|
|
7941
|
+
"after_user_action",
|
|
7942
|
+
{
|
|
7943
|
+
version: 1,
|
|
7944
|
+
dispatchOutcome: "not_sent",
|
|
7945
|
+
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7946
|
+
}
|
|
7947
|
+
)
|
|
7948
|
+
};
|
|
7949
|
+
}
|
|
7950
|
+
if (!permitted || signal?.aborted) {
|
|
7951
|
+
const code = signal?.aborted ? "ABORTED" : hardDenied?.code ?? "PERMISSION_DENIED";
|
|
7952
|
+
try {
|
|
7953
|
+
await audit("tool.finished", { status: "denied", errorCode: code });
|
|
7954
|
+
} catch {
|
|
7955
|
+
return {
|
|
7956
|
+
result: failure("Audit result unavailable", "AUDIT_UNAVAILABLE", "after_user_action", {
|
|
7957
|
+
version: 1,
|
|
7958
|
+
dispatchOutcome: "not_sent",
|
|
7959
|
+
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7960
|
+
})
|
|
7961
|
+
};
|
|
7962
|
+
}
|
|
7963
|
+
return {
|
|
7964
|
+
result: failure(
|
|
7965
|
+
signal?.aborted ? "Operation cancelled" : hardDenied?.message ?? "Tool permission denied",
|
|
7966
|
+
code,
|
|
7967
|
+
signal?.aborted ? "after_user_action" : hardDenied?.retryPolicy ?? "after_user_action"
|
|
7968
|
+
)
|
|
7969
|
+
};
|
|
7970
|
+
}
|
|
7971
|
+
if (shell) {
|
|
7972
|
+
try {
|
|
7973
|
+
const next = tool.approvalScope?.(finalInput, rootDir, executionCwd);
|
|
7974
|
+
if (next?.executionFingerprint !== shell.executionFingerprint) {
|
|
7975
|
+
if (attempt === 0 && next) {
|
|
7976
|
+
try {
|
|
7977
|
+
await audit("tool.target_changed", { status: "redeciding" });
|
|
7978
|
+
} catch {
|
|
7979
|
+
return {
|
|
7980
|
+
result: failure(
|
|
7981
|
+
"Audit unavailable; tool was not started",
|
|
7982
|
+
"AUDIT_UNAVAILABLE",
|
|
7983
|
+
"after_user_action",
|
|
7984
|
+
{
|
|
7985
|
+
version: 1,
|
|
7986
|
+
dispatchOutcome: "not_sent",
|
|
7987
|
+
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7988
|
+
}
|
|
7989
|
+
)
|
|
7990
|
+
};
|
|
7991
|
+
}
|
|
7992
|
+
shell = next;
|
|
7993
|
+
continue;
|
|
7994
|
+
}
|
|
7995
|
+
await audit("tool.finished", { status: "target_changed", errorCode: "TARGET_CHANGED" });
|
|
7996
|
+
return {
|
|
7997
|
+
result: failure(
|
|
7998
|
+
"Execution target changed after approval",
|
|
7999
|
+
"TARGET_CHANGED",
|
|
8000
|
+
"after_user_action"
|
|
8001
|
+
)
|
|
8002
|
+
};
|
|
8003
|
+
}
|
|
8004
|
+
} catch {
|
|
8005
|
+
return {
|
|
8006
|
+
result: failure(
|
|
8007
|
+
"Execution target changed after approval",
|
|
8008
|
+
"TARGET_CHANGED",
|
|
8009
|
+
"after_user_action"
|
|
8010
|
+
)
|
|
8011
|
+
};
|
|
8012
|
+
}
|
|
8013
|
+
}
|
|
8014
|
+
if (executionIdentity) {
|
|
8015
|
+
try {
|
|
8016
|
+
const nextIdentity = tool.executionIdentity?.(finalInput, rootDir);
|
|
8017
|
+
if (!nextIdentity || nextIdentity.serverIdentity !== executionIdentity.serverIdentity || nextIdentity.connectionGeneration !== executionIdentity.connectionGeneration || nextIdentity.catalogEpoch !== executionIdentity.catalogEpoch || nextIdentity.toolDigest !== executionIdentity.toolDigest) {
|
|
8018
|
+
await audit("tool.finished", {
|
|
8019
|
+
status: "identity_changed",
|
|
8020
|
+
errorCode: "MCP_TOOL_CHANGED"
|
|
8021
|
+
});
|
|
8022
|
+
return {
|
|
8023
|
+
result: failure(
|
|
8024
|
+
"Tool execution identity changed after approval",
|
|
8025
|
+
"MCP_TOOL_CHANGED",
|
|
8026
|
+
"after_user_action",
|
|
8027
|
+
{
|
|
8028
|
+
version: 1,
|
|
8029
|
+
dispatchOutcome: "not_sent",
|
|
8030
|
+
hostError: { phase: "validation", code: "MCP_TOOL_CHANGED" }
|
|
8031
|
+
}
|
|
8032
|
+
)
|
|
8033
|
+
};
|
|
8034
|
+
}
|
|
8035
|
+
} catch {
|
|
8036
|
+
return {
|
|
8037
|
+
result: failure(
|
|
8038
|
+
"Tool execution identity check failed after approval",
|
|
8039
|
+
"MCP_TOOL_CHANGED",
|
|
8040
|
+
"after_user_action",
|
|
8041
|
+
{
|
|
8042
|
+
version: 1,
|
|
8043
|
+
dispatchOutcome: "not_sent"
|
|
8044
|
+
}
|
|
8045
|
+
)
|
|
8046
|
+
};
|
|
8047
|
+
}
|
|
8048
|
+
}
|
|
8049
|
+
break;
|
|
8050
|
+
}
|
|
8051
|
+
return { shell, auditFields };
|
|
8052
|
+
}
|
|
8053
|
+
|
|
8054
|
+
// src/runtime/executor/result.ts
|
|
8055
|
+
var import_node_crypto17 = require("crypto");
|
|
8056
|
+
async function closeToolResult(deps) {
|
|
8057
|
+
const { call, tool, finalInput, hookCtx, hooks, neverFailures, audit, onToolResult, startedAt } = deps;
|
|
8058
|
+
const resultBlock = {
|
|
8059
|
+
type: "tool_result",
|
|
8060
|
+
toolUseId: call.id,
|
|
8061
|
+
content: deps.output.content,
|
|
8062
|
+
isError: deps.output.isError,
|
|
8063
|
+
...deps.output.errorCode ? { errorCode: deps.output.errorCode, retryPolicy: deps.output.retryPolicy } : {},
|
|
8064
|
+
execution: deps.output.execution
|
|
8065
|
+
};
|
|
8066
|
+
if (resultBlock.isError && resultBlock.errorCode) {
|
|
8067
|
+
resultBlock.content = JSON.stringify({
|
|
8068
|
+
code: resultBlock.errorCode,
|
|
8069
|
+
retryPolicy: resultBlock.retryPolicy,
|
|
8070
|
+
message: deps.output.content
|
|
8071
|
+
});
|
|
8072
|
+
}
|
|
8073
|
+
try {
|
|
8074
|
+
for (const hook of hooks.get("tool:after")) {
|
|
8075
|
+
await hook(
|
|
8076
|
+
hookCtx,
|
|
8077
|
+
{ id: call.id, name: call.name, input: finalInput },
|
|
8078
|
+
{ output: deps.output.content, isError: deps.output.isError }
|
|
8079
|
+
);
|
|
8080
|
+
}
|
|
8081
|
+
} catch {
|
|
8082
|
+
resultBlock.content = JSON.stringify({
|
|
8083
|
+
code: "HOOK_ERROR",
|
|
8084
|
+
retryPolicy: "after_user_action",
|
|
8085
|
+
message: "Tool finished but result processing failed"
|
|
8086
|
+
});
|
|
8087
|
+
resultBlock.isError = true;
|
|
8088
|
+
resultBlock.errorCode = "HOOK_ERROR";
|
|
8089
|
+
resultBlock.retryPolicy = "after_user_action";
|
|
8090
|
+
resultBlock.execution = {
|
|
8091
|
+
version: 1,
|
|
8092
|
+
dispatchOutcome: resultBlock.execution?.dispatchOutcome ?? "failed",
|
|
8093
|
+
hostError: {
|
|
8094
|
+
phase: "hook",
|
|
8095
|
+
code: "HOOK_ERROR"
|
|
8096
|
+
}
|
|
8097
|
+
};
|
|
8098
|
+
}
|
|
8099
|
+
resultBlock.durationMs = Math.max(0, Date.now() - startedAt);
|
|
8100
|
+
if (resultBlock.isError && resultBlock.retryPolicy === "never") {
|
|
8101
|
+
neverFailures.add(
|
|
8102
|
+
(0, import_node_crypto17.createHash)("sha256").update(stableJsonStringify([call.name, finalInput])).digest("hex")
|
|
8103
|
+
);
|
|
8104
|
+
}
|
|
8105
|
+
try {
|
|
8106
|
+
await audit("tool.finished", {
|
|
8107
|
+
status: resultBlock.isError ? "failed" : "completed",
|
|
8108
|
+
...resultBlock.errorCode ? { errorCode: resultBlock.errorCode } : {},
|
|
8109
|
+
durationMs: resultBlock.durationMs
|
|
8110
|
+
});
|
|
8111
|
+
} catch {
|
|
8112
|
+
resultBlock.isError = true;
|
|
8113
|
+
resultBlock.errorCode = "OUTCOME_UNKNOWN";
|
|
8114
|
+
resultBlock.retryPolicy = "after_user_action";
|
|
8115
|
+
resultBlock.content = JSON.stringify({
|
|
8116
|
+
code: "OUTCOME_UNKNOWN",
|
|
8117
|
+
retryPolicy: "after_user_action",
|
|
8118
|
+
message: "Tool outcome is unknown because the audit result could not be written"
|
|
8119
|
+
});
|
|
8120
|
+
resultBlock.execution = {
|
|
8121
|
+
version: 1,
|
|
8122
|
+
dispatchOutcome: resultBlock.execution?.dispatchOutcome ?? "succeeded",
|
|
8123
|
+
hostError: {
|
|
8124
|
+
phase: "audit",
|
|
8125
|
+
code: "AUDIT_UNAVAILABLE"
|
|
8126
|
+
}
|
|
8127
|
+
};
|
|
8128
|
+
return resultBlock;
|
|
8129
|
+
}
|
|
8130
|
+
await onToolResult?.(tool, finalInput, resultBlock);
|
|
8131
|
+
return resultBlock;
|
|
8132
|
+
}
|
|
8133
|
+
|
|
8134
|
+
// src/runtime/executor/timeout.ts
|
|
8135
|
+
function withToolTimeout(promise, toolName, timeoutMs, executionController) {
|
|
8136
|
+
let timer;
|
|
8137
|
+
const timeout = new Promise((_, reject) => {
|
|
8138
|
+
timer = setTimeout(() => {
|
|
8139
|
+
const error = new KapibalaError(`Tool '${toolName}' timed out after ${timeoutMs}ms`, {
|
|
8140
|
+
code: "OUTCOME_UNKNOWN",
|
|
8141
|
+
retryPolicy: "after_user_action",
|
|
8142
|
+
safeMessage: "Tool timed out; completion of side effects is unknown"
|
|
8143
|
+
});
|
|
8144
|
+
reject(error);
|
|
8145
|
+
executionController.abort(error);
|
|
8146
|
+
}, timeoutMs);
|
|
8147
|
+
});
|
|
8148
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
8149
|
+
}
|
|
8150
|
+
|
|
8151
|
+
// src/runtime/executor/types.ts
|
|
8152
|
+
var DEFAULT_TOOL_TIMEOUT_MS = 12e4;
|
|
8153
|
+
|
|
8154
|
+
// src/runtime/executor/index.ts
|
|
8155
|
+
var ToolExecutor = class {
|
|
8156
|
+
hardDenial;
|
|
8157
|
+
onToolResult;
|
|
8158
|
+
tools;
|
|
8159
|
+
hooks;
|
|
8160
|
+
workspaceRoot;
|
|
8161
|
+
executionCwd;
|
|
8162
|
+
sandbox;
|
|
8163
|
+
leaseCoordinator;
|
|
8164
|
+
rootDir;
|
|
8165
|
+
signal;
|
|
8166
|
+
logger;
|
|
8167
|
+
toolTimeoutMs;
|
|
8168
|
+
eventLogger;
|
|
8169
|
+
sessionId;
|
|
8170
|
+
runId;
|
|
8171
|
+
branchId;
|
|
8172
|
+
rootTaskId;
|
|
8173
|
+
depth;
|
|
8174
|
+
agentId;
|
|
8175
|
+
committedActions = [];
|
|
8176
|
+
rollbackActions = [];
|
|
8177
|
+
mode;
|
|
8178
|
+
permissionRules;
|
|
8179
|
+
approvalChannel;
|
|
8180
|
+
approvalCache;
|
|
8181
|
+
policy = new PermissionPolicy();
|
|
8182
|
+
neverFailures = /* @__PURE__ */ new Set();
|
|
8183
|
+
cancellation = new AbortController();
|
|
8184
|
+
constructor(options) {
|
|
8185
|
+
this.hardDenial = options.hardDenial;
|
|
8186
|
+
this.onToolResult = options.onToolResult;
|
|
8187
|
+
this.tools = options.tools;
|
|
8188
|
+
this.hooks = options.hooks;
|
|
8189
|
+
this.workspaceRoot = options.workspaceRoot ?? options.rootDir;
|
|
8190
|
+
this.executionCwd = options.executionCwd ?? options.rootDir;
|
|
8191
|
+
this.sandbox = options.sandbox;
|
|
8192
|
+
this.leaseCoordinator = options.leaseCoordinator;
|
|
8193
|
+
this.rootDir = options.rootDir;
|
|
8194
|
+
this.signal = options.signal ? AbortSignal.any([options.signal, this.cancellation.signal]) : this.cancellation.signal;
|
|
8195
|
+
this.logger = options.logger;
|
|
8196
|
+
this.toolTimeoutMs = options.toolTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
8197
|
+
this.eventLogger = options.eventLogger;
|
|
8198
|
+
this.sessionId = options.sessionId;
|
|
8199
|
+
this.runId = options.runId;
|
|
8200
|
+
this.branchId = options.branchId;
|
|
8201
|
+
this.rootTaskId = options.rootTaskId;
|
|
8202
|
+
this.depth = options.depth;
|
|
8203
|
+
this.agentId = options.agentId;
|
|
8204
|
+
this.mode = options.mode ?? "Approval";
|
|
8205
|
+
this.permissionRules = options.permissionRules ?? [];
|
|
8206
|
+
this.approvalChannel = options.approvalChannel;
|
|
8207
|
+
this.approvalCache = options.approvalCache ?? new SessionApprovalCache();
|
|
8208
|
+
}
|
|
8209
|
+
/**
|
|
8210
|
+
* 父工具事务提交成功后调用,按注册顺序执行所有提交动作。
|
|
8211
|
+
* 动作执行成功后逐项清理对应回滚动作,若某项失败保留剩余回滚动作。
|
|
8212
|
+
*/
|
|
8213
|
+
async commit() {
|
|
8214
|
+
while (this.committedActions.length > 0) {
|
|
8215
|
+
const action = this.committedActions.shift();
|
|
8216
|
+
await action();
|
|
8217
|
+
this.rollbackActions.shift();
|
|
8218
|
+
}
|
|
8219
|
+
this.rollbackActions.length = 0;
|
|
8220
|
+
}
|
|
8221
|
+
/**
|
|
8222
|
+
* 父工具事务提交失败或取消时调用,按逆序执行所有回滚清理动作。
|
|
8223
|
+
*/
|
|
8224
|
+
async rollback() {
|
|
8225
|
+
const actions = [...this.rollbackActions].reverse();
|
|
8226
|
+
this.committedActions.length = 0;
|
|
8227
|
+
this.rollbackActions.length = 0;
|
|
8228
|
+
for (const action of actions) {
|
|
8229
|
+
try {
|
|
8230
|
+
await action();
|
|
8231
|
+
} catch {
|
|
8232
|
+
}
|
|
8233
|
+
}
|
|
8234
|
+
}
|
|
8235
|
+
/** 中止本轮正在等待或执行的工具;调用方仍须等待 runAll 完成资源回收及结果审计。 */
|
|
8236
|
+
cancel() {
|
|
8237
|
+
this.cancellation.abort();
|
|
8238
|
+
}
|
|
8239
|
+
/**
|
|
8240
|
+
* 串行执行全部调用并返回与入参顺序一致的结果;某个结果要求用户介入后,其余调用直接拒绝。
|
|
8241
|
+
* onResult 在每个最终结果(含拒绝/失败)产生后提供独立快照,供在途预算估算,不代表历史已提交;回调不得抛错。
|
|
8242
|
+
*/
|
|
8243
|
+
async runAll(calls, onProgress, onResult) {
|
|
8244
|
+
const results = [];
|
|
8245
|
+
let requiresUserAction = false;
|
|
8246
|
+
for (const call of calls) {
|
|
8247
|
+
const result = await this.executeOne(call, onProgress, requiresUserAction);
|
|
8248
|
+
results.push(result);
|
|
8249
|
+
onResult?.(structuredClone(result));
|
|
8250
|
+
if (result.retryPolicy === "after_user_action") requiresUserAction = true;
|
|
8251
|
+
}
|
|
8252
|
+
return results;
|
|
8253
|
+
}
|
|
8254
|
+
/**
|
|
8255
|
+
* 在最终参数上裁决并审计一次调用,所有拒绝和异常均闭合为工具结果。
|
|
8256
|
+
* 为调用记录本次绝对工具根目录,供跨目录恢复后的文件事实解析;不使用历史目录授权。
|
|
8257
|
+
*/
|
|
8258
|
+
async executeOne(call, onProgress, forcedDeny = false) {
|
|
8259
|
+
call.executionRoot = import_node_path17.default.resolve(this.workspaceRoot);
|
|
8260
|
+
const startedAt = Date.now();
|
|
8261
|
+
const operationId = createLogId();
|
|
8262
|
+
const failure = (message, errorCode, retryPolicy = "never", execution) => ({
|
|
8263
|
+
type: "tool_result",
|
|
8264
|
+
toolUseId: call.id,
|
|
8265
|
+
content: JSON.stringify({ code: errorCode, retryPolicy, message }),
|
|
8266
|
+
isError: true,
|
|
8267
|
+
errorCode,
|
|
8268
|
+
retryPolicy,
|
|
8269
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
8270
|
+
execution: execution ?? {
|
|
8271
|
+
version: 1,
|
|
8272
|
+
dispatchOutcome: "not_sent"
|
|
8273
|
+
}
|
|
8274
|
+
});
|
|
8275
|
+
const audit = async (event, fields = {}) => {
|
|
8276
|
+
if (!this.eventLogger || !this.sessionId) return;
|
|
8277
|
+
await this.eventLogger.recordAudit({
|
|
8278
|
+
level: "info",
|
|
7746
8279
|
event,
|
|
7747
8280
|
sessionId: this.sessionId,
|
|
7748
8281
|
runId: this.runId,
|
|
@@ -7934,191 +8467,28 @@ var ToolExecutor = class {
|
|
|
7934
8467
|
}
|
|
7935
8468
|
return failure("Invalid tool execution identity", "INVALID_TARGET");
|
|
7936
8469
|
}
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
"AUDIT_UNAVAILABLE",
|
|
7960
|
-
"after_user_action",
|
|
7961
|
-
{
|
|
7962
|
-
version: 1,
|
|
7963
|
-
dispatchOutcome: "not_sent",
|
|
7964
|
-
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7965
|
-
}
|
|
7966
|
-
);
|
|
7967
|
-
}
|
|
7968
|
-
return failure(
|
|
7969
|
-
"Identical failed tool call cannot be repeated in this run",
|
|
7970
|
-
"REPEAT_BLOCKED"
|
|
7971
|
-
);
|
|
7972
|
-
}
|
|
7973
|
-
const hardDenied = this.hardDenial?.(tool, finalInput);
|
|
7974
|
-
const cached = this.approvalCache.get(request);
|
|
7975
|
-
const decision = this.policy.decide({
|
|
7976
|
-
hardDenied: hardDenied !== void 0,
|
|
7977
|
-
mode: typeof this.mode === "function" ? this.mode() : this.mode,
|
|
7978
|
-
toolName: call.name,
|
|
7979
|
-
capabilities: request.capabilities,
|
|
7980
|
-
dangerous: tool.metadata?.dangerous,
|
|
7981
|
-
cachedApproval: cached === "allow",
|
|
7982
|
-
cachedDenial: cached === "deny",
|
|
7983
|
-
rules: this.permissionRules,
|
|
7984
|
-
...shell ? { shell } : {}
|
|
7985
|
-
});
|
|
7986
|
-
let permitted = decision.decision === "allow";
|
|
7987
|
-
let source = decision.source;
|
|
7988
|
-
let approvalChoice;
|
|
7989
|
-
if (decision.decision === "ask") {
|
|
7990
|
-
let choice = "deny_once";
|
|
7991
|
-
try {
|
|
7992
|
-
choice = await this.approvalChannel?.requestApproval(request, this.signal) ?? "deny_once";
|
|
7993
|
-
} catch {
|
|
7994
|
-
choice = "cancel";
|
|
7995
|
-
}
|
|
7996
|
-
approvalChoice = choice;
|
|
7997
|
-
if (choice === "allow_session" && request.sessionAllowed)
|
|
7998
|
-
this.approvalCache.set(request, "allow");
|
|
7999
|
-
if (choice === "deny_session") this.approvalCache.set(request, "deny");
|
|
8000
|
-
permitted = choice === "allow_once" || choice === "allow_session" && request.sessionAllowed;
|
|
8001
|
-
source = this.approvalChannel ? "human" : "non_interactive";
|
|
8002
|
-
}
|
|
8003
|
-
auditFields = shell ? {
|
|
8004
|
-
shell: shell.interpreter,
|
|
8005
|
-
cwdDigest: (0, import_node_crypto16.createHash)("sha256").update(shell.cwd).digest("hex"),
|
|
8006
|
-
executableDigest: shell.executableDigest,
|
|
8007
|
-
executionFingerprint: shell.executionFingerprint,
|
|
8008
|
-
...commandAuditSummary(shell.command)
|
|
8009
|
-
} : executionIdentity ? {
|
|
8010
|
-
serverIdentity: executionIdentity.serverIdentity,
|
|
8011
|
-
connectionGeneration: executionIdentity.connectionGeneration,
|
|
8012
|
-
catalogEpoch: executionIdentity.catalogEpoch,
|
|
8013
|
-
toolDigest: executionIdentity.toolDigest
|
|
8014
|
-
} : {};
|
|
8015
|
-
try {
|
|
8016
|
-
await audit("tool.decided", {
|
|
8017
|
-
decision: permitted ? "allow" : "deny",
|
|
8018
|
-
decisionSource: source,
|
|
8019
|
-
...approvalChoice ? { approvalChoice } : {},
|
|
8020
|
-
...hardDenied?.fields ?? {},
|
|
8021
|
-
...auditFields
|
|
8022
|
-
});
|
|
8023
|
-
} catch {
|
|
8024
|
-
return failure(
|
|
8025
|
-
"Audit unavailable; tool was not started",
|
|
8026
|
-
"AUDIT_UNAVAILABLE",
|
|
8027
|
-
"after_user_action",
|
|
8028
|
-
{
|
|
8029
|
-
version: 1,
|
|
8030
|
-
dispatchOutcome: "not_sent",
|
|
8031
|
-
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
8032
|
-
}
|
|
8033
|
-
);
|
|
8034
|
-
}
|
|
8035
|
-
if (!permitted || this.signal?.aborted) {
|
|
8036
|
-
const code = this.signal?.aborted ? "ABORTED" : hardDenied?.code ?? "PERMISSION_DENIED";
|
|
8037
|
-
try {
|
|
8038
|
-
await audit("tool.finished", { status: "denied", errorCode: code });
|
|
8039
|
-
} catch {
|
|
8040
|
-
return failure("Audit result unavailable", "AUDIT_UNAVAILABLE", "after_user_action", {
|
|
8041
|
-
version: 1,
|
|
8042
|
-
dispatchOutcome: "not_sent",
|
|
8043
|
-
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
8044
|
-
});
|
|
8045
|
-
}
|
|
8046
|
-
return failure(
|
|
8047
|
-
this.signal?.aborted ? "Operation cancelled" : hardDenied?.message ?? "Tool permission denied",
|
|
8048
|
-
code,
|
|
8049
|
-
this.signal?.aborted ? "after_user_action" : hardDenied?.retryPolicy ?? "after_user_action"
|
|
8050
|
-
);
|
|
8051
|
-
}
|
|
8052
|
-
if (shell) {
|
|
8053
|
-
try {
|
|
8054
|
-
const next = tool.approvalScope?.(finalInput, this.rootDir, this.executionCwd);
|
|
8055
|
-
if (next?.executionFingerprint !== shell.executionFingerprint) {
|
|
8056
|
-
if (attempt === 0 && next) {
|
|
8057
|
-
try {
|
|
8058
|
-
await audit("tool.target_changed", { status: "redeciding" });
|
|
8059
|
-
} catch {
|
|
8060
|
-
return failure(
|
|
8061
|
-
"Audit unavailable; tool was not started",
|
|
8062
|
-
"AUDIT_UNAVAILABLE",
|
|
8063
|
-
"after_user_action",
|
|
8064
|
-
{
|
|
8065
|
-
version: 1,
|
|
8066
|
-
dispatchOutcome: "not_sent",
|
|
8067
|
-
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
8068
|
-
}
|
|
8069
|
-
);
|
|
8070
|
-
}
|
|
8071
|
-
shell = next;
|
|
8072
|
-
continue;
|
|
8073
|
-
}
|
|
8074
|
-
await audit("tool.finished", { status: "target_changed", errorCode: "TARGET_CHANGED" });
|
|
8075
|
-
return failure(
|
|
8076
|
-
"Execution target changed after approval",
|
|
8077
|
-
"TARGET_CHANGED",
|
|
8078
|
-
"after_user_action"
|
|
8079
|
-
);
|
|
8080
|
-
}
|
|
8081
|
-
} catch {
|
|
8082
|
-
return failure(
|
|
8083
|
-
"Execution target changed after approval",
|
|
8084
|
-
"TARGET_CHANGED",
|
|
8085
|
-
"after_user_action"
|
|
8086
|
-
);
|
|
8087
|
-
}
|
|
8088
|
-
}
|
|
8089
|
-
if (executionIdentity) {
|
|
8090
|
-
try {
|
|
8091
|
-
const nextIdentity = tool.executionIdentity?.(finalInput, this.rootDir);
|
|
8092
|
-
if (!nextIdentity || nextIdentity.serverIdentity !== executionIdentity.serverIdentity || nextIdentity.connectionGeneration !== executionIdentity.connectionGeneration || nextIdentity.catalogEpoch !== executionIdentity.catalogEpoch || nextIdentity.toolDigest !== executionIdentity.toolDigest) {
|
|
8093
|
-
await audit("tool.finished", {
|
|
8094
|
-
status: "identity_changed",
|
|
8095
|
-
errorCode: "MCP_TOOL_CHANGED"
|
|
8096
|
-
});
|
|
8097
|
-
return failure(
|
|
8098
|
-
"Tool execution identity changed after approval",
|
|
8099
|
-
"MCP_TOOL_CHANGED",
|
|
8100
|
-
"after_user_action",
|
|
8101
|
-
{
|
|
8102
|
-
version: 1,
|
|
8103
|
-
dispatchOutcome: "not_sent",
|
|
8104
|
-
hostError: { phase: "validation", code: "MCP_TOOL_CHANGED" }
|
|
8105
|
-
}
|
|
8106
|
-
);
|
|
8107
|
-
}
|
|
8108
|
-
} catch {
|
|
8109
|
-
return failure(
|
|
8110
|
-
"Tool execution identity check failed after approval",
|
|
8111
|
-
"MCP_TOOL_CHANGED",
|
|
8112
|
-
"after_user_action",
|
|
8113
|
-
{
|
|
8114
|
-
version: 1,
|
|
8115
|
-
dispatchOutcome: "not_sent"
|
|
8116
|
-
}
|
|
8117
|
-
);
|
|
8118
|
-
}
|
|
8119
|
-
}
|
|
8120
|
-
break;
|
|
8121
|
-
}
|
|
8470
|
+
const approval = await resolveToolApproval({
|
|
8471
|
+
call,
|
|
8472
|
+
finalInput,
|
|
8473
|
+
tool,
|
|
8474
|
+
shell,
|
|
8475
|
+
executionIdentity,
|
|
8476
|
+
rootDir: this.rootDir,
|
|
8477
|
+
executionCwd: this.executionCwd,
|
|
8478
|
+
mode: this.mode,
|
|
8479
|
+
permissionRules: this.permissionRules,
|
|
8480
|
+
approvalChannel: this.approvalChannel,
|
|
8481
|
+
approvalCache: this.approvalCache,
|
|
8482
|
+
hardDenial: this.hardDenial,
|
|
8483
|
+
neverFailures: this.neverFailures,
|
|
8484
|
+
policy: this.policy,
|
|
8485
|
+
signal: this.signal,
|
|
8486
|
+
audit,
|
|
8487
|
+
failure
|
|
8488
|
+
});
|
|
8489
|
+
if (approval.result) return approval.result;
|
|
8490
|
+
shell = approval.shell;
|
|
8491
|
+
const auditFields = approval.auditFields ?? {};
|
|
8122
8492
|
try {
|
|
8123
8493
|
await audit("tool.started", { status: "started", ...auditFields });
|
|
8124
8494
|
} catch {
|
|
@@ -8178,9 +8548,10 @@ var ToolExecutor = class {
|
|
|
8178
8548
|
{ signal: executionController.signal }
|
|
8179
8549
|
);
|
|
8180
8550
|
}
|
|
8181
|
-
const rawResult = tool.metadata?.managesTimeout ? await tool.execute(finalInput, toolCtx) : await
|
|
8551
|
+
const rawResult = tool.metadata?.managesTimeout ? await tool.execute(finalInput, toolCtx) : await withToolTimeout(
|
|
8182
8552
|
tool.execute(finalInput, toolCtx),
|
|
8183
8553
|
call.name,
|
|
8554
|
+
this.toolTimeoutMs,
|
|
8184
8555
|
executionController
|
|
8185
8556
|
);
|
|
8186
8557
|
if (typeof tool.toExecutionResult === "function") {
|
|
@@ -8241,105 +8612,18 @@ var ToolExecutor = class {
|
|
|
8241
8612
|
dispatchOutcome: outcome
|
|
8242
8613
|
};
|
|
8243
8614
|
}
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
retryPolicy: resultBlock.retryPolicy,
|
|
8256
|
-
message: outputStr
|
|
8257
|
-
});
|
|
8258
|
-
}
|
|
8259
|
-
try {
|
|
8260
|
-
for (const hook of this.hooks.get("tool:after")) {
|
|
8261
|
-
await hook(
|
|
8262
|
-
hookCtx,
|
|
8263
|
-
{ id: call.id, name: call.name, input: finalInput },
|
|
8264
|
-
{ output: outputStr, isError }
|
|
8265
|
-
);
|
|
8266
|
-
}
|
|
8267
|
-
} catch {
|
|
8268
|
-
resultBlock.content = JSON.stringify({
|
|
8269
|
-
code: "HOOK_ERROR",
|
|
8270
|
-
retryPolicy: "after_user_action",
|
|
8271
|
-
message: "Tool finished but result processing failed"
|
|
8272
|
-
});
|
|
8273
|
-
resultBlock.isError = true;
|
|
8274
|
-
resultBlock.errorCode = "HOOK_ERROR";
|
|
8275
|
-
resultBlock.retryPolicy = "after_user_action";
|
|
8276
|
-
resultBlock.execution = {
|
|
8277
|
-
version: 1,
|
|
8278
|
-
dispatchOutcome: resultBlock.execution?.dispatchOutcome ?? "failed",
|
|
8279
|
-
hostError: {
|
|
8280
|
-
phase: "hook",
|
|
8281
|
-
code: "HOOK_ERROR"
|
|
8282
|
-
}
|
|
8283
|
-
};
|
|
8284
|
-
}
|
|
8285
|
-
resultBlock.durationMs = Math.max(0, Date.now() - startedAt);
|
|
8286
|
-
if (resultBlock.isError && resultBlock.retryPolicy === "never") {
|
|
8287
|
-
this.neverFailures.add(
|
|
8288
|
-
(0, import_node_crypto16.createHash)("sha256").update(stableJsonStringify([call.name, finalInput])).digest("hex")
|
|
8289
|
-
);
|
|
8290
|
-
}
|
|
8291
|
-
try {
|
|
8292
|
-
await audit("tool.finished", {
|
|
8293
|
-
status: resultBlock.isError ? "failed" : "completed",
|
|
8294
|
-
...resultBlock.errorCode ? { errorCode: resultBlock.errorCode } : {},
|
|
8295
|
-
durationMs: resultBlock.durationMs
|
|
8296
|
-
});
|
|
8297
|
-
} catch {
|
|
8298
|
-
resultBlock.isError = true;
|
|
8299
|
-
resultBlock.errorCode = "OUTCOME_UNKNOWN";
|
|
8300
|
-
resultBlock.retryPolicy = "after_user_action";
|
|
8301
|
-
resultBlock.content = JSON.stringify({
|
|
8302
|
-
code: "OUTCOME_UNKNOWN",
|
|
8303
|
-
retryPolicy: "after_user_action",
|
|
8304
|
-
message: "Tool outcome is unknown because the audit result could not be written"
|
|
8305
|
-
});
|
|
8306
|
-
resultBlock.execution = {
|
|
8307
|
-
version: 1,
|
|
8308
|
-
dispatchOutcome: resultBlock.execution?.dispatchOutcome ?? "succeeded",
|
|
8309
|
-
hostError: {
|
|
8310
|
-
phase: "audit",
|
|
8311
|
-
code: "AUDIT_UNAVAILABLE"
|
|
8312
|
-
}
|
|
8313
|
-
};
|
|
8314
|
-
return resultBlock;
|
|
8315
|
-
}
|
|
8316
|
-
await this.onToolResult?.(tool, finalInput, resultBlock);
|
|
8317
|
-
return resultBlock;
|
|
8318
|
-
}
|
|
8319
|
-
/**
|
|
8320
|
-
* 给工具执行加超时护栏:Promise.race 在超时后reject,循环拿到 isError 结果继续运转,
|
|
8321
|
-
* 不会因某个异步工具(如未来接入的网络/exec 工具)挂起而永久卡死。
|
|
8322
|
-
*
|
|
8323
|
-
* 局限:同步 CPU 密集型实现会阻塞事件循环,timer 同样无法触发;
|
|
8324
|
-
* 因此内置工具仍需避免执行用户可控的同步高复杂度计算。
|
|
8325
|
-
*/
|
|
8326
|
-
withTimeout(promise, toolName, executionController) {
|
|
8327
|
-
let timer;
|
|
8328
|
-
const timeout = new Promise((_, reject) => {
|
|
8329
|
-
timer = setTimeout(() => {
|
|
8330
|
-
const error = new KapibalaError(
|
|
8331
|
-
`Tool '${toolName}' timed out after ${this.toolTimeoutMs}ms`,
|
|
8332
|
-
{
|
|
8333
|
-
code: "OUTCOME_UNKNOWN",
|
|
8334
|
-
retryPolicy: "after_user_action",
|
|
8335
|
-
safeMessage: "Tool timed out; completion of side effects is unknown"
|
|
8336
|
-
}
|
|
8337
|
-
);
|
|
8338
|
-
reject(error);
|
|
8339
|
-
executionController.abort(error);
|
|
8340
|
-
}, this.toolTimeoutMs);
|
|
8615
|
+
return closeToolResult({
|
|
8616
|
+
call,
|
|
8617
|
+
tool,
|
|
8618
|
+
finalInput,
|
|
8619
|
+
hookCtx,
|
|
8620
|
+
hooks: this.hooks,
|
|
8621
|
+
neverFailures: this.neverFailures,
|
|
8622
|
+
audit,
|
|
8623
|
+
onToolResult: this.onToolResult,
|
|
8624
|
+
startedAt,
|
|
8625
|
+
output: { content: outputStr, isError, errorCode, retryPolicy, execution }
|
|
8341
8626
|
});
|
|
8342
|
-
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
8343
8627
|
}
|
|
8344
8628
|
/**
|
|
8345
8629
|
* 构建当前会话在派生时刻的父权限策略快照。
|
|
@@ -8938,7 +9222,7 @@ var OpenAICompatibleProvider = class {
|
|
|
8938
9222
|
};
|
|
8939
9223
|
|
|
8940
9224
|
// src/models/anthropic/index.ts
|
|
8941
|
-
var
|
|
9225
|
+
var import_node_crypto18 = require("crypto");
|
|
8942
9226
|
function invalidResponse2() {
|
|
8943
9227
|
throw new ModelError("Invalid Anthropic response state", {
|
|
8944
9228
|
code: "MODEL_INVALID_RESPONSE",
|
|
@@ -8953,7 +9237,7 @@ function wireToolId(id) {
|
|
|
8953
9237
|
stage: "request",
|
|
8954
9238
|
retryable: false
|
|
8955
9239
|
});
|
|
8956
|
-
return /^[A-Za-z0-9_-]{1,64}$/.test(id) ? id : `call_${(0,
|
|
9240
|
+
return /^[A-Za-z0-9_-]{1,64}$/.test(id) ? id : `call_${(0, import_node_crypto18.createHash)("sha256").update(id).digest("hex").slice(0, 48)}`;
|
|
8957
9241
|
}
|
|
8958
9242
|
var AnthropicProvider = class {
|
|
8959
9243
|
name = "anthropic";
|
|
@@ -9319,7 +9603,7 @@ var AnthropicProvider = class {
|
|
|
9319
9603
|
};
|
|
9320
9604
|
|
|
9321
9605
|
// src/models/openai-responses/index.ts
|
|
9322
|
-
var
|
|
9606
|
+
var import_node_crypto19 = require("crypto");
|
|
9323
9607
|
function invalid(message = "Invalid Responses output Item") {
|
|
9324
9608
|
throw new ModelError(message, {
|
|
9325
9609
|
code: "MODEL_INVALID_RESPONSE",
|
|
@@ -9340,7 +9624,7 @@ function wireCallId(id) {
|
|
|
9340
9624
|
stage: "request",
|
|
9341
9625
|
retryable: false
|
|
9342
9626
|
});
|
|
9343
|
-
return /^[A-Za-z0-9_-]{1,64}$/.test(id) ? id : `call_${(0,
|
|
9627
|
+
return /^[A-Za-z0-9_-]{1,64}$/.test(id) ? id : `call_${(0, import_node_crypto19.createHash)("sha256").update(id).digest("hex").slice(0, 48)}`;
|
|
9344
9628
|
}
|
|
9345
9629
|
function validateItem(item, final) {
|
|
9346
9630
|
if (!item || typeof item !== "object" || !nonempty(item.id)) invalid();
|
|
@@ -10037,13 +10321,99 @@ Follow these key behavioral rules:
|
|
|
10037
10321
|
}
|
|
10038
10322
|
return lines.join("\n");
|
|
10039
10323
|
}
|
|
10040
|
-
getL3Environment() {
|
|
10041
|
-
const platform = import_node_os6.default.platform();
|
|
10042
|
-
return `# Environment Context:
|
|
10043
|
-
- Current Working Directory (Workspace Root): ${this.rootDir}
|
|
10044
|
-
- Operating System: ${platform}`;
|
|
10324
|
+
getL3Environment() {
|
|
10325
|
+
const platform = import_node_os6.default.platform();
|
|
10326
|
+
return `# Environment Context:
|
|
10327
|
+
- Current Working Directory (Workspace Root): ${this.rootDir}
|
|
10328
|
+
- Operating System: ${platform}`;
|
|
10329
|
+
}
|
|
10330
|
+
};
|
|
10331
|
+
|
|
10332
|
+
// src/runtime/loop/hook-integrity.ts
|
|
10333
|
+
function assertHookToolDefinitionsIntact(request, snapshot) {
|
|
10334
|
+
if (!request.tools) return;
|
|
10335
|
+
for (const t of request.tools) {
|
|
10336
|
+
const original = snapshot.get(t.name);
|
|
10337
|
+
if (!original) {
|
|
10338
|
+
throw new ModelError(`Hook injected tool '${t.name}' not present in snapshot`, {
|
|
10339
|
+
stage: "request",
|
|
10340
|
+
retryable: false
|
|
10341
|
+
});
|
|
10342
|
+
}
|
|
10343
|
+
if (t.description !== original.description || JSON.stringify(t.parameters) !== JSON.stringify(original.parameters)) {
|
|
10344
|
+
throw new ModelError(
|
|
10345
|
+
`Hook mutated tool definition for '${t.name}' (description or parameters drifted from snapshot)`,
|
|
10346
|
+
{
|
|
10347
|
+
stage: "request",
|
|
10348
|
+
retryable: false
|
|
10349
|
+
}
|
|
10350
|
+
);
|
|
10351
|
+
}
|
|
10352
|
+
}
|
|
10353
|
+
}
|
|
10354
|
+
function snapshotAssistantIntegrity(message) {
|
|
10355
|
+
return {
|
|
10356
|
+
identity: stableSerialize({ ...message, content: void 0 }),
|
|
10357
|
+
originalTools: stableSerialize(
|
|
10358
|
+
message.content.filter((block) => block.type === "tool_use" || block.type === "tool_result")
|
|
10359
|
+
),
|
|
10360
|
+
originalContent: stableSerialize(message.content),
|
|
10361
|
+
privateOutput: message.content.some(
|
|
10362
|
+
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.origin || block.signature)
|
|
10363
|
+
),
|
|
10364
|
+
hasToolCalls: message.content.some((block) => block.type === "tool_use")
|
|
10365
|
+
};
|
|
10366
|
+
}
|
|
10367
|
+
function assertAssistantMessageUntampered(message, snapshot) {
|
|
10368
|
+
if (snapshot.identity !== stableSerialize({ ...message, content: void 0 }))
|
|
10369
|
+
throw new ModelError("Hook changed assistant message identity", {
|
|
10370
|
+
stage: "request",
|
|
10371
|
+
retryable: false
|
|
10372
|
+
});
|
|
10373
|
+
if (snapshot.originalTools !== stableSerialize(
|
|
10374
|
+
message.content.filter((block) => block.type === "tool_use" || block.type === "tool_result")
|
|
10375
|
+
))
|
|
10376
|
+
throw new ModelError("Hook changed validated tool model output", {
|
|
10377
|
+
stage: "request",
|
|
10378
|
+
retryable: false
|
|
10379
|
+
});
|
|
10380
|
+
if (snapshot.originalContent !== stableSerialize(message.content)) {
|
|
10381
|
+
if (snapshot.privateOutput || message.content.some(
|
|
10382
|
+
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.origin || block.signature)
|
|
10383
|
+
))
|
|
10384
|
+
throw new ModelError("Hook changed signed or private model output", {
|
|
10385
|
+
stage: "request",
|
|
10386
|
+
retryable: false
|
|
10387
|
+
});
|
|
10388
|
+
if (snapshot.hasToolCalls)
|
|
10389
|
+
throw new ModelError("Hook changed validated tool model output", {
|
|
10390
|
+
stage: "request",
|
|
10391
|
+
retryable: false
|
|
10392
|
+
});
|
|
10393
|
+
for (const block of message.content)
|
|
10394
|
+
if (block.type === "text" || block.type === "tool_use") block.protocolMeta = void 0;
|
|
10395
|
+
}
|
|
10396
|
+
}
|
|
10397
|
+
|
|
10398
|
+
// src/runtime/loop/tool-call-validation.ts
|
|
10399
|
+
function assertToolCallsExecutable(history, toolCalls) {
|
|
10400
|
+
const knownCalls = new Set(
|
|
10401
|
+
history.flatMap(
|
|
10402
|
+
(message) => message.content.filter((block) => block.type === "tool_use").map((block) => block.id)
|
|
10403
|
+
)
|
|
10404
|
+
);
|
|
10405
|
+
for (const call of toolCalls) {
|
|
10406
|
+
if (!call.name?.trim() || !call.input || typeof call.input !== "object" || Array.isArray(call.input))
|
|
10407
|
+
throw new ModelError("Invalid tool call arguments or name; no tools executed", {
|
|
10408
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
10409
|
+
stage: "stream",
|
|
10410
|
+
retryable: false
|
|
10411
|
+
});
|
|
10412
|
+
if (!call.id?.trim() || knownCalls.has(call.id))
|
|
10413
|
+
throw new ModelError("Duplicate tool call ID; no tools executed");
|
|
10414
|
+
knownCalls.add(call.id);
|
|
10045
10415
|
}
|
|
10046
|
-
}
|
|
10416
|
+
}
|
|
10047
10417
|
|
|
10048
10418
|
// src/runtime/loop/index.ts
|
|
10049
10419
|
var AgentLoop = class {
|
|
@@ -10127,26 +10497,7 @@ var AgentLoop = class {
|
|
|
10127
10497
|
request = modified;
|
|
10128
10498
|
}
|
|
10129
10499
|
}
|
|
10130
|
-
|
|
10131
|
-
for (const t of request.tools) {
|
|
10132
|
-
const original = toolSnapshot.get(t.name);
|
|
10133
|
-
if (!original) {
|
|
10134
|
-
throw new ModelError(`Hook injected tool '${t.name}' not present in snapshot`, {
|
|
10135
|
-
stage: "request",
|
|
10136
|
-
retryable: false
|
|
10137
|
-
});
|
|
10138
|
-
}
|
|
10139
|
-
if (t.description !== original.description || JSON.stringify(t.parameters) !== JSON.stringify(original.parameters)) {
|
|
10140
|
-
throw new ModelError(
|
|
10141
|
-
`Hook mutated tool definition for '${t.name}' (description or parameters drifted from snapshot)`,
|
|
10142
|
-
{
|
|
10143
|
-
stage: "request",
|
|
10144
|
-
retryable: false
|
|
10145
|
-
}
|
|
10146
|
-
);
|
|
10147
|
-
}
|
|
10148
|
-
}
|
|
10149
|
-
}
|
|
10500
|
+
assertHookToolDefinitionsIntact(request, toolSnapshot);
|
|
10150
10501
|
if (this.prepareRequest) request = yield* this.prepareRequest(request, history);
|
|
10151
10502
|
yield {
|
|
10152
10503
|
type: "step_log",
|
|
@@ -10297,22 +10648,7 @@ var AgentLoop = class {
|
|
|
10297
10648
|
stage: "stream",
|
|
10298
10649
|
retryable: false
|
|
10299
10650
|
});
|
|
10300
|
-
|
|
10301
|
-
history.flatMap(
|
|
10302
|
-
(message) => message.content.filter((block) => block.type === "tool_use").map((block) => block.id)
|
|
10303
|
-
)
|
|
10304
|
-
);
|
|
10305
|
-
for (const call of toolCalls) {
|
|
10306
|
-
if (!call.name?.trim() || !call.input || typeof call.input !== "object" || Array.isArray(call.input))
|
|
10307
|
-
throw new ModelError("Invalid tool call arguments or name; no tools executed", {
|
|
10308
|
-
code: "MODEL_INVALID_RESPONSE",
|
|
10309
|
-
stage: "stream",
|
|
10310
|
-
retryable: false
|
|
10311
|
-
});
|
|
10312
|
-
if (!call.id?.trim() || knownCalls.has(call.id))
|
|
10313
|
-
throw new ModelError("Duplicate tool call ID; no tools executed");
|
|
10314
|
-
knownCalls.add(call.id);
|
|
10315
|
-
}
|
|
10651
|
+
assertToolCallsExecutable(history, toolCalls);
|
|
10316
10652
|
break;
|
|
10317
10653
|
} catch (error) {
|
|
10318
10654
|
if (!(error instanceof ContextOverflowError) || outputSeen || accumulatedText || accumulatedThinking || toolCalls.length || overflowRetried || !this.prepareRequest || this.signal?.aborted)
|
|
@@ -10425,47 +10761,9 @@ var AgentLoop = class {
|
|
|
10425
10761
|
timestamp: Date.now()
|
|
10426
10762
|
};
|
|
10427
10763
|
validateProtocolContent(assistantMessage.content);
|
|
10428
|
-
const
|
|
10429
|
-
const identity = stableSerialize({ ...assistantMessage, content: void 0 });
|
|
10430
|
-
const originalTools = stableSerialize(
|
|
10431
|
-
assistantMessage.content.filter(
|
|
10432
|
-
(block) => block.type === "tool_use" || block.type === "tool_result"
|
|
10433
|
-
)
|
|
10434
|
-
);
|
|
10435
|
-
const privateOutput = assistantMessage.content.some(
|
|
10436
|
-
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.origin || block.signature)
|
|
10437
|
-
);
|
|
10764
|
+
const integrity = snapshotAssistantIntegrity(assistantMessage);
|
|
10438
10765
|
await this.hooks.emit("model:after", hookCtx, { message: assistantMessage });
|
|
10439
|
-
|
|
10440
|
-
throw new ModelError("Hook changed assistant message identity", {
|
|
10441
|
-
stage: "request",
|
|
10442
|
-
retryable: false
|
|
10443
|
-
});
|
|
10444
|
-
if (originalTools !== stableSerialize(
|
|
10445
|
-
assistantMessage.content.filter(
|
|
10446
|
-
(block) => block.type === "tool_use" || block.type === "tool_result"
|
|
10447
|
-
)
|
|
10448
|
-
))
|
|
10449
|
-
throw new ModelError("Hook changed validated tool model output", {
|
|
10450
|
-
stage: "request",
|
|
10451
|
-
retryable: false
|
|
10452
|
-
});
|
|
10453
|
-
if (originalContent !== stableSerialize(assistantMessage.content)) {
|
|
10454
|
-
if (privateOutput || assistantMessage.content.some(
|
|
10455
|
-
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.origin || block.signature)
|
|
10456
|
-
))
|
|
10457
|
-
throw new ModelError("Hook changed signed or private model output", {
|
|
10458
|
-
stage: "request",
|
|
10459
|
-
retryable: false
|
|
10460
|
-
});
|
|
10461
|
-
if (toolCalls.length)
|
|
10462
|
-
throw new ModelError("Hook changed validated tool model output", {
|
|
10463
|
-
stage: "request",
|
|
10464
|
-
retryable: false
|
|
10465
|
-
});
|
|
10466
|
-
for (const block of assistantMessage.content)
|
|
10467
|
-
if (block.type === "text" || block.type === "tool_use") block.protocolMeta = void 0;
|
|
10468
|
-
}
|
|
10766
|
+
assertAssistantMessageUntampered(assistantMessage, integrity);
|
|
10469
10767
|
validateProtocolContent(assistantMessage.content);
|
|
10470
10768
|
if (toolCalls.length === 0) {
|
|
10471
10769
|
history.push(assistantMessage);
|
|
@@ -11234,7 +11532,6 @@ var AgentRuntime = class {
|
|
|
11234
11532
|
task.status = "failed";
|
|
11235
11533
|
}
|
|
11236
11534
|
task.error = runError.message;
|
|
11237
|
-
await this.cascadeCancelChildren(task.agentId, "Parent task failed");
|
|
11238
11535
|
} finally {
|
|
11239
11536
|
this.runnerContexts.delete(task.agentId);
|
|
11240
11537
|
this.clearTaskTimer(task.agentId);
|
|
@@ -11243,6 +11540,9 @@ var AgentRuntime = class {
|
|
|
11243
11540
|
task.status = "failed";
|
|
11244
11541
|
task.error = runError?.message ?? "Unknown execution failure";
|
|
11245
11542
|
}
|
|
11543
|
+
if (task.status === "failed") {
|
|
11544
|
+
await this.cascadeCancelChildren(task.agentId, task.error ?? "Parent task failed");
|
|
11545
|
+
}
|
|
11246
11546
|
task.finishedAt = Date.now();
|
|
11247
11547
|
this.releaseTaskSlotOnce(task);
|
|
11248
11548
|
this.outbox.enqueueFromTask(task);
|
|
@@ -11849,6 +12149,274 @@ function cancellableGenerator(create, external) {
|
|
|
11849
12149
|
};
|
|
11850
12150
|
}
|
|
11851
12151
|
|
|
12152
|
+
// src/context/session/persistence.ts
|
|
12153
|
+
async function persistAssistantMessage(event, store, markUnknownUsage) {
|
|
12154
|
+
if (store) await store.append(event.message);
|
|
12155
|
+
if (!event.usage) markUnknownUsage();
|
|
12156
|
+
}
|
|
12157
|
+
async function persistToolMessages(messages, store) {
|
|
12158
|
+
if (!store) return;
|
|
12159
|
+
for (const message of messages) await store.append(message);
|
|
12160
|
+
}
|
|
12161
|
+
function buildInterruptedToolMessage(assistantEvent) {
|
|
12162
|
+
return {
|
|
12163
|
+
id: createMessageId(),
|
|
12164
|
+
role: "tool",
|
|
12165
|
+
content: assistantEvent.message.content.filter((block) => block.type === "tool_use").map((toolUse) => ({
|
|
12166
|
+
type: "tool_result",
|
|
12167
|
+
toolUseId: toolUse.id,
|
|
12168
|
+
content: JSON.stringify({
|
|
12169
|
+
code: "OUTCOME_UNKNOWN",
|
|
12170
|
+
retryPolicy: "after_user_action",
|
|
12171
|
+
message: "Tool execution was interrupted; outcome is unknown"
|
|
12172
|
+
}),
|
|
12173
|
+
isError: true,
|
|
12174
|
+
errorCode: "OUTCOME_UNKNOWN",
|
|
12175
|
+
retryPolicy: "after_user_action"
|
|
12176
|
+
})),
|
|
12177
|
+
timestamp: Date.now()
|
|
12178
|
+
};
|
|
12179
|
+
}
|
|
12180
|
+
function findClosedToolMessages(history, assistantEvent) {
|
|
12181
|
+
const assistantIndex = history.lastIndexOf(assistantEvent.message);
|
|
12182
|
+
if (assistantIndex < 0) return [];
|
|
12183
|
+
const expected = new Set(
|
|
12184
|
+
assistantEvent.message.content.filter((block) => block.type === "tool_use").map((toolUse) => toolUse.id)
|
|
12185
|
+
);
|
|
12186
|
+
const messages = [];
|
|
12187
|
+
for (let index2 = assistantIndex + 1; index2 < history.length; index2++) {
|
|
12188
|
+
const message = history[index2];
|
|
12189
|
+
if (message.role !== "tool") break;
|
|
12190
|
+
messages.push(message);
|
|
12191
|
+
for (const block of message.content) {
|
|
12192
|
+
if (block.type === "tool_result") expected.delete(block.toolUseId);
|
|
12193
|
+
}
|
|
12194
|
+
}
|
|
12195
|
+
return expected.size === 0 ? messages : [];
|
|
12196
|
+
}
|
|
12197
|
+
|
|
12198
|
+
// src/context/session/request-prepare.ts
|
|
12199
|
+
function verifyPrivateBlockIntegrity(projected, sourceView) {
|
|
12200
|
+
for (const message of projected) {
|
|
12201
|
+
const original = sourceView.find((source) => source.id === message.id);
|
|
12202
|
+
const privateBag = /* @__PURE__ */ new Map();
|
|
12203
|
+
const blockBag = /* @__PURE__ */ new Map();
|
|
12204
|
+
for (const block of original?.content ?? []) {
|
|
12205
|
+
const serialized = stableSerialize(block);
|
|
12206
|
+
if (block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && block.origin)
|
|
12207
|
+
privateBag.set(serialized, (privateBag.get(serialized) ?? 0) + 1);
|
|
12208
|
+
blockBag.set(serialized, (blockBag.get(serialized) ?? 0) + 1);
|
|
12209
|
+
}
|
|
12210
|
+
const take = (bag, serialized) => {
|
|
12211
|
+
const count = bag.get(serialized) ?? 0;
|
|
12212
|
+
if (!count) return false;
|
|
12213
|
+
if (count === 1) bag.delete(serialized);
|
|
12214
|
+
else bag.set(serialized, count - 1);
|
|
12215
|
+
return true;
|
|
12216
|
+
};
|
|
12217
|
+
for (const block of message.content) {
|
|
12218
|
+
if (block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && block.origin) {
|
|
12219
|
+
if (!original || !take(privateBag, stableSerialize(block)))
|
|
12220
|
+
throw new ModelError("Hook changed signed or private request provenance", {
|
|
12221
|
+
stage: "request",
|
|
12222
|
+
retryable: false
|
|
12223
|
+
});
|
|
12224
|
+
}
|
|
12225
|
+
if (block.type !== "text" && block.type !== "tool_use") continue;
|
|
12226
|
+
if (block.protocolMeta && !take(blockBag, stableSerialize(block)))
|
|
12227
|
+
block.protocolMeta = void 0;
|
|
12228
|
+
}
|
|
12229
|
+
}
|
|
12230
|
+
}
|
|
12231
|
+
function assertSignedContinuationStable(check) {
|
|
12232
|
+
const { candidate, previousRequest, history, runId, nativeBinding, projectHistory } = check;
|
|
12233
|
+
const frozen = Boolean(
|
|
12234
|
+
previousRequest && history.some(
|
|
12235
|
+
(message) => message.interactionId === runId && message.content.some(
|
|
12236
|
+
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.signature && block.origin?.runId === runId)
|
|
12237
|
+
)
|
|
12238
|
+
)
|
|
12239
|
+
);
|
|
12240
|
+
if (frozen) {
|
|
12241
|
+
const generated = nativeBinding ? projectProtocolHistory(
|
|
12242
|
+
projectHistory(history).filter(
|
|
12243
|
+
(message) => message.interactionId === runId && message.role !== "user"
|
|
12244
|
+
),
|
|
12245
|
+
nativeBinding,
|
|
12246
|
+
candidate.context
|
|
12247
|
+
) : [];
|
|
12248
|
+
for (const original of generated) {
|
|
12249
|
+
const matches = candidate.messages.filter((message) => message.id === original.id);
|
|
12250
|
+
if (matches.length !== 1 || stableSerialize(matches[0]) !== stableSerialize(original))
|
|
12251
|
+
throw new ModelError("Signed continuation prefix or tool chain changed", {
|
|
12252
|
+
stage: "request",
|
|
12253
|
+
retryable: false
|
|
12254
|
+
});
|
|
12255
|
+
}
|
|
12256
|
+
}
|
|
12257
|
+
if (frozen && previousRequest && (stableSerialize(candidate.messages.slice(0, previousRequest.messages.length)) !== stableSerialize(previousRequest.messages) || candidate.systemPrompt !== previousRequest.systemPrompt || stableSerialize(candidate.tools) !== stableSerialize(previousRequest.tools)))
|
|
12258
|
+
throw new ModelError("Signed continuation prefix changed", {
|
|
12259
|
+
stage: "request",
|
|
12260
|
+
retryable: false
|
|
12261
|
+
});
|
|
12262
|
+
return frozen;
|
|
12263
|
+
}
|
|
12264
|
+
|
|
12265
|
+
// src/context/session/skills.ts
|
|
12266
|
+
function createSkillResourceAuditor(eventLogger, sessionId) {
|
|
12267
|
+
return async (source, input, page) => {
|
|
12268
|
+
try {
|
|
12269
|
+
await eventLogger.recordAudit({
|
|
12270
|
+
level: "info",
|
|
12271
|
+
event: "skill.resource_read",
|
|
12272
|
+
sessionId,
|
|
12273
|
+
runId: source.runId,
|
|
12274
|
+
fields: {
|
|
12275
|
+
skillName: source.name,
|
|
12276
|
+
contentDigest: source.contentDigest,
|
|
12277
|
+
invocationId: source.invocationId,
|
|
12278
|
+
pathDigest: skillDigest(input.path),
|
|
12279
|
+
resourceDigest: skillDigest(page.text),
|
|
12280
|
+
outputBytes: Buffer.byteLength(page.text),
|
|
12281
|
+
outputTruncated: page.truncated,
|
|
12282
|
+
offset: input.offset,
|
|
12283
|
+
startLine: input.startLine,
|
|
12284
|
+
endLine: input.endLine
|
|
12285
|
+
}
|
|
12286
|
+
});
|
|
12287
|
+
} catch {
|
|
12288
|
+
throw new KapibalaError("Skill resource audit unavailable", {
|
|
12289
|
+
code: "AUDIT_UNAVAILABLE",
|
|
12290
|
+
retryPolicy: "after_user_action",
|
|
12291
|
+
safeMessage: "\u6280\u80FD\u8D44\u6E90\u8BFB\u53D6\u5BA1\u8BA1\u4E0D\u53EF\u7528\uFF0C\u672C\u8F6E\u505C\u6B62"
|
|
12292
|
+
});
|
|
12293
|
+
}
|
|
12294
|
+
};
|
|
12295
|
+
}
|
|
12296
|
+
async function commitSkillEvents(deps) {
|
|
12297
|
+
const { skillRuntime, eventLogger, sessionId, runId, persistence } = deps;
|
|
12298
|
+
if (!skillRuntime) return [];
|
|
12299
|
+
return skillRuntime.commit(async (source, messageId) => {
|
|
12300
|
+
try {
|
|
12301
|
+
await eventLogger.recordAudit({
|
|
12302
|
+
level: "info",
|
|
12303
|
+
event: "skill.loaded",
|
|
12304
|
+
sessionId,
|
|
12305
|
+
runId,
|
|
12306
|
+
fields: {
|
|
12307
|
+
skillName: source.name,
|
|
12308
|
+
skillScope: source.scope,
|
|
12309
|
+
contentDigest: source.contentDigest,
|
|
12310
|
+
renderedDigest: source.renderedDigest,
|
|
12311
|
+
pathDigest: skillDigest(source.filePath),
|
|
12312
|
+
caller: source.caller,
|
|
12313
|
+
invocationId: source.invocationId,
|
|
12314
|
+
messageId
|
|
12315
|
+
}
|
|
12316
|
+
});
|
|
12317
|
+
} catch {
|
|
12318
|
+
throw new KapibalaError("Skill commit audit unavailable", {
|
|
12319
|
+
code: "AUDIT_UNAVAILABLE",
|
|
12320
|
+
retryPolicy: "after_user_action",
|
|
12321
|
+
safeMessage: "\u6280\u80FD\u63D0\u4EA4\u5BA1\u8BA1\u4E0D\u53EF\u7528\uFF0C\u672C\u8F6E\u505C\u6B62"
|
|
12322
|
+
});
|
|
12323
|
+
}
|
|
12324
|
+
}, persistence);
|
|
12325
|
+
}
|
|
12326
|
+
async function authorizeExplicitSkill(deps) {
|
|
12327
|
+
const {
|
|
12328
|
+
name,
|
|
12329
|
+
runId,
|
|
12330
|
+
signal,
|
|
12331
|
+
rootDir,
|
|
12332
|
+
mode,
|
|
12333
|
+
permissionRules,
|
|
12334
|
+
approvalChannel,
|
|
12335
|
+
approvalCache,
|
|
12336
|
+
eventLogger,
|
|
12337
|
+
sessionId
|
|
12338
|
+
} = deps;
|
|
12339
|
+
const request = {
|
|
12340
|
+
toolName: "load_skill",
|
|
12341
|
+
capabilities: ["fs:read"],
|
|
12342
|
+
input: { name },
|
|
12343
|
+
rootDir,
|
|
12344
|
+
sessionAllowed: true
|
|
12345
|
+
};
|
|
12346
|
+
const cached = approvalCache.get(request);
|
|
12347
|
+
const decision = new PermissionPolicy().decide({
|
|
12348
|
+
toolName: request.toolName,
|
|
12349
|
+
capabilities: request.capabilities,
|
|
12350
|
+
mode,
|
|
12351
|
+
rules: permissionRules,
|
|
12352
|
+
cachedApproval: cached === "allow",
|
|
12353
|
+
cachedDenial: cached === "deny"
|
|
12354
|
+
});
|
|
12355
|
+
let permitted = decision.decision === "allow";
|
|
12356
|
+
let approvalChoice;
|
|
12357
|
+
if (decision.decision === "ask") {
|
|
12358
|
+
try {
|
|
12359
|
+
approvalChoice = await approvalChannel?.requestApproval(request, signal) ?? "deny_once";
|
|
12360
|
+
} catch {
|
|
12361
|
+
approvalChoice = "cancel";
|
|
12362
|
+
}
|
|
12363
|
+
permitted = approvalChoice === "allow_once" || approvalChoice === "allow_session";
|
|
12364
|
+
if (approvalChoice === "allow_session") approvalCache.set(request, "allow");
|
|
12365
|
+
if (approvalChoice === "deny_session") approvalCache.set(request, "deny");
|
|
12366
|
+
}
|
|
12367
|
+
await eventLogger.recordAudit({
|
|
12368
|
+
level: "info",
|
|
12369
|
+
event: "skill.decided",
|
|
12370
|
+
sessionId,
|
|
12371
|
+
runId,
|
|
12372
|
+
fields: {
|
|
12373
|
+
skillName: name,
|
|
12374
|
+
caller: "user",
|
|
12375
|
+
decision: permitted && !signal?.aborted ? "allow" : "deny",
|
|
12376
|
+
decisionSource: decision.decision === "ask" ? approvalChannel ? "human" : "non_interactive" : decision.source,
|
|
12377
|
+
approvalChoice
|
|
12378
|
+
}
|
|
12379
|
+
});
|
|
12380
|
+
if (!permitted) throw skillFailure("SKILL_INVOCATION_FORBIDDEN", "\u5BBF\u4E3B\u6743\u9650\u7981\u6B62\u663E\u5F0F\u6280\u80FD\u8BFB\u53D6");
|
|
12381
|
+
signal?.throwIfAborted();
|
|
12382
|
+
}
|
|
12383
|
+
|
|
12384
|
+
// src/context/session/usage.ts
|
|
12385
|
+
function recordUsage(attemptId, usage, accountedAttempts, usageStats) {
|
|
12386
|
+
if (accountedAttempts.has(attemptId)) return "duplicate";
|
|
12387
|
+
if (!usage || ![usage.promptTokens, usage.completionTokens, usage.totalTokens].every(
|
|
12388
|
+
(n) => Number.isSafeInteger(n) && n >= 0
|
|
12389
|
+
))
|
|
12390
|
+
return "unknown";
|
|
12391
|
+
accountedAttempts.add(attemptId);
|
|
12392
|
+
usageStats.promptTokens += usage.promptTokens;
|
|
12393
|
+
usageStats.completionTokens += usage.completionTokens;
|
|
12394
|
+
usageStats.totalTokens += usage.totalTokens;
|
|
12395
|
+
return "recorded";
|
|
12396
|
+
}
|
|
12397
|
+
function recordSummaryUsage(attemptId, usage, summaryAttempts, summaryUsage) {
|
|
12398
|
+
if (summaryAttempts.has(attemptId)) return "duplicate";
|
|
12399
|
+
if (!usage || ![usage.promptTokens, usage.completionTokens, usage.totalTokens].every(
|
|
12400
|
+
(n) => Number.isSafeInteger(n) && n >= 0
|
|
12401
|
+
))
|
|
12402
|
+
return "unknown";
|
|
12403
|
+
summaryAttempts.add(attemptId);
|
|
12404
|
+
summaryUsage.promptTokens += usage.promptTokens;
|
|
12405
|
+
summaryUsage.completionTokens += usage.completionTokens;
|
|
12406
|
+
summaryUsage.totalTokens += usage.totalTokens;
|
|
12407
|
+
return "recorded";
|
|
12408
|
+
}
|
|
12409
|
+
function computeContextUsage(profile, usedTokens, estimatedUsage = false) {
|
|
12410
|
+
const contextWindow = resolveContextWindow(profile.contextWindow);
|
|
12411
|
+
return {
|
|
12412
|
+
usedTokens,
|
|
12413
|
+
limitTokens: contextWindow.tokens,
|
|
12414
|
+
percent: usedTokens === void 0 ? void 0 : usedTokens / contextWindow.tokens * 100,
|
|
12415
|
+
estimatedLimit: contextWindow.estimated,
|
|
12416
|
+
...estimatedUsage ? { estimatedUsage: true } : {}
|
|
12417
|
+
};
|
|
12418
|
+
}
|
|
12419
|
+
|
|
11852
12420
|
// src/context/session/index.ts
|
|
11853
12421
|
var AgentSession = class {
|
|
11854
12422
|
skillRegistry;
|
|
@@ -11934,40 +12502,26 @@ var AgentSession = class {
|
|
|
11934
12502
|
this.projectKey = resolvedWorkspace.projectKey;
|
|
11935
12503
|
this.sandbox = config.sandbox ?? new PathSandbox({ workspaceRoot: this.workspaceRoot });
|
|
11936
12504
|
this.leaseCoordinator = config.leaseCoordinator ?? new WorkspaceLeaseCoordinator();
|
|
12505
|
+
this.userInstructionsPath = config.userInstructionsPath;
|
|
12506
|
+
const sinks = config.eventLogger ? void 0 : createDefaultLogSinks(config.loggingDirectory);
|
|
12507
|
+
const eventLogger = config.eventLogger ?? new StructuredLogger({
|
|
12508
|
+
operationSink: sinks.operationSink,
|
|
12509
|
+
auditSink: sinks.auditSink,
|
|
12510
|
+
debug: config.logger !== void 0,
|
|
12511
|
+
onDiagnostic: config.onDiagnostic ?? config.logger
|
|
12512
|
+
});
|
|
12513
|
+
this.eventLogger = {
|
|
12514
|
+
record: (input) => eventLogger.record({ ...input, conversationId: this.persistentId }),
|
|
12515
|
+
recordAudit: (input) => eventLogger.recordAudit({ ...input, conversationId: this.persistentId })
|
|
12516
|
+
};
|
|
12517
|
+
this.auditRecoveryDirectory = config.auditRecoveryDirectory ?? sinks?.auditSink.directory;
|
|
11937
12518
|
this.tools = new ToolRegistry();
|
|
11938
12519
|
if (config.skills) {
|
|
11939
12520
|
this.skillRegistry = new SkillRegistry(config.skills);
|
|
11940
12521
|
this.skillRuntime = new SkillRuntime(
|
|
11941
12522
|
this.skillRegistry,
|
|
11942
12523
|
this.tools,
|
|
11943
|
-
|
|
11944
|
-
try {
|
|
11945
|
-
await this.eventLogger.recordAudit({
|
|
11946
|
-
level: "info",
|
|
11947
|
-
event: "skill.resource_read",
|
|
11948
|
-
sessionId: this.sessionId,
|
|
11949
|
-
runId: source.runId,
|
|
11950
|
-
fields: {
|
|
11951
|
-
skillName: source.name,
|
|
11952
|
-
contentDigest: source.contentDigest,
|
|
11953
|
-
invocationId: source.invocationId,
|
|
11954
|
-
pathDigest: skillDigest(input.path),
|
|
11955
|
-
resourceDigest: skillDigest(page.text),
|
|
11956
|
-
outputBytes: Buffer.byteLength(page.text),
|
|
11957
|
-
outputTruncated: page.truncated,
|
|
11958
|
-
offset: input.offset,
|
|
11959
|
-
startLine: input.startLine,
|
|
11960
|
-
endLine: input.endLine
|
|
11961
|
-
}
|
|
11962
|
-
});
|
|
11963
|
-
} catch {
|
|
11964
|
-
throw new KapibalaError("Skill resource audit unavailable", {
|
|
11965
|
-
code: "AUDIT_UNAVAILABLE",
|
|
11966
|
-
retryPolicy: "after_user_action",
|
|
11967
|
-
safeMessage: "\u6280\u80FD\u8D44\u6E90\u8BFB\u53D6\u5BA1\u8BA1\u4E0D\u53EF\u7528\uFF0C\u672C\u8F6E\u505C\u6B62"
|
|
11968
|
-
});
|
|
11969
|
-
}
|
|
11970
|
-
}
|
|
12524
|
+
createSkillResourceAuditor(this.eventLogger, this.sessionId)
|
|
11971
12525
|
);
|
|
11972
12526
|
}
|
|
11973
12527
|
this.hooks = new HookRegistry();
|
|
@@ -11980,19 +12534,6 @@ var AgentSession = class {
|
|
|
11980
12534
|
this.mode = config.mode ?? "Approval";
|
|
11981
12535
|
this.permissionRules = config.permissionRules ?? [];
|
|
11982
12536
|
this.approvalChannel = config.approvalChannel;
|
|
11983
|
-
this.userInstructionsPath = config.userInstructionsPath;
|
|
11984
|
-
const sinks = config.eventLogger ? void 0 : createDefaultLogSinks(config.loggingDirectory);
|
|
11985
|
-
const eventLogger = config.eventLogger ?? new StructuredLogger({
|
|
11986
|
-
operationSink: sinks.operationSink,
|
|
11987
|
-
auditSink: sinks.auditSink,
|
|
11988
|
-
debug: config.logger !== void 0,
|
|
11989
|
-
onDiagnostic: config.onDiagnostic ?? config.logger
|
|
11990
|
-
});
|
|
11991
|
-
this.eventLogger = {
|
|
11992
|
-
record: (input) => eventLogger.record({ ...input, conversationId: this.persistentId }),
|
|
11993
|
-
recordAudit: (input) => eventLogger.recordAudit({ ...input, conversationId: this.persistentId })
|
|
11994
|
-
};
|
|
11995
|
-
this.auditRecoveryDirectory = config.auditRecoveryDirectory ?? sinks?.auditSink.directory;
|
|
11996
12537
|
this.rootTaskId = config.rootTaskId;
|
|
11997
12538
|
this.depth = config.depth ?? 0;
|
|
11998
12539
|
this.agentId = config.agentId;
|
|
@@ -12597,71 +13138,21 @@ var AgentSession = class {
|
|
|
12597
13138
|
runId,
|
|
12598
13139
|
modelId: profile.id
|
|
12599
13140
|
}) : this.context.project(history);
|
|
12600
|
-
|
|
12601
|
-
const original = sourceView.find((source) => source.id === message.id);
|
|
12602
|
-
const privateBag = /* @__PURE__ */ new Map();
|
|
12603
|
-
const blockBag = /* @__PURE__ */ new Map();
|
|
12604
|
-
for (const block of original?.content ?? []) {
|
|
12605
|
-
const serialized = stableSerialize(block);
|
|
12606
|
-
if (block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && block.origin)
|
|
12607
|
-
privateBag.set(serialized, (privateBag.get(serialized) ?? 0) + 1);
|
|
12608
|
-
blockBag.set(serialized, (blockBag.get(serialized) ?? 0) + 1);
|
|
12609
|
-
}
|
|
12610
|
-
const take = (bag, serialized) => {
|
|
12611
|
-
const count = bag.get(serialized) ?? 0;
|
|
12612
|
-
if (!count) return false;
|
|
12613
|
-
if (count === 1) bag.delete(serialized);
|
|
12614
|
-
else bag.set(serialized, count - 1);
|
|
12615
|
-
return true;
|
|
12616
|
-
};
|
|
12617
|
-
for (const block of message.content) {
|
|
12618
|
-
if (block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && block.origin) {
|
|
12619
|
-
if (!original || !take(privateBag, stableSerialize(block)))
|
|
12620
|
-
throw new ModelError("Hook changed signed or private request provenance", {
|
|
12621
|
-
stage: "request",
|
|
12622
|
-
retryable: false
|
|
12623
|
-
});
|
|
12624
|
-
}
|
|
12625
|
-
if (block.type !== "text" && block.type !== "tool_use") continue;
|
|
12626
|
-
if (block.protocolMeta && !take(blockBag, stableSerialize(block)))
|
|
12627
|
-
block.protocolMeta = void 0;
|
|
12628
|
-
}
|
|
12629
|
-
}
|
|
12630
|
-
const frozen = Boolean(
|
|
12631
|
-
previousRequest && history.some(
|
|
12632
|
-
(message) => message.interactionId === runId && message.content.some(
|
|
12633
|
-
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.signature && block.origin?.runId === runId)
|
|
12634
|
-
)
|
|
12635
|
-
)
|
|
12636
|
-
);
|
|
13141
|
+
verifyPrivateBlockIntegrity(projected, sourceView);
|
|
12637
13142
|
const candidate = {
|
|
12638
13143
|
...request,
|
|
12639
13144
|
messages: projected,
|
|
12640
13145
|
context: { runId, modelId: profile.id },
|
|
12641
13146
|
signal: options?.signal
|
|
12642
13147
|
};
|
|
12643
|
-
|
|
12644
|
-
|
|
12645
|
-
|
|
12646
|
-
|
|
12647
|
-
|
|
12648
|
-
|
|
12649
|
-
|
|
12650
|
-
|
|
12651
|
-
for (const original of generated) {
|
|
12652
|
-
const matches = candidate.messages.filter((message) => message.id === original.id);
|
|
12653
|
-
if (matches.length !== 1 || stableSerialize(matches[0]) !== stableSerialize(original))
|
|
12654
|
-
throw new ModelError("Signed continuation prefix or tool chain changed", {
|
|
12655
|
-
stage: "request",
|
|
12656
|
-
retryable: false
|
|
12657
|
-
});
|
|
12658
|
-
}
|
|
12659
|
-
}
|
|
12660
|
-
if (frozen && previousRequest && (stableSerialize(candidate.messages.slice(0, previousRequest.messages.length)) !== stableSerialize(previousRequest.messages) || candidate.systemPrompt !== previousRequest.systemPrompt || stableSerialize(candidate.tools) !== stableSerialize(previousRequest.tools)))
|
|
12661
|
-
throw new ModelError("Signed continuation prefix changed", {
|
|
12662
|
-
stage: "request",
|
|
12663
|
-
retryable: false
|
|
12664
|
-
});
|
|
13148
|
+
const frozen = assertSignedContinuationStable({
|
|
13149
|
+
candidate,
|
|
13150
|
+
previousRequest,
|
|
13151
|
+
history,
|
|
13152
|
+
runId,
|
|
13153
|
+
nativeBinding,
|
|
13154
|
+
projectHistory: (projectedHistory) => this.context.project(projectedHistory)
|
|
13155
|
+
});
|
|
12665
13156
|
const context = this.context;
|
|
12666
13157
|
const skills = this.skillRuntime;
|
|
12667
13158
|
const preparation = context.prepare(history, candidate, profile, runId, reason, !frozen);
|
|
@@ -12710,6 +13201,7 @@ var AgentSession = class {
|
|
|
12710
13201
|
let persistedHistoryLength = this.history.length;
|
|
12711
13202
|
let loopCompleted = false;
|
|
12712
13203
|
let loopFailed = false;
|
|
13204
|
+
let consumerInterrupted = false;
|
|
12713
13205
|
try {
|
|
12714
13206
|
for await (const event of loop.run(this.history)) {
|
|
12715
13207
|
await this.logContextEvent(event, runId);
|
|
@@ -12784,7 +13276,12 @@ var AgentSession = class {
|
|
|
12784
13276
|
if (event.type === "message_stop" || event.type === "tool_messages") {
|
|
12785
13277
|
persistedHistoryLength = this.history.length;
|
|
12786
13278
|
}
|
|
12787
|
-
|
|
13279
|
+
try {
|
|
13280
|
+
yield event;
|
|
13281
|
+
} catch (error) {
|
|
13282
|
+
consumerInterrupted = true;
|
|
13283
|
+
throw error;
|
|
13284
|
+
}
|
|
12788
13285
|
}
|
|
12789
13286
|
loopCompleted = true;
|
|
12790
13287
|
} catch (error) {
|
|
@@ -12801,7 +13298,7 @@ var AgentSession = class {
|
|
|
12801
13298
|
}
|
|
12802
13299
|
throw error;
|
|
12803
13300
|
} finally {
|
|
12804
|
-
if (!loopCompleted && !loopFailed) {
|
|
13301
|
+
if (!loopCompleted && (!loopFailed || consumerInterrupted)) {
|
|
12805
13302
|
await executor.rollback();
|
|
12806
13303
|
if (this.store) {
|
|
12807
13304
|
for (const message of this.history.slice(persistedHistoryLength)) {
|
|
@@ -12933,102 +13430,44 @@ var AgentSession = class {
|
|
|
12933
13430
|
}
|
|
12934
13431
|
/** 完整消息提交后完成技能审计;审计失败保留真实历史,停止本轮且不发布成功事件。 */
|
|
12935
13432
|
async commitSkills(runId) {
|
|
12936
|
-
|
|
12937
|
-
|
|
12938
|
-
|
|
12939
|
-
|
|
12940
|
-
|
|
12941
|
-
|
|
12942
|
-
|
|
12943
|
-
sessionId: this.sessionId,
|
|
12944
|
-
runId,
|
|
12945
|
-
fields: {
|
|
12946
|
-
skillName: source.name,
|
|
12947
|
-
skillScope: source.scope,
|
|
12948
|
-
contentDigest: source.contentDigest,
|
|
12949
|
-
renderedDigest: source.renderedDigest,
|
|
12950
|
-
pathDigest: skillDigest(source.filePath),
|
|
12951
|
-
caller: source.caller,
|
|
12952
|
-
invocationId: source.invocationId,
|
|
12953
|
-
messageId
|
|
12954
|
-
}
|
|
12955
|
-
});
|
|
12956
|
-
} catch {
|
|
12957
|
-
throw new KapibalaError("Skill commit audit unavailable", {
|
|
12958
|
-
code: "AUDIT_UNAVAILABLE",
|
|
12959
|
-
retryPolicy: "after_user_action",
|
|
12960
|
-
safeMessage: "\u6280\u80FD\u63D0\u4EA4\u5BA1\u8BA1\u4E0D\u53EF\u7528\uFF0C\u672C\u8F6E\u505C\u6B62"
|
|
12961
|
-
});
|
|
12962
|
-
}
|
|
12963
|
-
},
|
|
12964
|
-
this.store ? "disk" : "memory"
|
|
12965
|
-
);
|
|
13433
|
+
return commitSkillEvents({
|
|
13434
|
+
skillRuntime: this.skillRuntime,
|
|
13435
|
+
eventLogger: this.eventLogger,
|
|
13436
|
+
sessionId: this.sessionId,
|
|
13437
|
+
runId,
|
|
13438
|
+
persistence: this.store ? "disk" : "memory"
|
|
13439
|
+
});
|
|
12966
13440
|
}
|
|
12967
13441
|
/** 显式加载仍按同一读取策略、精确缓存与宿主审批裁决;拒绝/取消/审计失败时不追加用户任务。 */
|
|
12968
|
-
|
|
12969
|
-
|
|
12970
|
-
|
|
12971
|
-
|
|
12972
|
-
|
|
13442
|
+
authorizeSkill(name, runId, signal) {
|
|
13443
|
+
return authorizeExplicitSkill({
|
|
13444
|
+
name,
|
|
13445
|
+
runId,
|
|
13446
|
+
signal,
|
|
12973
13447
|
rootDir: this.rootDir,
|
|
12974
|
-
sessionAllowed: true
|
|
12975
|
-
};
|
|
12976
|
-
const cached = this.approvalCache.get(request);
|
|
12977
|
-
const decision = new PermissionPolicy().decide({
|
|
12978
|
-
toolName: request.toolName,
|
|
12979
|
-
capabilities: request.capabilities,
|
|
12980
13448
|
mode: this.mode,
|
|
12981
|
-
|
|
12982
|
-
|
|
12983
|
-
|
|
12984
|
-
|
|
12985
|
-
|
|
12986
|
-
let approvalChoice;
|
|
12987
|
-
if (decision.decision === "ask") {
|
|
12988
|
-
try {
|
|
12989
|
-
approvalChoice = await this.approvalChannel?.requestApproval(request, signal) ?? "deny_once";
|
|
12990
|
-
} catch {
|
|
12991
|
-
approvalChoice = "cancel";
|
|
12992
|
-
}
|
|
12993
|
-
permitted = approvalChoice === "allow_once" || approvalChoice === "allow_session";
|
|
12994
|
-
if (approvalChoice === "allow_session") this.approvalCache.set(request, "allow");
|
|
12995
|
-
if (approvalChoice === "deny_session") this.approvalCache.set(request, "deny");
|
|
12996
|
-
}
|
|
12997
|
-
await this.eventLogger.recordAudit({
|
|
12998
|
-
level: "info",
|
|
12999
|
-
event: "skill.decided",
|
|
13000
|
-
sessionId: this.sessionId,
|
|
13001
|
-
runId,
|
|
13002
|
-
fields: {
|
|
13003
|
-
skillName: name,
|
|
13004
|
-
caller: "user",
|
|
13005
|
-
decision: permitted && !signal?.aborted ? "allow" : "deny",
|
|
13006
|
-
decisionSource: decision.decision === "ask" ? this.approvalChannel ? "human" : "non_interactive" : decision.source,
|
|
13007
|
-
approvalChoice
|
|
13008
|
-
}
|
|
13449
|
+
permissionRules: this.permissionRules,
|
|
13450
|
+
approvalChannel: this.approvalChannel,
|
|
13451
|
+
approvalCache: this.approvalCache,
|
|
13452
|
+
eventLogger: this.eventLogger,
|
|
13453
|
+
sessionId: this.sessionId
|
|
13009
13454
|
});
|
|
13010
|
-
if (!permitted) throw skillFailure("SKILL_INVOCATION_FORBIDDEN", "\u5BBF\u4E3B\u6743\u9650\u7981\u6B62\u663E\u5F0F\u6280\u80FD\u8BFB\u53D6");
|
|
13011
|
-
signal?.throwIfAborted();
|
|
13012
13455
|
}
|
|
13013
13456
|
/**
|
|
13014
13457
|
* 计算最近一次内部模型请求的上下文占用;无 usage 时显式标注预算估算。
|
|
13015
13458
|
* run 累计 promptTokens 会重复计算每一步都重发的历史,不能用于窗口占用率。
|
|
13016
13459
|
*/
|
|
13017
13460
|
createContextUsage(usedTokens, estimatedUsage = false) {
|
|
13018
|
-
|
|
13019
|
-
|
|
13020
|
-
);
|
|
13021
|
-
return {
|
|
13461
|
+
return computeContextUsage(
|
|
13462
|
+
this.lastPrimaryProfile ?? this.getActiveProfile(),
|
|
13022
13463
|
usedTokens,
|
|
13023
|
-
|
|
13024
|
-
|
|
13025
|
-
estimatedLimit: contextWindow.estimated,
|
|
13026
|
-
...estimatedUsage ? { estimatedUsage: true } : {}
|
|
13027
|
-
};
|
|
13464
|
+
estimatedUsage
|
|
13465
|
+
);
|
|
13028
13466
|
}
|
|
13029
13467
|
async persistAssistant(event) {
|
|
13030
|
-
|
|
13031
|
-
|
|
13468
|
+
await persistAssistantMessage(event, this.store, () => {
|
|
13469
|
+
this.usageKnown = false;
|
|
13470
|
+
});
|
|
13032
13471
|
}
|
|
13033
13472
|
/** 创建上下文协调器并注入当前路由;摘要不经过主任务 Loop,插件结果不默认为可剪裁。 */
|
|
13034
13473
|
createContextManager() {
|
|
@@ -13047,17 +13486,8 @@ var AgentSession = class {
|
|
|
13047
13486
|
}
|
|
13048
13487
|
/** 摘要实际消耗按 attempt 身份独立累加;缺失消耗保留未知,不计入主任务指标。 */
|
|
13049
13488
|
accountSummaryUsage(attemptId, usage) {
|
|
13050
|
-
|
|
13051
|
-
this.
|
|
13052
|
-
if (!usage || ![usage.promptTokens, usage.completionTokens, usage.totalTokens].every(
|
|
13053
|
-
(n) => Number.isSafeInteger(n) && n >= 0
|
|
13054
|
-
)) {
|
|
13055
|
-
this.summaryUsageKnown = false;
|
|
13056
|
-
return;
|
|
13057
|
-
}
|
|
13058
|
-
this.summaryUsage.promptTokens += usage.promptTokens;
|
|
13059
|
-
this.summaryUsage.completionTokens += usage.completionTokens;
|
|
13060
|
-
this.summaryUsage.totalTokens += usage.totalTokens;
|
|
13489
|
+
const outcome = recordSummaryUsage(attemptId, usage, this.summaryAttempts, this.summaryUsage);
|
|
13490
|
+
if (outcome === "unknown") this.summaryUsageKnown = false;
|
|
13061
13491
|
}
|
|
13062
13492
|
/**
|
|
13063
13493
|
* 空闲时手动摘要旧完整前缀,不创建用户任务、不执行工具,不突破当前与最近成功交互。
|
|
@@ -13130,61 +13560,21 @@ var AgentSession = class {
|
|
|
13130
13560
|
}
|
|
13131
13561
|
/** 以模型 attempt 身份幂等累加真实消耗;非法或缺失 usage 保持未知。 */
|
|
13132
13562
|
accountUsage(attemptId, usage) {
|
|
13133
|
-
|
|
13134
|
-
if (
|
|
13135
|
-
(n) => Number.isSafeInteger(n) && n >= 0
|
|
13136
|
-
)) {
|
|
13137
|
-
this.usageKnown = false;
|
|
13138
|
-
return;
|
|
13139
|
-
}
|
|
13140
|
-
this.accountedAttempts.add(attemptId);
|
|
13141
|
-
this.usageStats.promptTokens += usage.promptTokens;
|
|
13142
|
-
this.usageStats.completionTokens += usage.completionTokens;
|
|
13143
|
-
this.usageStats.totalTokens += usage.totalTokens;
|
|
13563
|
+
const outcome = recordUsage(attemptId, usage, this.accountedAttempts, this.usageStats);
|
|
13564
|
+
if (outcome === "unknown") this.usageKnown = false;
|
|
13144
13565
|
}
|
|
13145
13566
|
async persistToolMessages(messages) {
|
|
13146
|
-
|
|
13147
|
-
for (const message of messages) await this.store.append(message);
|
|
13567
|
+
await persistToolMessages(messages, this.store);
|
|
13148
13568
|
}
|
|
13149
13569
|
/** 中止时为缓冲 assistant 的每个 tool_use 生成 OUTCOME_UNKNOWN 结果并入历史,保持事务闭合。 */
|
|
13150
13570
|
closeInterruptedToolCalls(assistantEvent, error) {
|
|
13151
|
-
const message =
|
|
13152
|
-
id: createMessageId(),
|
|
13153
|
-
role: "tool",
|
|
13154
|
-
content: assistantEvent.message.content.filter((block) => block.type === "tool_use").map((toolUse) => ({
|
|
13155
|
-
type: "tool_result",
|
|
13156
|
-
toolUseId: toolUse.id,
|
|
13157
|
-
content: JSON.stringify({
|
|
13158
|
-
code: "OUTCOME_UNKNOWN",
|
|
13159
|
-
retryPolicy: "after_user_action",
|
|
13160
|
-
message: "Tool execution was interrupted; outcome is unknown"
|
|
13161
|
-
}),
|
|
13162
|
-
isError: true,
|
|
13163
|
-
errorCode: "OUTCOME_UNKNOWN",
|
|
13164
|
-
retryPolicy: "after_user_action"
|
|
13165
|
-
})),
|
|
13166
|
-
timestamp: Date.now()
|
|
13167
|
-
};
|
|
13571
|
+
const message = buildInterruptedToolMessage(assistantEvent);
|
|
13168
13572
|
this.history.push(message);
|
|
13169
13573
|
return [message];
|
|
13170
13574
|
}
|
|
13171
13575
|
/** 收集历史中紧跟该 assistant 的连续 tool 消息;仅当覆盖全部 tool_use 时视为已闭合。 */
|
|
13172
13576
|
findClosedToolMessages(assistantEvent) {
|
|
13173
|
-
|
|
13174
|
-
if (assistantIndex < 0) return [];
|
|
13175
|
-
const expected = new Set(
|
|
13176
|
-
assistantEvent.message.content.filter((block) => block.type === "tool_use").map((toolUse) => toolUse.id)
|
|
13177
|
-
);
|
|
13178
|
-
const messages = [];
|
|
13179
|
-
for (let index2 = assistantIndex + 1; index2 < this.history.length; index2++) {
|
|
13180
|
-
const message = this.history[index2];
|
|
13181
|
-
if (message.role !== "tool") break;
|
|
13182
|
-
messages.push(message);
|
|
13183
|
-
for (const block of message.content) {
|
|
13184
|
-
if (block.type === "tool_result") expected.delete(block.toolUseId);
|
|
13185
|
-
}
|
|
13186
|
-
}
|
|
13187
|
-
return expected.size === 0 ? messages : [];
|
|
13577
|
+
return findClosedToolMessages(this.history, assistantEvent);
|
|
13188
13578
|
}
|
|
13189
13579
|
};
|
|
13190
13580
|
// Annotate the CommonJS export names for ESM import in node:
|