@kiturone/kapibala 0.0.9 → 0.1.0
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 +1245 -802
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +115 -32
- package/dist/index.d.ts +115 -32
- package/dist/index.js +1244 -801
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -103,6 +103,7 @@ __export(index_exports, {
|
|
|
103
103
|
createLogId: () => createLogId,
|
|
104
104
|
createMessageId: () => createMessageId,
|
|
105
105
|
createRunCommandTool: () => createRunCommandTool,
|
|
106
|
+
createSessionError: () => createSessionError,
|
|
106
107
|
createSpawnAgentTool: () => createSpawnAgentTool,
|
|
107
108
|
createWaitAgentTool: () => createWaitAgentTool,
|
|
108
109
|
defineTool: () => defineTool,
|
|
@@ -158,7 +159,6 @@ __export(index_exports, {
|
|
|
158
159
|
sanitizeModelErrorMessage: () => sanitizeModelErrorMessage,
|
|
159
160
|
sanitizeSessionTitle: () => sanitizeSessionTitle,
|
|
160
161
|
scanSkillRoot: () => scanSkillRoot,
|
|
161
|
-
sharedWorkspaceLeaseCoordinator: () => sharedWorkspaceLeaseCoordinator,
|
|
162
162
|
skillDigest: () => skillDigest,
|
|
163
163
|
skillFailure: () => skillFailure,
|
|
164
164
|
skillPathKey: () => skillPathKey,
|
|
@@ -308,6 +308,21 @@ var KapibalaError = class extends Error {
|
|
|
308
308
|
this.dispatchOutcome = options?.dispatchOutcome;
|
|
309
309
|
}
|
|
310
310
|
};
|
|
311
|
+
var SESSION_SAFE_MESSAGES = {
|
|
312
|
+
SESSION_LOCKED: "\u4F1A\u8BDD\u5DF2\u88AB\u5176\u4ED6\u6301\u6709\u8005\u5360\u7528",
|
|
313
|
+
SESSION_RECOVERY_IN_PROGRESS: "\u4F1A\u8BDD\u6B63\u5728\u6062\u590D",
|
|
314
|
+
SESSION_LOCK_OWNER_CHANGED: "\u4F1A\u8BDD\u9501\u6301\u6709\u8005\u5DF2\u53D8\u5316",
|
|
315
|
+
SESSION_NOT_FOUND: "\u672A\u627E\u5230\u6307\u5B9A\u4F1A\u8BDD",
|
|
316
|
+
SESSION_ID_AMBIGUOUS: "\u4F1A\u8BDD\u524D\u7F00\u4E0D\u552F\u4E00",
|
|
317
|
+
HISTORY_PAGE_INVALID: "\u5386\u53F2\u9875\u53C2\u6570\u65E0\u6548"
|
|
318
|
+
};
|
|
319
|
+
function createSessionError(code, message) {
|
|
320
|
+
return new KapibalaError(message, {
|
|
321
|
+
code,
|
|
322
|
+
safeMessage: SESSION_SAFE_MESSAGES[code],
|
|
323
|
+
retryPolicy: "after_user_action"
|
|
324
|
+
});
|
|
325
|
+
}
|
|
311
326
|
var PreDispatchError = class extends KapibalaError {
|
|
312
327
|
constructor(message, options) {
|
|
313
328
|
super(message, {
|
|
@@ -696,15 +711,50 @@ var import_node_path = __toESM(require("path"), 1);
|
|
|
696
711
|
|
|
697
712
|
// src/context/jsonl-lines.ts
|
|
698
713
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
699
|
-
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 = {}) {
|
|
700
746
|
const fd = import_node_fs.default.openSync(file, "r");
|
|
701
747
|
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
702
748
|
let pending = Buffer.alloc(0);
|
|
703
749
|
let offset = 0;
|
|
750
|
+
let total = 0;
|
|
704
751
|
try {
|
|
752
|
+
assertHistoryReadBudget(import_node_fs.default.fstatSync(fd).size, options);
|
|
705
753
|
while (true) {
|
|
706
754
|
const length = import_node_fs.default.readSync(fd, buffer, 0, buffer.length, null);
|
|
707
755
|
if (!length) break;
|
|
756
|
+
total += length;
|
|
757
|
+
assertHistoryReadBudget(total, options);
|
|
708
758
|
pending = Buffer.concat([pending, buffer.subarray(0, length)]);
|
|
709
759
|
let start = 0;
|
|
710
760
|
let end = pending.indexOf(10, start);
|
|
@@ -738,7 +788,7 @@ var SESSION_SCHEMA_VERSION_V2 = 2;
|
|
|
738
788
|
var SESSION_SCHEMA_VERSION = SESSION_SCHEMA_VERSION_V2;
|
|
739
789
|
var MAIN_BRANCH_ID = "main";
|
|
740
790
|
function isGlobalControlRecordType(type) {
|
|
741
|
-
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";
|
|
742
792
|
}
|
|
743
793
|
var types = /* @__PURE__ */ new Set([
|
|
744
794
|
"header",
|
|
@@ -757,7 +807,8 @@ var types = /* @__PURE__ */ new Set([
|
|
|
757
807
|
"branch_created",
|
|
758
808
|
"branch_selected",
|
|
759
809
|
"branch_summary",
|
|
760
|
-
"archive_state"
|
|
810
|
+
"archive_state",
|
|
811
|
+
"pin_state"
|
|
761
812
|
]);
|
|
762
813
|
function supportsSessionState(store) {
|
|
763
814
|
return "appendRecord" in store && typeof store.appendRecord === "function" && "loadState" in store && typeof store.loadState === "function";
|
|
@@ -840,6 +891,13 @@ function assertClosedToolTransaction(records, targetRecordId) {
|
|
|
840
891
|
}
|
|
841
892
|
}
|
|
842
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;
|
|
843
901
|
filePath;
|
|
844
902
|
queue = Promise.resolve();
|
|
845
903
|
records = [];
|
|
@@ -849,10 +907,6 @@ var SessionStore = class _SessionStore {
|
|
|
849
907
|
schemaVersion = SESSION_SCHEMA_VERSION_V2;
|
|
850
908
|
activeBranchId = "main";
|
|
851
909
|
branchLeaves = /* @__PURE__ */ new Map();
|
|
852
|
-
constructor(filePath, schemaVersion = SESSION_SCHEMA_VERSION_V2) {
|
|
853
|
-
this.filePath = import_node_path.default.resolve(filePath);
|
|
854
|
-
this.schemaVersion = schemaVersion;
|
|
855
|
-
}
|
|
856
910
|
/** 获取当前会话文件使用的 Schema 版本(1 或 2)。 */
|
|
857
911
|
getSchemaVersion() {
|
|
858
912
|
return this.schemaVersion;
|
|
@@ -875,8 +929,8 @@ var SessionStore = class _SessionStore {
|
|
|
875
929
|
this.onDiagnostic = onDiagnostic;
|
|
876
930
|
}
|
|
877
931
|
/** 以独占创建方式安装新头部(默认生成 Schema v2 格式),已有文件明确失败,绝不覆盖其它会话。 */
|
|
878
|
-
static async create(filePath, header) {
|
|
879
|
-
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 });
|
|
880
934
|
import_node_fs2.default.mkdirSync(import_node_path.default.dirname(store.filePath), { recursive: true });
|
|
881
935
|
const record = store.makeRecord("header", {
|
|
882
936
|
...header,
|
|
@@ -897,8 +951,8 @@ var SessionStore = class _SessionStore {
|
|
|
897
951
|
return store;
|
|
898
952
|
}
|
|
899
953
|
/** 打开并验证已有会话;尾部残片先保存到隔离文件,中部损坏与未知版本拒绝恢复。 */
|
|
900
|
-
static async open(filePath) {
|
|
901
|
-
const store = new _SessionStore(filePath);
|
|
954
|
+
static async open(filePath, options = {}) {
|
|
955
|
+
const store = new _SessionStore(filePath, SESSION_SCHEMA_VERSION_V2, { ...options });
|
|
902
956
|
store.records = store.readFile(true);
|
|
903
957
|
store.refreshStamp();
|
|
904
958
|
return store;
|
|
@@ -992,6 +1046,24 @@ var SessionStore = class _SessionStore {
|
|
|
992
1046
|
}
|
|
993
1047
|
return false;
|
|
994
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
|
+
}
|
|
995
1067
|
/**
|
|
996
1068
|
* 获取当前会话中所有已知分支的元数据与状态列表。
|
|
997
1069
|
*/
|
|
@@ -1283,14 +1355,15 @@ ${summaryText}` }]
|
|
|
1283
1355
|
}
|
|
1284
1356
|
/** 校验每个完整记录,只有最后非空行的 JSON 残片可隔离,语义损坏绝不作为残片吞掉。 */
|
|
1285
1357
|
readFile(recover) {
|
|
1286
|
-
const iterator = readJsonlLines(this.filePath);
|
|
1358
|
+
const iterator = readJsonlLines(this.filePath, this.readOptions);
|
|
1287
1359
|
let lastTerminated = true;
|
|
1288
1360
|
let blankSeen = false;
|
|
1289
1361
|
const records = [];
|
|
1290
1362
|
const identities = /* @__PURE__ */ new Set();
|
|
1291
1363
|
const messages = /* @__PURE__ */ new Set();
|
|
1292
|
-
|
|
1293
|
-
|
|
1364
|
+
const branchLeaves = /* @__PURE__ */ new Map();
|
|
1365
|
+
let activeBranchId = MAIN_BRANCH_ID;
|
|
1366
|
+
let schemaVersion = this.schemaVersion;
|
|
1294
1367
|
let detectedSchemaVersion = null;
|
|
1295
1368
|
let i = -1;
|
|
1296
1369
|
for (const line of iterator) {
|
|
@@ -1305,7 +1378,7 @@ ${summaryText}` }]
|
|
|
1305
1378
|
try {
|
|
1306
1379
|
record = JSON.parse(line.text);
|
|
1307
1380
|
} catch {
|
|
1308
|
-
if (!recover ||
|
|
1381
|
+
if (!recover || schemaVersion === SESSION_SCHEMA_VERSION_V1 || !records.length || Array.from(iterator).some((next) => next.text.trim()))
|
|
1309
1382
|
throw new Error(`Session corrupt at line ${i + 1}`);
|
|
1310
1383
|
import_node_fs2.default.writeFileSync(
|
|
1311
1384
|
`${this.filePath}.corrupt-${createMessageId()}`,
|
|
@@ -1329,7 +1402,7 @@ ${summaryText}` }]
|
|
|
1329
1402
|
throw new Error("Unsupported session schema version");
|
|
1330
1403
|
}
|
|
1331
1404
|
detectedSchemaVersion = record.schemaVersion;
|
|
1332
|
-
|
|
1405
|
+
schemaVersion = detectedSchemaVersion;
|
|
1333
1406
|
} else if (record.schemaVersion !== detectedSchemaVersion) {
|
|
1334
1407
|
throw new Error(`Session corrupt schema version mismatch at line ${i + 1}`);
|
|
1335
1408
|
}
|
|
@@ -1346,11 +1419,11 @@ ${summaryText}` }]
|
|
|
1346
1419
|
} else if (record.type === "header") {
|
|
1347
1420
|
throw new Error("Session corrupt: repeated header");
|
|
1348
1421
|
}
|
|
1349
|
-
if (
|
|
1422
|
+
if (schemaVersion === SESSION_SCHEMA_VERSION_V1) {
|
|
1350
1423
|
if (record.parentId !== (records.at(-1)?.recordId ?? null)) {
|
|
1351
1424
|
throw new Error(`Session corrupt parent at line ${i + 1}`);
|
|
1352
1425
|
}
|
|
1353
|
-
|
|
1426
|
+
branchLeaves.set(MAIN_BRANCH_ID, record.recordId);
|
|
1354
1427
|
} else {
|
|
1355
1428
|
if (typeof record.seq !== "number" || record.seq !== records.length) {
|
|
1356
1429
|
throw new Error(`Session corrupt seq at line ${i + 1}`);
|
|
@@ -1364,37 +1437,37 @@ ${summaryText}` }]
|
|
|
1364
1437
|
throw new Error(`Session corrupt control record parent at line ${i + 1}`);
|
|
1365
1438
|
}
|
|
1366
1439
|
if (record.type === "header") {
|
|
1367
|
-
|
|
1368
|
-
|
|
1440
|
+
branchLeaves.set(MAIN_BRANCH_ID, record.recordId);
|
|
1441
|
+
activeBranchId = MAIN_BRANCH_ID;
|
|
1369
1442
|
} else if (record.type === "branch_created") {
|
|
1370
1443
|
const bId = record.payload.branchId;
|
|
1371
1444
|
const baseId = record.payload.baseRecordId;
|
|
1372
1445
|
if (typeof bId !== "string" || !bId) {
|
|
1373
1446
|
throw new Error(`Session corrupt branch_created branchId at line ${i + 1}`);
|
|
1374
1447
|
}
|
|
1375
|
-
if (
|
|
1448
|
+
if (branchLeaves.has(bId)) {
|
|
1376
1449
|
throw new Error(`Duplicate branch ID: ${bId}`);
|
|
1377
1450
|
}
|
|
1378
1451
|
if (typeof baseId !== "string" || !identities.has(baseId)) {
|
|
1379
1452
|
throw new Error(`Session corrupt branch_created baseRecordId at line ${i + 1}`);
|
|
1380
1453
|
}
|
|
1381
|
-
|
|
1454
|
+
branchLeaves.set(bId, baseId);
|
|
1382
1455
|
} else if (record.type === "branch_selected") {
|
|
1383
1456
|
const bId = record.payload.branchId;
|
|
1384
|
-
if (typeof bId !== "string" || !
|
|
1457
|
+
if (typeof bId !== "string" || !branchLeaves.has(bId)) {
|
|
1385
1458
|
throw new Error(`Selected branch not found: ${bId}`);
|
|
1386
1459
|
}
|
|
1387
|
-
|
|
1460
|
+
activeBranchId = bId;
|
|
1388
1461
|
}
|
|
1389
1462
|
} else {
|
|
1390
1463
|
const bId = record.branchId;
|
|
1391
|
-
if (typeof bId !== "string" || !
|
|
1464
|
+
if (typeof bId !== "string" || !branchLeaves.has(bId)) {
|
|
1392
1465
|
throw new Error(`Session corrupt branchId at line ${i + 1}`);
|
|
1393
1466
|
}
|
|
1394
1467
|
if (typeof record.parentId !== "string" || !identities.has(record.parentId)) {
|
|
1395
1468
|
throw new Error(`Session corrupt parent at line ${i + 1}`);
|
|
1396
1469
|
}
|
|
1397
|
-
|
|
1470
|
+
branchLeaves.set(bId, record.recordId);
|
|
1398
1471
|
}
|
|
1399
1472
|
}
|
|
1400
1473
|
identities.add(record.recordId);
|
|
@@ -1413,7 +1486,7 @@ ${summaryText}` }]
|
|
|
1413
1486
|
if (!records.length) {
|
|
1414
1487
|
throw new Error("Session corrupt header");
|
|
1415
1488
|
}
|
|
1416
|
-
if (recover && !lastTerminated && records.length === i + 1 &&
|
|
1489
|
+
if (recover && !lastTerminated && records.length === i + 1 && schemaVersion !== SESSION_SCHEMA_VERSION_V1) {
|
|
1417
1490
|
const fd = import_node_fs2.default.openSync(this.filePath, "a");
|
|
1418
1491
|
try {
|
|
1419
1492
|
import_node_fs2.default.writeFileSync(fd, "\n");
|
|
@@ -1422,6 +1495,9 @@ ${summaryText}` }]
|
|
|
1422
1495
|
import_node_fs2.default.closeSync(fd);
|
|
1423
1496
|
}
|
|
1424
1497
|
}
|
|
1498
|
+
this.branchLeaves = branchLeaves;
|
|
1499
|
+
this.activeBranchId = activeBranchId;
|
|
1500
|
+
this.schemaVersion = schemaVersion;
|
|
1425
1501
|
return records;
|
|
1426
1502
|
}
|
|
1427
1503
|
};
|
|
@@ -1453,19 +1529,23 @@ var SessionLock = class _SessionLock {
|
|
|
1453
1529
|
if (error.code !== "EEXIST") throw error;
|
|
1454
1530
|
const stale = _SessionLock.readOwner(lockPath);
|
|
1455
1531
|
if (!stale || !_SessionLock.isDead(stale.pid))
|
|
1456
|
-
throw
|
|
1532
|
+
throw createSessionError(
|
|
1533
|
+
"SESSION_LOCKED",
|
|
1457
1534
|
`Session locked by a live or unknown owner${stale ? ` (pid=${stale.pid})` : ""}`
|
|
1458
1535
|
);
|
|
1459
1536
|
try {
|
|
1460
1537
|
import_node_fs3.default.mkdirSync(recovery);
|
|
1461
1538
|
} catch {
|
|
1462
|
-
throw
|
|
1539
|
+
throw createSessionError(
|
|
1540
|
+
"SESSION_RECOVERY_IN_PROGRESS",
|
|
1541
|
+
"Session locked: recovery in progress"
|
|
1542
|
+
);
|
|
1463
1543
|
}
|
|
1464
1544
|
try {
|
|
1465
1545
|
import_node_fs3.default.writeFileSync(import_node_path2.default.join(recovery, "owner.json"), JSON.stringify(owner), { flag: "wx" });
|
|
1466
1546
|
const current = _SessionLock.readOwner(lockPath);
|
|
1467
1547
|
if (current?.nonce !== stale.nonce || current.pid !== stale.pid || !_SessionLock.isDead(current.pid)) {
|
|
1468
|
-
throw
|
|
1548
|
+
throw createSessionError("SESSION_LOCK_OWNER_CHANGED", "Session locked: owner changed");
|
|
1469
1549
|
}
|
|
1470
1550
|
const quarantine = `${lockPath}.stale-${(0, import_node_crypto4.randomUUID)()}`;
|
|
1471
1551
|
import_node_fs3.default.renameSync(lockPath, quarantine);
|
|
@@ -1485,7 +1565,8 @@ var SessionLock = class _SessionLock {
|
|
|
1485
1565
|
}
|
|
1486
1566
|
if (installError.code === "EEXIST") {
|
|
1487
1567
|
const winner = _SessionLock.readOwner(lockPath);
|
|
1488
|
-
throw
|
|
1568
|
+
throw createSessionError(
|
|
1569
|
+
"SESSION_LOCKED",
|
|
1489
1570
|
`Session locked by a live or unknown owner${winner ? ` (pid=${winner.pid})` : ""}`
|
|
1490
1571
|
);
|
|
1491
1572
|
}
|
|
@@ -1519,7 +1600,7 @@ var SessionLock = class _SessionLock {
|
|
|
1519
1600
|
if (this.released) return;
|
|
1520
1601
|
const current = _SessionLock.readOwner(this.lockPath);
|
|
1521
1602
|
if (current?.nonce !== this.owner.nonce || current.pid !== this.owner.pid)
|
|
1522
|
-
throw
|
|
1603
|
+
throw createSessionError("SESSION_LOCK_OWNER_CHANGED", "Session lock owner changed");
|
|
1523
1604
|
import_node_fs3.default.unlinkSync(import_node_path2.default.join(this.lockPath, "owner.json"));
|
|
1524
1605
|
import_node_fs3.default.rmdirSync(this.lockPath);
|
|
1525
1606
|
this.released = true;
|
|
@@ -1550,7 +1631,11 @@ var SessionLock = class _SessionLock {
|
|
|
1550
1631
|
/** 无残留或残留进程已死亡时放行;确认存活的回收进行中则明确失败。 */
|
|
1551
1632
|
static ensureNoLiveRecovery(recovery) {
|
|
1552
1633
|
_SessionLock.clearAbandonedRecovery(recovery);
|
|
1553
|
-
if (import_node_fs3.default.existsSync(recovery))
|
|
1634
|
+
if (import_node_fs3.default.existsSync(recovery))
|
|
1635
|
+
throw createSessionError(
|
|
1636
|
+
"SESSION_RECOVERY_IN_PROGRESS",
|
|
1637
|
+
"Session locked: recovery in progress"
|
|
1638
|
+
);
|
|
1554
1639
|
}
|
|
1555
1640
|
/** 仅 ESRCH 表示确定死亡;PID 复用或 EPERM 均保持原锁。 */
|
|
1556
1641
|
static isDead(pid) {
|
|
@@ -1700,18 +1785,21 @@ function resolveWorkspaceContext(input) {
|
|
|
1700
1785
|
function normalizedProjectRoot(root) {
|
|
1701
1786
|
return process.platform === "win32" ? root.replaceAll("\\", "/").toLowerCase() : root;
|
|
1702
1787
|
}
|
|
1703
|
-
function resolveSessionProject(cwd) {
|
|
1788
|
+
function resolveSessionProject(cwd, mode = "git") {
|
|
1704
1789
|
const realCwd = toNativePhysicalPath(cwd);
|
|
1705
1790
|
let root = realCwd;
|
|
1706
1791
|
let gitBranch;
|
|
1707
1792
|
let gitWorktree = false;
|
|
1708
1793
|
try {
|
|
1709
|
-
|
|
1794
|
+
const gitRoot = toNativePhysicalPath(
|
|
1710
1795
|
(0, import_node_child_process.execFileSync)("git", ["-C", realCwd, "rev-parse", "--show-toplevel"], {
|
|
1711
1796
|
encoding: "utf8",
|
|
1712
1797
|
stdio: ["ignore", "pipe", "ignore"]
|
|
1713
1798
|
}).trim()
|
|
1714
1799
|
);
|
|
1800
|
+
if (mode === "git") root = gitRoot;
|
|
1801
|
+
if (normalizedProjectRoot(gitRoot) !== normalizedProjectRoot(root))
|
|
1802
|
+
return { projectRoot: root, cwd: realCwd, projectKey: computeProjectKey(root) };
|
|
1715
1803
|
gitWorktree = true;
|
|
1716
1804
|
gitBranch = (0, import_node_child_process.execFileSync)("git", ["-C", realCwd, "symbolic-ref", "--short", "-q", "HEAD"], {
|
|
1717
1805
|
encoding: "utf8",
|
|
@@ -1719,11 +1807,10 @@ function resolveSessionProject(cwd) {
|
|
|
1719
1807
|
}).trim() || void 0;
|
|
1720
1808
|
} catch {
|
|
1721
1809
|
}
|
|
1722
|
-
const normalized = normalizedProjectRoot(root);
|
|
1723
1810
|
return {
|
|
1724
1811
|
projectRoot: root,
|
|
1725
1812
|
cwd: realCwd,
|
|
1726
|
-
projectKey: (
|
|
1813
|
+
projectKey: computeProjectKey(root),
|
|
1727
1814
|
gitBranch,
|
|
1728
1815
|
gitWorktree
|
|
1729
1816
|
};
|
|
@@ -1741,7 +1828,10 @@ var SessionManager = class {
|
|
|
1741
1828
|
directory;
|
|
1742
1829
|
handles = /* @__PURE__ */ new Map();
|
|
1743
1830
|
diagnostic;
|
|
1831
|
+
readOptions;
|
|
1744
1832
|
constructor(options = {}) {
|
|
1833
|
+
this.readOptions = { maxReadBytes: options.maxReadBytes };
|
|
1834
|
+
assertHistoryReadBudget(0, this.readOptions);
|
|
1745
1835
|
const rootCandidate = options.workspaceRoot ?? options.projectRoot ?? options.cwd;
|
|
1746
1836
|
if (!rootCandidate) {
|
|
1747
1837
|
throw new KapibalaError("Workspace root is required for SessionManager", {
|
|
@@ -1749,7 +1839,7 @@ var SessionManager = class {
|
|
|
1749
1839
|
safeMessage: "\u5FC5\u987B\u663E\u5F0F\u6307\u5B9A\u5DE5\u4F5C\u533A\u6839\u8DEF\u5F84\u6216\u9879\u76EE\u76EE\u5F55"
|
|
1750
1840
|
});
|
|
1751
1841
|
}
|
|
1752
|
-
this.project = resolveSessionProject(rootCandidate);
|
|
1842
|
+
this.project = resolveSessionProject(rootCandidate, options.projectRootMode);
|
|
1753
1843
|
this.directory = import_node_path4.default.join(
|
|
1754
1844
|
options.homeDirectory ?? import_node_os.default.homedir(),
|
|
1755
1845
|
".kapibala",
|
|
@@ -1765,12 +1855,16 @@ var SessionManager = class {
|
|
|
1765
1855
|
const file = import_node_path4.default.join(this.directory, `${Date.now()}_${conversationId}.jsonl`);
|
|
1766
1856
|
const lock = await SessionLock.acquire(this.lockPath(conversationId));
|
|
1767
1857
|
try {
|
|
1768
|
-
const store = await SessionStore.create(
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
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
|
+
);
|
|
1774
1868
|
return await this.attach(store, lock);
|
|
1775
1869
|
} catch (error) {
|
|
1776
1870
|
await lock.release();
|
|
@@ -1779,89 +1873,146 @@ var SessionManager = class {
|
|
|
1779
1873
|
}
|
|
1780
1874
|
/**
|
|
1781
1875
|
* 按正文活动排序分页;有效 sidecar 只读元数据,不读取完整 JSONL。默认排除已归档会话。
|
|
1782
|
-
* pageSize 由调用方显式给出时必须在 1–100
|
|
1876
|
+
* pageSize 由调用方显式给出时必须在 1–100 之间;filter 在分页前筛选元数据,调用方须自行处理敏感信息。
|
|
1877
|
+
* 置顶项整体前置、组内仍按活动时间排序;排序只作用于本方法的返回值,不影响 continueRecent。
|
|
1783
1878
|
*/
|
|
1784
1879
|
async list(page = 1, options) {
|
|
1785
1880
|
const pageSize = options?.pageSize ?? 20;
|
|
1786
1881
|
if (!Number.isSafeInteger(page) || page < 1 || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100)
|
|
1787
|
-
throw
|
|
1882
|
+
throw createSessionError("HISTORY_PAGE_INVALID", "History page must be a positive integer");
|
|
1788
1883
|
const all = await this.allMetadata();
|
|
1789
|
-
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
|
+
];
|
|
1790
1890
|
return {
|
|
1791
|
-
items:
|
|
1792
|
-
total:
|
|
1891
|
+
items: ordered.slice((page - 1) * pageSize, page * pageSize),
|
|
1892
|
+
total: ordered.length,
|
|
1793
1893
|
page,
|
|
1794
|
-
pages: Math.max(1, Math.ceil(
|
|
1894
|
+
pages: Math.max(1, Math.ceil(ordered.length / pageSize))
|
|
1795
1895
|
};
|
|
1796
1896
|
}
|
|
1797
1897
|
/** 解析完整 ID 或唯一前缀对应的会话文件与 ID。 */
|
|
1798
1898
|
resolveSession(id) {
|
|
1799
|
-
if (!id || !/^[a-zA-Z0-9-]+$/.test(id))
|
|
1899
|
+
if (!id || !/^[a-zA-Z0-9-]+$/.test(id))
|
|
1900
|
+
throw createSessionError("SESSION_NOT_FOUND", "Session not found");
|
|
1800
1901
|
const all = import_node_fs5.default.readdirSync(this.directory).filter((name) => /^\d+_[a-zA-Z0-9-]+\.jsonl$/.test(name)).map((fileName) => ({
|
|
1801
1902
|
fileName,
|
|
1802
1903
|
conversationId: fileName.slice(fileName.indexOf("_") + 1, -6)
|
|
1803
1904
|
}));
|
|
1804
1905
|
const exact = all.find((m) => m.conversationId === id || m.fileName === id);
|
|
1805
1906
|
const matches = exact ? [exact] : all.filter((m) => m.conversationId.startsWith(id));
|
|
1806
|
-
if (!matches.length) throw
|
|
1807
|
-
if (matches.length !== 1)
|
|
1907
|
+
if (!matches.length) throw createSessionError("SESSION_NOT_FOUND", `Session not found: ${id}`);
|
|
1908
|
+
if (matches.length !== 1)
|
|
1909
|
+
throw createSessionError("SESSION_ID_AMBIGUOUS", `Ambiguous session prefix: ${id}`);
|
|
1808
1910
|
return matches[0];
|
|
1809
1911
|
}
|
|
1810
1912
|
/**
|
|
1811
|
-
*
|
|
1913
|
+
* 归档、恢复、重命名与置顶都向正文追加一条全局控制记录,共用同一套前置约束。
|
|
1812
1914
|
*
|
|
1813
1915
|
* 约束:
|
|
1814
1916
|
* - 必须取得会话独占锁;
|
|
1815
|
-
* -
|
|
1816
|
-
* -
|
|
1917
|
+
* - 若会话当前正处于活动打开状态,拒绝并抛出 SESSION_IN_USE(调用方先关闭句柄);
|
|
1918
|
+
* - v1 只读历史不能变更,明确以 LEGACY_SESSION_READ_ONLY 失败而不是静默忽略;
|
|
1919
|
+
* - 写入后按正文重建 sidecar,状态只以正文记录为准。
|
|
1817
1920
|
*/
|
|
1818
|
-
async
|
|
1921
|
+
async appendControlRecord(id, subject, append) {
|
|
1819
1922
|
const { fileName, conversationId } = this.resolveSession(id);
|
|
1820
1923
|
if (this.handles.has(conversationId)) {
|
|
1821
|
-
throw new KapibalaError("Session is currently active or in use
|
|
1924
|
+
throw new KapibalaError("Session is currently active or in use", {
|
|
1822
1925
|
code: "SESSION_IN_USE",
|
|
1823
|
-
safeMessage:
|
|
1926
|
+
safeMessage: `\u4F1A\u8BDD\u6B63\u5728\u4F7F\u7528\u4E2D\uFF0C\u65E0\u6CD5${subject}`
|
|
1824
1927
|
});
|
|
1825
1928
|
}
|
|
1826
1929
|
const targetFile = import_node_path4.default.join(this.directory, fileName);
|
|
1827
1930
|
const lock = await SessionLock.acquire(this.lockPath(conversationId));
|
|
1828
1931
|
try {
|
|
1829
|
-
const store = await SessionStore.open(targetFile);
|
|
1932
|
+
const store = await SessionStore.open(targetFile, this.readOptions);
|
|
1830
1933
|
if (store.isReadOnly()) {
|
|
1831
|
-
throw new KapibalaError("Cannot
|
|
1934
|
+
throw new KapibalaError("Cannot modify legacy v1 session state", {
|
|
1832
1935
|
code: "LEGACY_SESSION_READ_ONLY",
|
|
1833
|
-
safeMessage:
|
|
1936
|
+
safeMessage: `\u65E0\u6CD5\u53D8\u66F4\u53EA\u8BFB\u5386\u53F2\u4F1A\u8BDD\u7684${subject}\u72B6\u6001`
|
|
1834
1937
|
});
|
|
1835
1938
|
}
|
|
1836
|
-
await store
|
|
1837
|
-
|
|
1838
|
-
const meta = this.buildMetadata(targetFile, records);
|
|
1839
|
-
meta.archived = true;
|
|
1840
|
-
this.writeMetadata(targetFile, meta);
|
|
1939
|
+
await append(store);
|
|
1940
|
+
this.writeMetadata(targetFile, this.buildMetadata(targetFile, await store.readRecords()));
|
|
1841
1941
|
} finally {
|
|
1842
1942
|
await lock.release();
|
|
1843
1943
|
}
|
|
1844
1944
|
}
|
|
1845
1945
|
/**
|
|
1846
|
-
*
|
|
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 },正文保持不动。
|
|
1847
1953
|
*/
|
|
1848
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) {
|
|
1849
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
|
+
}
|
|
1850
2000
|
const targetFile = import_node_path4.default.join(this.directory, fileName);
|
|
1851
2001
|
const lock = await SessionLock.acquire(this.lockPath(conversationId));
|
|
1852
2002
|
try {
|
|
1853
|
-
const
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2003
|
+
const headerLines = this.readDiscoveryRecords(targetFile);
|
|
2004
|
+
let header;
|
|
2005
|
+
try {
|
|
2006
|
+
header = headerLines.next().value.payload;
|
|
2007
|
+
} finally {
|
|
2008
|
+
headerLines.return(void 0);
|
|
1859
2009
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
const
|
|
1863
|
-
|
|
1864
|
-
|
|
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);
|
|
1865
2016
|
} finally {
|
|
1866
2017
|
await lock.release();
|
|
1867
2018
|
}
|
|
@@ -1873,6 +2024,11 @@ var SessionManager = class {
|
|
|
1873
2024
|
const file = import_node_path4.default.join(this.directory, fileName);
|
|
1874
2025
|
try {
|
|
1875
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
|
+
}
|
|
1876
2032
|
if (!meta) {
|
|
1877
2033
|
const records = this.readDiscoveryRecords(file);
|
|
1878
2034
|
meta = this.buildMetadata(file, records);
|
|
@@ -1883,7 +2039,17 @@ var SessionManager = class {
|
|
|
1883
2039
|
}
|
|
1884
2040
|
}
|
|
1885
2041
|
all.push(meta);
|
|
1886
|
-
} 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
|
+
}
|
|
1887
2053
|
this.report(`Cannot discover session: ${fileName}`);
|
|
1888
2054
|
}
|
|
1889
2055
|
}
|
|
@@ -1933,7 +2099,10 @@ var SessionManager = class {
|
|
|
1933
2099
|
}
|
|
1934
2100
|
if (header.conversationId !== meta.conversationId || normalizedProjectRoot(header.projectRoot) !== normalizedProjectRoot(this.project.projectRoot))
|
|
1935
2101
|
throw new Error("Foreign session identity");
|
|
1936
|
-
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
|
+
);
|
|
1937
2106
|
if ((await store.loadState()).header.conversationId !== meta.conversationId)
|
|
1938
2107
|
throw new Error("Session identity mismatch");
|
|
1939
2108
|
await store.load();
|
|
@@ -1945,7 +2114,9 @@ var SessionManager = class {
|
|
|
1945
2114
|
}
|
|
1946
2115
|
/** 继续最近有正文的会话;没有历史才新建,显式打开失败绝不回退为空会话。 */
|
|
1947
2116
|
async continueRecent(options) {
|
|
1948
|
-
const latest = (await this.allMetadata()).find(
|
|
2117
|
+
const latest = (await this.allMetadata()).find(
|
|
2118
|
+
(m) => m.messageCount > 0 || m.unavailableReason
|
|
2119
|
+
);
|
|
1949
2120
|
if (latest)
|
|
1950
2121
|
return this.open(latest.conversationId, {
|
|
1951
2122
|
autoUpgradeLegacy: options?.autoUpgradeLegacy ?? true
|
|
@@ -1953,11 +2124,21 @@ var SessionManager = class {
|
|
|
1953
2124
|
this.report("No previous session with content; created a new session");
|
|
1954
2125
|
return this.create();
|
|
1955
2126
|
}
|
|
1956
|
-
/**
|
|
2127
|
+
/**
|
|
2128
|
+
* 重命名已打开或待打开的会话;标题作为独立耐久记录,标题或浏览都不改变正文活动排序。
|
|
2129
|
+
*
|
|
2130
|
+
* 未持有句柄时临时打开并在追加后释放:只改标题不该把会话一直挂在写锁上,
|
|
2131
|
+
* 否则同进程后续的归档、删除与置顶都会被自己占住的锁挡掉。
|
|
2132
|
+
*/
|
|
1957
2133
|
async rename(id, title) {
|
|
1958
2134
|
if (!title.trim()) throw new Error("Session title cannot be empty");
|
|
2135
|
+
const alreadyOpen = this.handles.has(this.resolveSession(id).conversationId);
|
|
1959
2136
|
const handle = await this.open(id, { autoUpgradeLegacy: true });
|
|
1960
|
-
|
|
2137
|
+
try {
|
|
2138
|
+
await handle.store.appendRecord("title", { title: sanitizeSessionTitle(title) });
|
|
2139
|
+
} finally {
|
|
2140
|
+
if (!alreadyOpen) await handle.release();
|
|
2141
|
+
}
|
|
1961
2142
|
}
|
|
1962
2143
|
/** 等候所有存储队列后释放句柄;宿主必须先结束 AgentSession 的工具/摘要清理。 */
|
|
1963
2144
|
async close() {
|
|
@@ -1977,7 +2158,7 @@ var SessionManager = class {
|
|
|
1977
2158
|
for (const source of sources) {
|
|
1978
2159
|
if (!import_node_fs5.default.existsSync(source)) continue;
|
|
1979
2160
|
const before = import_node_fs5.default.statSync(source);
|
|
1980
|
-
const bytes =
|
|
2161
|
+
const bytes = readHistoryBytes(source, this.readOptions);
|
|
1981
2162
|
const identity = (0, import_node_crypto6.createHash)("sha256").update(import_node_fs5.default.realpathSync.native(source)).update("\0").update(bytes).digest("hex");
|
|
1982
2163
|
const importLock = await SessionLock.acquire(
|
|
1983
2164
|
import_node_path4.default.join(this.directory, `import-${identity}.lock`)
|
|
@@ -2003,19 +2184,23 @@ var SessionManager = class {
|
|
|
2003
2184
|
const id = createMessageId();
|
|
2004
2185
|
const target = import_node_path4.default.join(this.directory, `${Date.now()}_${id}.jsonl`);
|
|
2005
2186
|
temp = `${target}.tmp-${createMessageId()}`;
|
|
2006
|
-
const store = await SessionStore.create(
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
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
|
+
);
|
|
2013
2198
|
await store.appendRecord("legacy_import", { source, identity });
|
|
2014
2199
|
await store.appendRecord("title", { title: "\u65E7\u5386\u53F2\u5BFC\u5165" });
|
|
2015
2200
|
for (const message of identifyHistory(raw, identity)) await store.append(message);
|
|
2016
2201
|
await store.load();
|
|
2017
2202
|
const after = import_node_fs5.default.statSync(source);
|
|
2018
|
-
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)) {
|
|
2019
2204
|
this.report("Legacy history is changing; import postponed");
|
|
2020
2205
|
continue;
|
|
2021
2206
|
}
|
|
@@ -2063,17 +2248,22 @@ var SessionManager = class {
|
|
|
2063
2248
|
}));
|
|
2064
2249
|
const exact = all.find((m) => m.conversationId === idOrFileName);
|
|
2065
2250
|
const matches = exact ? [exact] : all.filter((m) => m.conversationId.startsWith(idOrFileName));
|
|
2066
|
-
if (!matches.length)
|
|
2067
|
-
|
|
2251
|
+
if (!matches.length)
|
|
2252
|
+
throw createSessionError("SESSION_NOT_FOUND", `Session not found: ${idOrFileName}`);
|
|
2253
|
+
if (matches.length !== 1)
|
|
2254
|
+
throw createSessionError(
|
|
2255
|
+
"SESSION_ID_AMBIGUOUS",
|
|
2256
|
+
`Ambiguous session prefix: ${idOrFileName}`
|
|
2257
|
+
);
|
|
2068
2258
|
sourceFileName = matches[0].fileName;
|
|
2069
2259
|
}
|
|
2070
2260
|
const sourcePath = import_node_path4.default.join(this.directory, sourceFileName);
|
|
2071
2261
|
if (!import_node_fs5.default.existsSync(sourcePath)) {
|
|
2072
|
-
throw
|
|
2262
|
+
throw createSessionError("SESSION_NOT_FOUND", `Session not found: ${idOrFileName}`);
|
|
2073
2263
|
}
|
|
2074
2264
|
const nativeSource = toNativePhysicalPath(sourcePath);
|
|
2075
2265
|
const beforeStat = import_node_fs5.default.statSync(nativeSource);
|
|
2076
|
-
const bytes =
|
|
2266
|
+
const bytes = readHistoryBytes(nativeSource, this.readOptions);
|
|
2077
2267
|
const upgradeIdentity = (0, import_node_crypto6.createHash)("sha256").update(nativeSource).update("\0").update(bytes).digest("hex");
|
|
2078
2268
|
const existingMeta = (await this.allMetadata()).find(
|
|
2079
2269
|
(m) => m.upgradeIdentity === upgradeIdentity
|
|
@@ -2092,7 +2282,7 @@ var SessionManager = class {
|
|
|
2092
2282
|
if (doubleCheck) {
|
|
2093
2283
|
return this.open(doubleCheck.conversationId);
|
|
2094
2284
|
}
|
|
2095
|
-
const sourceStore = await SessionStore.open(nativeSource);
|
|
2285
|
+
const sourceStore = await SessionStore.open(nativeSource, this.readOptions);
|
|
2096
2286
|
if (sourceStore.getSchemaVersion() !== SESSION_SCHEMA_VERSION_V1) {
|
|
2097
2287
|
const state = await sourceStore.loadState();
|
|
2098
2288
|
return this.open(state.header.conversationId);
|
|
@@ -2159,7 +2349,7 @@ var SessionManager = class {
|
|
|
2159
2349
|
import_node_fs5.default.closeSync(fd);
|
|
2160
2350
|
}
|
|
2161
2351
|
const afterStat = import_node_fs5.default.statSync(nativeSource);
|
|
2162
|
-
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)) {
|
|
2163
2353
|
throw new Error("Source legacy session changed during upgrade; retry aborted");
|
|
2164
2354
|
}
|
|
2165
2355
|
import_node_fs5.default.renameSync(tempPath, targetPath);
|
|
@@ -2322,7 +2512,7 @@ var SessionManager = class {
|
|
|
2322
2512
|
store.setAppendObserver(
|
|
2323
2513
|
(record) => {
|
|
2324
2514
|
const previous = JSON.parse(
|
|
2325
|
-
|
|
2515
|
+
readHistoryBytes(this.metadataPath(store.filePath), this.readOptions).toString("utf8")
|
|
2326
2516
|
);
|
|
2327
2517
|
const currentPrev = record.prevRecordId ?? record.parentId;
|
|
2328
2518
|
if (previous.conversationId !== header.conversationId || currentPrev !== null && currentPrev !== (previous.logRevision ?? previous.revision))
|
|
@@ -2367,10 +2557,28 @@ var SessionManager = class {
|
|
|
2367
2557
|
this.handles.set(header.conversationId, handle);
|
|
2368
2558
|
return handle;
|
|
2369
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
|
+
}
|
|
2370
2576
|
/** 缓存通过文件名、身份、统计与正文 stat 校验;失败交由正文重建。 */
|
|
2371
2577
|
readMetadata(file) {
|
|
2372
2578
|
try {
|
|
2373
|
-
const meta = JSON.parse(
|
|
2579
|
+
const meta = JSON.parse(
|
|
2580
|
+
readHistoryBytes(this.metadataPath(file), this.readOptions).toString("utf8")
|
|
2581
|
+
);
|
|
2374
2582
|
const stat = import_node_fs5.default.statSync(file);
|
|
2375
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)
|
|
2376
2584
|
return meta;
|
|
@@ -2380,7 +2588,7 @@ var SessionManager = class {
|
|
|
2380
2588
|
}
|
|
2381
2589
|
/** 浏览仅解析完整记录;严格打开由 SessionStore 验证,浏览不能改写残片。 */
|
|
2382
2590
|
*readDiscoveryRecords(file) {
|
|
2383
|
-
const iterator = readJsonlLines(file);
|
|
2591
|
+
const iterator = readJsonlLines(file, this.readOptions);
|
|
2384
2592
|
let first = true;
|
|
2385
2593
|
let blankSeen = false;
|
|
2386
2594
|
for (const line of iterator) {
|
|
@@ -2435,7 +2643,8 @@ var SessionManager = class {
|
|
|
2435
2643
|
contentRevision: first.value.recordId,
|
|
2436
2644
|
activeBranchId: MAIN_BRANCH_ID,
|
|
2437
2645
|
activeLeafId: first.value.recordId,
|
|
2438
|
-
archived: false
|
|
2646
|
+
archived: false,
|
|
2647
|
+
pinned: false
|
|
2439
2648
|
};
|
|
2440
2649
|
const branchLeaves = /* @__PURE__ */ new Map();
|
|
2441
2650
|
branchLeaves.set(MAIN_BRANCH_ID, first.value.recordId);
|
|
@@ -2488,6 +2697,8 @@ var SessionManager = class {
|
|
|
2488
2697
|
if (bId) meta.activeBranchId = bId;
|
|
2489
2698
|
} else if (record.type === "archive_state") {
|
|
2490
2699
|
meta.archived = Boolean(record.payload.archived);
|
|
2700
|
+
} else if (record.type === "pin_state") {
|
|
2701
|
+
meta.pinned = Boolean(record.payload.pinned);
|
|
2491
2702
|
}
|
|
2492
2703
|
if (branchLeaves && !isGlobalControlRecordType(record.type)) {
|
|
2493
2704
|
const bId = record.branchId ?? meta.activeBranchId ?? MAIN_BRANCH_ID;
|
|
@@ -2994,6 +3205,7 @@ var CrossProjectSessionCatalog = class {
|
|
|
2994
3205
|
let title = "\u65B0\u4F1A\u8BDD";
|
|
2995
3206
|
let activeBranchId = MAIN_BRANCH_ID;
|
|
2996
3207
|
let archived = false;
|
|
3208
|
+
let pinned = false;
|
|
2997
3209
|
let lastRecordId = headerRecord.recordId;
|
|
2998
3210
|
for (const line of iterator) {
|
|
2999
3211
|
if (!line.text.trim()) continue;
|
|
@@ -3014,6 +3226,8 @@ var CrossProjectSessionCatalog = class {
|
|
|
3014
3226
|
title = String(rec.payload.title);
|
|
3015
3227
|
} else if (rec.type === "archive_state") {
|
|
3016
3228
|
archived = Boolean(rec.payload.archived);
|
|
3229
|
+
} else if (rec.type === "pin_state") {
|
|
3230
|
+
pinned = Boolean(rec.payload.pinned);
|
|
3017
3231
|
} else if (rec.type === "branch_selected") {
|
|
3018
3232
|
activeBranchId = String(rec.payload.branchId);
|
|
3019
3233
|
}
|
|
@@ -3032,6 +3246,7 @@ var CrossProjectSessionCatalog = class {
|
|
|
3032
3246
|
title,
|
|
3033
3247
|
activeBranchId,
|
|
3034
3248
|
archived,
|
|
3249
|
+
pinned,
|
|
3035
3250
|
size: stat.size,
|
|
3036
3251
|
mtimeMs: stat.mtimeMs,
|
|
3037
3252
|
revision: lastRecordId,
|
|
@@ -3438,7 +3653,6 @@ var WorkspaceLeaseCoordinator = class {
|
|
|
3438
3653
|
}
|
|
3439
3654
|
}
|
|
3440
3655
|
};
|
|
3441
|
-
var sharedWorkspaceLeaseCoordinator = new WorkspaceLeaseCoordinator();
|
|
3442
3656
|
|
|
3443
3657
|
// src/context/manager.ts
|
|
3444
3658
|
var import_node_crypto10 = require("crypto");
|
|
@@ -6170,7 +6384,8 @@ function stopProcessTree(child, nativeGroup) {
|
|
|
6170
6384
|
try {
|
|
6171
6385
|
process.kill(-child.pid, "SIGTERM");
|
|
6172
6386
|
return true;
|
|
6173
|
-
} catch {
|
|
6387
|
+
} catch (error) {
|
|
6388
|
+
if (error.code === "ESRCH") return true;
|
|
6174
6389
|
try {
|
|
6175
6390
|
child.kill("SIGTERM");
|
|
6176
6391
|
} catch {
|
|
@@ -6191,7 +6406,8 @@ function forceProcessTree(child, nativeGroup) {
|
|
|
6191
6406
|
try {
|
|
6192
6407
|
process.kill(-child.pid, "SIGKILL");
|
|
6193
6408
|
return true;
|
|
6194
|
-
} catch {
|
|
6409
|
+
} catch (error) {
|
|
6410
|
+
if (error.code === "ESRCH") return true;
|
|
6195
6411
|
try {
|
|
6196
6412
|
child.kill("SIGKILL");
|
|
6197
6413
|
} catch {
|
|
@@ -6212,13 +6428,31 @@ async function confirmProcessTreeStopped(child, terminationSucceeded, nativeGrou
|
|
|
6212
6428
|
if (process.platform === "win32") return true;
|
|
6213
6429
|
const deadline = Date.now() + 1e3;
|
|
6214
6430
|
while (Date.now() < deadline) {
|
|
6431
|
+
let exists;
|
|
6215
6432
|
try {
|
|
6216
6433
|
process.kill(-child.pid, 0);
|
|
6434
|
+
exists = true;
|
|
6217
6435
|
} catch (error) {
|
|
6218
|
-
|
|
6219
|
-
return
|
|
6436
|
+
const code = error.code;
|
|
6437
|
+
if (code === "ESRCH") return true;
|
|
6438
|
+
if (code !== "EPERM") return false;
|
|
6439
|
+
exists = true;
|
|
6440
|
+
}
|
|
6441
|
+
if (groupHasLiveProcess(child.pid)) {
|
|
6442
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
6443
|
+
continue;
|
|
6220
6444
|
}
|
|
6221
|
-
|
|
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;
|
|
6222
6456
|
}
|
|
6223
6457
|
return false;
|
|
6224
6458
|
}
|
|
@@ -7586,90 +7820,419 @@ var HookRegistry = class {
|
|
|
7586
7820
|
};
|
|
7587
7821
|
|
|
7588
7822
|
// src/runtime/executor/index.ts
|
|
7589
|
-
var import_node_crypto16 = require("crypto");
|
|
7590
7823
|
var import_node_path17 = __toESM(require("path"), 1);
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
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
|
-
await action();
|
|
7654
|
-
this.rollbackActions.shift();
|
|
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)) {
|
|
7863
|
+
try {
|
|
7864
|
+
await audit("tool.decided", { decision: "deny", decisionSource: "repeat_block" });
|
|
7865
|
+
await audit("tool.finished", { status: "blocked", errorCode: "REPEAT_BLOCKED" });
|
|
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
|
+
};
|
|
7879
|
+
}
|
|
7880
|
+
return {
|
|
7881
|
+
result: failure(
|
|
7882
|
+
"Identical failed tool call cannot be repeated in this run",
|
|
7883
|
+
"REPEAT_BLOCKED"
|
|
7884
|
+
)
|
|
7885
|
+
};
|
|
7655
7886
|
}
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
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 } : {}
|
|
7899
|
+
});
|
|
7900
|
+
let permitted = decision.decision === "allow";
|
|
7901
|
+
let source = decision.source;
|
|
7902
|
+
let approvalChoice;
|
|
7903
|
+
if (decision.decision === "ask") {
|
|
7904
|
+
let choice = "deny_once";
|
|
7666
7905
|
try {
|
|
7667
|
-
await
|
|
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 });
|
|
7668
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
|
+
};
|
|
7669
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
|
+
};
|
|
7670
7970
|
}
|
|
7671
|
-
|
|
7672
|
-
|
|
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 完成资源回收及结果审计。 */
|
|
7673
8236
|
cancel() {
|
|
7674
8237
|
this.cancellation.abort();
|
|
7675
8238
|
}
|
|
@@ -7904,191 +8467,28 @@ var ToolExecutor = class {
|
|
|
7904
8467
|
}
|
|
7905
8468
|
return failure("Invalid tool execution identity", "INVALID_TARGET");
|
|
7906
8469
|
}
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
"AUDIT_UNAVAILABLE",
|
|
7930
|
-
"after_user_action",
|
|
7931
|
-
{
|
|
7932
|
-
version: 1,
|
|
7933
|
-
dispatchOutcome: "not_sent",
|
|
7934
|
-
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7935
|
-
}
|
|
7936
|
-
);
|
|
7937
|
-
}
|
|
7938
|
-
return failure(
|
|
7939
|
-
"Identical failed tool call cannot be repeated in this run",
|
|
7940
|
-
"REPEAT_BLOCKED"
|
|
7941
|
-
);
|
|
7942
|
-
}
|
|
7943
|
-
const hardDenied = this.hardDenial?.(tool, finalInput);
|
|
7944
|
-
const cached = this.approvalCache.get(request);
|
|
7945
|
-
const decision = this.policy.decide({
|
|
7946
|
-
hardDenied: hardDenied !== void 0,
|
|
7947
|
-
mode: typeof this.mode === "function" ? this.mode() : this.mode,
|
|
7948
|
-
toolName: call.name,
|
|
7949
|
-
capabilities: request.capabilities,
|
|
7950
|
-
dangerous: tool.metadata?.dangerous,
|
|
7951
|
-
cachedApproval: cached === "allow",
|
|
7952
|
-
cachedDenial: cached === "deny",
|
|
7953
|
-
rules: this.permissionRules,
|
|
7954
|
-
...shell ? { shell } : {}
|
|
7955
|
-
});
|
|
7956
|
-
let permitted = decision.decision === "allow";
|
|
7957
|
-
let source = decision.source;
|
|
7958
|
-
let approvalChoice;
|
|
7959
|
-
if (decision.decision === "ask") {
|
|
7960
|
-
let choice = "deny_once";
|
|
7961
|
-
try {
|
|
7962
|
-
choice = await this.approvalChannel?.requestApproval(request, this.signal) ?? "deny_once";
|
|
7963
|
-
} catch {
|
|
7964
|
-
choice = "cancel";
|
|
7965
|
-
}
|
|
7966
|
-
approvalChoice = choice;
|
|
7967
|
-
if (choice === "allow_session" && request.sessionAllowed)
|
|
7968
|
-
this.approvalCache.set(request, "allow");
|
|
7969
|
-
if (choice === "deny_session") this.approvalCache.set(request, "deny");
|
|
7970
|
-
permitted = choice === "allow_once" || choice === "allow_session" && request.sessionAllowed;
|
|
7971
|
-
source = this.approvalChannel ? "human" : "non_interactive";
|
|
7972
|
-
}
|
|
7973
|
-
auditFields = shell ? {
|
|
7974
|
-
shell: shell.interpreter,
|
|
7975
|
-
cwdDigest: (0, import_node_crypto16.createHash)("sha256").update(shell.cwd).digest("hex"),
|
|
7976
|
-
executableDigest: shell.executableDigest,
|
|
7977
|
-
executionFingerprint: shell.executionFingerprint,
|
|
7978
|
-
...commandAuditSummary(shell.command)
|
|
7979
|
-
} : executionIdentity ? {
|
|
7980
|
-
serverIdentity: executionIdentity.serverIdentity,
|
|
7981
|
-
connectionGeneration: executionIdentity.connectionGeneration,
|
|
7982
|
-
catalogEpoch: executionIdentity.catalogEpoch,
|
|
7983
|
-
toolDigest: executionIdentity.toolDigest
|
|
7984
|
-
} : {};
|
|
7985
|
-
try {
|
|
7986
|
-
await audit("tool.decided", {
|
|
7987
|
-
decision: permitted ? "allow" : "deny",
|
|
7988
|
-
decisionSource: source,
|
|
7989
|
-
...approvalChoice ? { approvalChoice } : {},
|
|
7990
|
-
...hardDenied?.fields ?? {},
|
|
7991
|
-
...auditFields
|
|
7992
|
-
});
|
|
7993
|
-
} catch {
|
|
7994
|
-
return failure(
|
|
7995
|
-
"Audit unavailable; tool was not started",
|
|
7996
|
-
"AUDIT_UNAVAILABLE",
|
|
7997
|
-
"after_user_action",
|
|
7998
|
-
{
|
|
7999
|
-
version: 1,
|
|
8000
|
-
dispatchOutcome: "not_sent",
|
|
8001
|
-
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
8002
|
-
}
|
|
8003
|
-
);
|
|
8004
|
-
}
|
|
8005
|
-
if (!permitted || this.signal?.aborted) {
|
|
8006
|
-
const code = this.signal?.aborted ? "ABORTED" : hardDenied?.code ?? "PERMISSION_DENIED";
|
|
8007
|
-
try {
|
|
8008
|
-
await audit("tool.finished", { status: "denied", errorCode: code });
|
|
8009
|
-
} catch {
|
|
8010
|
-
return failure("Audit result unavailable", "AUDIT_UNAVAILABLE", "after_user_action", {
|
|
8011
|
-
version: 1,
|
|
8012
|
-
dispatchOutcome: "not_sent",
|
|
8013
|
-
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
8014
|
-
});
|
|
8015
|
-
}
|
|
8016
|
-
return failure(
|
|
8017
|
-
this.signal?.aborted ? "Operation cancelled" : hardDenied?.message ?? "Tool permission denied",
|
|
8018
|
-
code,
|
|
8019
|
-
this.signal?.aborted ? "after_user_action" : hardDenied?.retryPolicy ?? "after_user_action"
|
|
8020
|
-
);
|
|
8021
|
-
}
|
|
8022
|
-
if (shell) {
|
|
8023
|
-
try {
|
|
8024
|
-
const next = tool.approvalScope?.(finalInput, this.rootDir, this.executionCwd);
|
|
8025
|
-
if (next?.executionFingerprint !== shell.executionFingerprint) {
|
|
8026
|
-
if (attempt === 0 && next) {
|
|
8027
|
-
try {
|
|
8028
|
-
await audit("tool.target_changed", { status: "redeciding" });
|
|
8029
|
-
} catch {
|
|
8030
|
-
return failure(
|
|
8031
|
-
"Audit unavailable; tool was not started",
|
|
8032
|
-
"AUDIT_UNAVAILABLE",
|
|
8033
|
-
"after_user_action",
|
|
8034
|
-
{
|
|
8035
|
-
version: 1,
|
|
8036
|
-
dispatchOutcome: "not_sent",
|
|
8037
|
-
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
8038
|
-
}
|
|
8039
|
-
);
|
|
8040
|
-
}
|
|
8041
|
-
shell = next;
|
|
8042
|
-
continue;
|
|
8043
|
-
}
|
|
8044
|
-
await audit("tool.finished", { status: "target_changed", errorCode: "TARGET_CHANGED" });
|
|
8045
|
-
return failure(
|
|
8046
|
-
"Execution target changed after approval",
|
|
8047
|
-
"TARGET_CHANGED",
|
|
8048
|
-
"after_user_action"
|
|
8049
|
-
);
|
|
8050
|
-
}
|
|
8051
|
-
} catch {
|
|
8052
|
-
return failure(
|
|
8053
|
-
"Execution target changed after approval",
|
|
8054
|
-
"TARGET_CHANGED",
|
|
8055
|
-
"after_user_action"
|
|
8056
|
-
);
|
|
8057
|
-
}
|
|
8058
|
-
}
|
|
8059
|
-
if (executionIdentity) {
|
|
8060
|
-
try {
|
|
8061
|
-
const nextIdentity = tool.executionIdentity?.(finalInput, this.rootDir);
|
|
8062
|
-
if (!nextIdentity || nextIdentity.serverIdentity !== executionIdentity.serverIdentity || nextIdentity.connectionGeneration !== executionIdentity.connectionGeneration || nextIdentity.catalogEpoch !== executionIdentity.catalogEpoch || nextIdentity.toolDigest !== executionIdentity.toolDigest) {
|
|
8063
|
-
await audit("tool.finished", {
|
|
8064
|
-
status: "identity_changed",
|
|
8065
|
-
errorCode: "MCP_TOOL_CHANGED"
|
|
8066
|
-
});
|
|
8067
|
-
return failure(
|
|
8068
|
-
"Tool execution identity changed after approval",
|
|
8069
|
-
"MCP_TOOL_CHANGED",
|
|
8070
|
-
"after_user_action",
|
|
8071
|
-
{
|
|
8072
|
-
version: 1,
|
|
8073
|
-
dispatchOutcome: "not_sent",
|
|
8074
|
-
hostError: { phase: "validation", code: "MCP_TOOL_CHANGED" }
|
|
8075
|
-
}
|
|
8076
|
-
);
|
|
8077
|
-
}
|
|
8078
|
-
} catch {
|
|
8079
|
-
return failure(
|
|
8080
|
-
"Tool execution identity check failed after approval",
|
|
8081
|
-
"MCP_TOOL_CHANGED",
|
|
8082
|
-
"after_user_action",
|
|
8083
|
-
{
|
|
8084
|
-
version: 1,
|
|
8085
|
-
dispatchOutcome: "not_sent"
|
|
8086
|
-
}
|
|
8087
|
-
);
|
|
8088
|
-
}
|
|
8089
|
-
}
|
|
8090
|
-
break;
|
|
8091
|
-
}
|
|
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 ?? {};
|
|
8092
8492
|
try {
|
|
8093
8493
|
await audit("tool.started", { status: "started", ...auditFields });
|
|
8094
8494
|
} catch {
|
|
@@ -8117,6 +8517,7 @@ var ToolExecutor = class {
|
|
|
8117
8517
|
this.signal?.addEventListener("abort", abortFromSession, { once: true });
|
|
8118
8518
|
}
|
|
8119
8519
|
const toolCtx = {
|
|
8520
|
+
leaseCoordinator: this.leaseCoordinator,
|
|
8120
8521
|
workspaceRoot: this.workspaceRoot,
|
|
8121
8522
|
executionCwd: this.executionCwd,
|
|
8122
8523
|
sandbox: this.sandbox,
|
|
@@ -8147,9 +8548,10 @@ var ToolExecutor = class {
|
|
|
8147
8548
|
{ signal: executionController.signal }
|
|
8148
8549
|
);
|
|
8149
8550
|
}
|
|
8150
|
-
const rawResult = tool.metadata?.managesTimeout ? await tool.execute(finalInput, toolCtx) : await
|
|
8551
|
+
const rawResult = tool.metadata?.managesTimeout ? await tool.execute(finalInput, toolCtx) : await withToolTimeout(
|
|
8151
8552
|
tool.execute(finalInput, toolCtx),
|
|
8152
8553
|
call.name,
|
|
8554
|
+
this.toolTimeoutMs,
|
|
8153
8555
|
executionController
|
|
8154
8556
|
);
|
|
8155
8557
|
if (typeof tool.toExecutionResult === "function") {
|
|
@@ -8210,105 +8612,18 @@ var ToolExecutor = class {
|
|
|
8210
8612
|
dispatchOutcome: outcome
|
|
8211
8613
|
};
|
|
8212
8614
|
}
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
retryPolicy: resultBlock.retryPolicy,
|
|
8225
|
-
message: outputStr
|
|
8226
|
-
});
|
|
8227
|
-
}
|
|
8228
|
-
try {
|
|
8229
|
-
for (const hook of this.hooks.get("tool:after")) {
|
|
8230
|
-
await hook(
|
|
8231
|
-
hookCtx,
|
|
8232
|
-
{ id: call.id, name: call.name, input: finalInput },
|
|
8233
|
-
{ output: outputStr, isError }
|
|
8234
|
-
);
|
|
8235
|
-
}
|
|
8236
|
-
} catch {
|
|
8237
|
-
resultBlock.content = JSON.stringify({
|
|
8238
|
-
code: "HOOK_ERROR",
|
|
8239
|
-
retryPolicy: "after_user_action",
|
|
8240
|
-
message: "Tool finished but result processing failed"
|
|
8241
|
-
});
|
|
8242
|
-
resultBlock.isError = true;
|
|
8243
|
-
resultBlock.errorCode = "HOOK_ERROR";
|
|
8244
|
-
resultBlock.retryPolicy = "after_user_action";
|
|
8245
|
-
resultBlock.execution = {
|
|
8246
|
-
version: 1,
|
|
8247
|
-
dispatchOutcome: resultBlock.execution?.dispatchOutcome ?? "failed",
|
|
8248
|
-
hostError: {
|
|
8249
|
-
phase: "hook",
|
|
8250
|
-
code: "HOOK_ERROR"
|
|
8251
|
-
}
|
|
8252
|
-
};
|
|
8253
|
-
}
|
|
8254
|
-
resultBlock.durationMs = Math.max(0, Date.now() - startedAt);
|
|
8255
|
-
if (resultBlock.isError && resultBlock.retryPolicy === "never") {
|
|
8256
|
-
this.neverFailures.add(
|
|
8257
|
-
(0, import_node_crypto16.createHash)("sha256").update(stableJsonStringify([call.name, finalInput])).digest("hex")
|
|
8258
|
-
);
|
|
8259
|
-
}
|
|
8260
|
-
try {
|
|
8261
|
-
await audit("tool.finished", {
|
|
8262
|
-
status: resultBlock.isError ? "failed" : "completed",
|
|
8263
|
-
...resultBlock.errorCode ? { errorCode: resultBlock.errorCode } : {},
|
|
8264
|
-
durationMs: resultBlock.durationMs
|
|
8265
|
-
});
|
|
8266
|
-
} catch {
|
|
8267
|
-
resultBlock.isError = true;
|
|
8268
|
-
resultBlock.errorCode = "OUTCOME_UNKNOWN";
|
|
8269
|
-
resultBlock.retryPolicy = "after_user_action";
|
|
8270
|
-
resultBlock.content = JSON.stringify({
|
|
8271
|
-
code: "OUTCOME_UNKNOWN",
|
|
8272
|
-
retryPolicy: "after_user_action",
|
|
8273
|
-
message: "Tool outcome is unknown because the audit result could not be written"
|
|
8274
|
-
});
|
|
8275
|
-
resultBlock.execution = {
|
|
8276
|
-
version: 1,
|
|
8277
|
-
dispatchOutcome: resultBlock.execution?.dispatchOutcome ?? "succeeded",
|
|
8278
|
-
hostError: {
|
|
8279
|
-
phase: "audit",
|
|
8280
|
-
code: "AUDIT_UNAVAILABLE"
|
|
8281
|
-
}
|
|
8282
|
-
};
|
|
8283
|
-
return resultBlock;
|
|
8284
|
-
}
|
|
8285
|
-
await this.onToolResult?.(tool, finalInput, resultBlock);
|
|
8286
|
-
return resultBlock;
|
|
8287
|
-
}
|
|
8288
|
-
/**
|
|
8289
|
-
* 给工具执行加超时护栏:Promise.race 在超时后reject,循环拿到 isError 结果继续运转,
|
|
8290
|
-
* 不会因某个异步工具(如未来接入的网络/exec 工具)挂起而永久卡死。
|
|
8291
|
-
*
|
|
8292
|
-
* 局限:同步 CPU 密集型实现会阻塞事件循环,timer 同样无法触发;
|
|
8293
|
-
* 因此内置工具仍需避免执行用户可控的同步高复杂度计算。
|
|
8294
|
-
*/
|
|
8295
|
-
withTimeout(promise, toolName, executionController) {
|
|
8296
|
-
let timer;
|
|
8297
|
-
const timeout = new Promise((_, reject) => {
|
|
8298
|
-
timer = setTimeout(() => {
|
|
8299
|
-
const error = new KapibalaError(
|
|
8300
|
-
`Tool '${toolName}' timed out after ${this.toolTimeoutMs}ms`,
|
|
8301
|
-
{
|
|
8302
|
-
code: "OUTCOME_UNKNOWN",
|
|
8303
|
-
retryPolicy: "after_user_action",
|
|
8304
|
-
safeMessage: "Tool timed out; completion of side effects is unknown"
|
|
8305
|
-
}
|
|
8306
|
-
);
|
|
8307
|
-
reject(error);
|
|
8308
|
-
executionController.abort(error);
|
|
8309
|
-
}, 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 }
|
|
8310
8626
|
});
|
|
8311
|
-
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
8312
8627
|
}
|
|
8313
8628
|
/**
|
|
8314
8629
|
* 构建当前会话在派生时刻的父权限策略快照。
|
|
@@ -8907,7 +9222,7 @@ var OpenAICompatibleProvider = class {
|
|
|
8907
9222
|
};
|
|
8908
9223
|
|
|
8909
9224
|
// src/models/anthropic/index.ts
|
|
8910
|
-
var
|
|
9225
|
+
var import_node_crypto18 = require("crypto");
|
|
8911
9226
|
function invalidResponse2() {
|
|
8912
9227
|
throw new ModelError("Invalid Anthropic response state", {
|
|
8913
9228
|
code: "MODEL_INVALID_RESPONSE",
|
|
@@ -8922,7 +9237,7 @@ function wireToolId(id) {
|
|
|
8922
9237
|
stage: "request",
|
|
8923
9238
|
retryable: false
|
|
8924
9239
|
});
|
|
8925
|
-
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)}`;
|
|
8926
9241
|
}
|
|
8927
9242
|
var AnthropicProvider = class {
|
|
8928
9243
|
name = "anthropic";
|
|
@@ -9288,7 +9603,7 @@ var AnthropicProvider = class {
|
|
|
9288
9603
|
};
|
|
9289
9604
|
|
|
9290
9605
|
// src/models/openai-responses/index.ts
|
|
9291
|
-
var
|
|
9606
|
+
var import_node_crypto19 = require("crypto");
|
|
9292
9607
|
function invalid(message = "Invalid Responses output Item") {
|
|
9293
9608
|
throw new ModelError(message, {
|
|
9294
9609
|
code: "MODEL_INVALID_RESPONSE",
|
|
@@ -9309,7 +9624,7 @@ function wireCallId(id) {
|
|
|
9309
9624
|
stage: "request",
|
|
9310
9625
|
retryable: false
|
|
9311
9626
|
});
|
|
9312
|
-
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)}`;
|
|
9313
9628
|
}
|
|
9314
9629
|
function validateItem(item, final) {
|
|
9315
9630
|
if (!item || typeof item !== "object" || !nonempty(item.id)) invalid();
|
|
@@ -10006,13 +10321,99 @@ Follow these key behavioral rules:
|
|
|
10006
10321
|
}
|
|
10007
10322
|
return lines.join("\n");
|
|
10008
10323
|
}
|
|
10009
|
-
getL3Environment() {
|
|
10010
|
-
const platform = import_node_os6.default.platform();
|
|
10011
|
-
return `# Environment Context:
|
|
10012
|
-
- Current Working Directory (Workspace Root): ${this.rootDir}
|
|
10013
|
-
- 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);
|
|
10014
10415
|
}
|
|
10015
|
-
}
|
|
10416
|
+
}
|
|
10016
10417
|
|
|
10017
10418
|
// src/runtime/loop/index.ts
|
|
10018
10419
|
var AgentLoop = class {
|
|
@@ -10096,26 +10497,7 @@ var AgentLoop = class {
|
|
|
10096
10497
|
request = modified;
|
|
10097
10498
|
}
|
|
10098
10499
|
}
|
|
10099
|
-
|
|
10100
|
-
for (const t of request.tools) {
|
|
10101
|
-
const original = toolSnapshot.get(t.name);
|
|
10102
|
-
if (!original) {
|
|
10103
|
-
throw new ModelError(`Hook injected tool '${t.name}' not present in snapshot`, {
|
|
10104
|
-
stage: "request",
|
|
10105
|
-
retryable: false
|
|
10106
|
-
});
|
|
10107
|
-
}
|
|
10108
|
-
if (t.description !== original.description || JSON.stringify(t.parameters) !== JSON.stringify(original.parameters)) {
|
|
10109
|
-
throw new ModelError(
|
|
10110
|
-
`Hook mutated tool definition for '${t.name}' (description or parameters drifted from snapshot)`,
|
|
10111
|
-
{
|
|
10112
|
-
stage: "request",
|
|
10113
|
-
retryable: false
|
|
10114
|
-
}
|
|
10115
|
-
);
|
|
10116
|
-
}
|
|
10117
|
-
}
|
|
10118
|
-
}
|
|
10500
|
+
assertHookToolDefinitionsIntact(request, toolSnapshot);
|
|
10119
10501
|
if (this.prepareRequest) request = yield* this.prepareRequest(request, history);
|
|
10120
10502
|
yield {
|
|
10121
10503
|
type: "step_log",
|
|
@@ -10266,22 +10648,7 @@ var AgentLoop = class {
|
|
|
10266
10648
|
stage: "stream",
|
|
10267
10649
|
retryable: false
|
|
10268
10650
|
});
|
|
10269
|
-
|
|
10270
|
-
history.flatMap(
|
|
10271
|
-
(message) => message.content.filter((block) => block.type === "tool_use").map((block) => block.id)
|
|
10272
|
-
)
|
|
10273
|
-
);
|
|
10274
|
-
for (const call of toolCalls) {
|
|
10275
|
-
if (!call.name?.trim() || !call.input || typeof call.input !== "object" || Array.isArray(call.input))
|
|
10276
|
-
throw new ModelError("Invalid tool call arguments or name; no tools executed", {
|
|
10277
|
-
code: "MODEL_INVALID_RESPONSE",
|
|
10278
|
-
stage: "stream",
|
|
10279
|
-
retryable: false
|
|
10280
|
-
});
|
|
10281
|
-
if (!call.id?.trim() || knownCalls.has(call.id))
|
|
10282
|
-
throw new ModelError("Duplicate tool call ID; no tools executed");
|
|
10283
|
-
knownCalls.add(call.id);
|
|
10284
|
-
}
|
|
10651
|
+
assertToolCallsExecutable(history, toolCalls);
|
|
10285
10652
|
break;
|
|
10286
10653
|
} catch (error) {
|
|
10287
10654
|
if (!(error instanceof ContextOverflowError) || outputSeen || accumulatedText || accumulatedThinking || toolCalls.length || overflowRetried || !this.prepareRequest || this.signal?.aborted)
|
|
@@ -10394,47 +10761,9 @@ var AgentLoop = class {
|
|
|
10394
10761
|
timestamp: Date.now()
|
|
10395
10762
|
};
|
|
10396
10763
|
validateProtocolContent(assistantMessage.content);
|
|
10397
|
-
const
|
|
10398
|
-
const identity = stableSerialize({ ...assistantMessage, content: void 0 });
|
|
10399
|
-
const originalTools = stableSerialize(
|
|
10400
|
-
assistantMessage.content.filter(
|
|
10401
|
-
(block) => block.type === "tool_use" || block.type === "tool_result"
|
|
10402
|
-
)
|
|
10403
|
-
);
|
|
10404
|
-
const privateOutput = assistantMessage.content.some(
|
|
10405
|
-
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.origin || block.signature)
|
|
10406
|
-
);
|
|
10764
|
+
const integrity = snapshotAssistantIntegrity(assistantMessage);
|
|
10407
10765
|
await this.hooks.emit("model:after", hookCtx, { message: assistantMessage });
|
|
10408
|
-
|
|
10409
|
-
throw new ModelError("Hook changed assistant message identity", {
|
|
10410
|
-
stage: "request",
|
|
10411
|
-
retryable: false
|
|
10412
|
-
});
|
|
10413
|
-
if (originalTools !== stableSerialize(
|
|
10414
|
-
assistantMessage.content.filter(
|
|
10415
|
-
(block) => block.type === "tool_use" || block.type === "tool_result"
|
|
10416
|
-
)
|
|
10417
|
-
))
|
|
10418
|
-
throw new ModelError("Hook changed validated tool model output", {
|
|
10419
|
-
stage: "request",
|
|
10420
|
-
retryable: false
|
|
10421
|
-
});
|
|
10422
|
-
if (originalContent !== stableSerialize(assistantMessage.content)) {
|
|
10423
|
-
if (privateOutput || assistantMessage.content.some(
|
|
10424
|
-
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.origin || block.signature)
|
|
10425
|
-
))
|
|
10426
|
-
throw new ModelError("Hook changed signed or private model output", {
|
|
10427
|
-
stage: "request",
|
|
10428
|
-
retryable: false
|
|
10429
|
-
});
|
|
10430
|
-
if (toolCalls.length)
|
|
10431
|
-
throw new ModelError("Hook changed validated tool model output", {
|
|
10432
|
-
stage: "request",
|
|
10433
|
-
retryable: false
|
|
10434
|
-
});
|
|
10435
|
-
for (const block of assistantMessage.content)
|
|
10436
|
-
if (block.type === "text" || block.type === "tool_use") block.protocolMeta = void 0;
|
|
10437
|
-
}
|
|
10766
|
+
assertAssistantMessageUntampered(assistantMessage, integrity);
|
|
10438
10767
|
validateProtocolContent(assistantMessage.content);
|
|
10439
10768
|
if (toolCalls.length === 0) {
|
|
10440
10769
|
history.push(assistantMessage);
|
|
@@ -10839,6 +11168,7 @@ var SessionInbox = class _SessionInbox {
|
|
|
10839
11168
|
|
|
10840
11169
|
// src/runtime/agent/runtime.ts
|
|
10841
11170
|
var AgentRuntime = class {
|
|
11171
|
+
runnerContexts = /* @__PURE__ */ new Map();
|
|
10842
11172
|
tasks = /* @__PURE__ */ new Map();
|
|
10843
11173
|
controllers = /* @__PURE__ */ new Map();
|
|
10844
11174
|
timers = /* @__PURE__ */ new Map();
|
|
@@ -10922,6 +11252,13 @@ var AgentRuntime = class {
|
|
|
10922
11252
|
* - 登记为 preparing 状态并返回 accepted 回执,使父工具事务能立刻闭合。
|
|
10923
11253
|
*/
|
|
10924
11254
|
async prepareSpawn(context, params) {
|
|
11255
|
+
const coordinator = context.leaseCoordinator;
|
|
11256
|
+
if (!coordinator || typeof coordinator !== "object" || typeof coordinator.isLocked !== "function" || typeof coordinator.getActiveLease !== "function" || typeof coordinator.tryAcquire !== "function" || typeof coordinator.acquire !== "function") {
|
|
11257
|
+
throw new KapibalaError("Spawn requires the parent workspace lease coordinator", {
|
|
11258
|
+
code: "CONFIG_INVALID",
|
|
11259
|
+
safeMessage: "\u5FC5\u987B\u663E\u5F0F\u4F20\u5165\u7236\u4F1A\u8BDD\u5199\u79DF\u7EA6\u534F\u8C03\u5668"
|
|
11260
|
+
});
|
|
11261
|
+
}
|
|
10925
11262
|
if (this.isClosed) {
|
|
10926
11263
|
throw new KapibalaError("AgentRuntime is closed", {
|
|
10927
11264
|
code: "CONFIG_INVALID",
|
|
@@ -11064,6 +11401,7 @@ var AgentRuntime = class {
|
|
|
11064
11401
|
timeoutMs
|
|
11065
11402
|
})
|
|
11066
11403
|
};
|
|
11404
|
+
this.runnerContexts.set(agentId, Object.freeze({ leaseCoordinator: context.leaseCoordinator }));
|
|
11067
11405
|
this.tasks.set(agentId, taskRecord);
|
|
11068
11406
|
this.totalActiveCount++;
|
|
11069
11407
|
this.treeActiveCounts.set(rootTaskId, treeActive + 1);
|
|
@@ -11116,6 +11454,7 @@ var AgentRuntime = class {
|
|
|
11116
11454
|
const task = this.tasks.get(agentId);
|
|
11117
11455
|
if (!task) return;
|
|
11118
11456
|
if (task.status === "preparing") {
|
|
11457
|
+
this.runnerContexts.delete(agentId);
|
|
11119
11458
|
task.status = "cancelled";
|
|
11120
11459
|
task.error = "Parent transaction aborted before commit";
|
|
11121
11460
|
task.finishedAt = Date.now();
|
|
@@ -11141,7 +11480,10 @@ var AgentRuntime = class {
|
|
|
11141
11480
|
this.timers.set(task.agentId, timer);
|
|
11142
11481
|
let runError;
|
|
11143
11482
|
try {
|
|
11144
|
-
const
|
|
11483
|
+
const context = this.runnerContexts.get(task.agentId);
|
|
11484
|
+
if (!context)
|
|
11485
|
+
throw new KapibalaError("Runner context is unavailable", { code: "CONFIG_INVALID" });
|
|
11486
|
+
const result = await this.runner(task, controller.signal, context);
|
|
11145
11487
|
if (task.status === "cancelling") {
|
|
11146
11488
|
task.status = "cancelled";
|
|
11147
11489
|
return;
|
|
@@ -11190,14 +11532,17 @@ var AgentRuntime = class {
|
|
|
11190
11532
|
task.status = "failed";
|
|
11191
11533
|
}
|
|
11192
11534
|
task.error = runError.message;
|
|
11193
|
-
await this.cascadeCancelChildren(task.agentId, "Parent task failed");
|
|
11194
11535
|
} finally {
|
|
11536
|
+
this.runnerContexts.delete(task.agentId);
|
|
11195
11537
|
this.clearTaskTimer(task.agentId);
|
|
11196
11538
|
this.controllers.delete(task.agentId);
|
|
11197
11539
|
if (!isTerminalTaskStatus(task.status)) {
|
|
11198
11540
|
task.status = "failed";
|
|
11199
11541
|
task.error = runError?.message ?? "Unknown execution failure";
|
|
11200
11542
|
}
|
|
11543
|
+
if (task.status === "failed") {
|
|
11544
|
+
await this.cascadeCancelChildren(task.agentId, task.error ?? "Parent task failed");
|
|
11545
|
+
}
|
|
11201
11546
|
task.finishedAt = Date.now();
|
|
11202
11547
|
this.releaseTaskSlotOnce(task);
|
|
11203
11548
|
this.outbox.enqueueFromTask(task);
|
|
@@ -11267,6 +11612,7 @@ var AgentRuntime = class {
|
|
|
11267
11612
|
controller?.abort(new Error(reason));
|
|
11268
11613
|
await this.cascadeCancelChildren(agentId, reason);
|
|
11269
11614
|
const inFlight = this.runningPromises.get(agentId);
|
|
11615
|
+
if (!inFlight) this.runnerContexts.delete(agentId);
|
|
11270
11616
|
if (inFlight) {
|
|
11271
11617
|
const GRACE_PERIOD_MS = 5e3;
|
|
11272
11618
|
let timer;
|
|
@@ -11375,6 +11721,7 @@ var AgentRuntime = class {
|
|
|
11375
11721
|
}
|
|
11376
11722
|
this.timers.clear();
|
|
11377
11723
|
this.controllers.clear();
|
|
11724
|
+
this.runnerContexts.clear();
|
|
11378
11725
|
this.stateListeners.clear();
|
|
11379
11726
|
}
|
|
11380
11727
|
/**
|
|
@@ -11515,9 +11862,15 @@ function createSpawnAgentTool(runtime) {
|
|
|
11515
11862
|
required: ["description", "prompt", "role"]
|
|
11516
11863
|
},
|
|
11517
11864
|
async execute(input, ctx) {
|
|
11865
|
+
if (!ctx.leaseCoordinator)
|
|
11866
|
+
throw new KapibalaError("Missing parent lease coordinator", {
|
|
11867
|
+
code: "CONFIG_INVALID",
|
|
11868
|
+
safeMessage: "\u7F3A\u5C11\u7236\u4F1A\u8BDD\u5199\u79DF\u7EA6\u534F\u8C03\u5668"
|
|
11869
|
+
});
|
|
11518
11870
|
const workspaceRoot = ctx.workspaceRoot ?? ctx.rootDir;
|
|
11519
11871
|
const executionCwd = ctx.executionCwd ?? workspaceRoot;
|
|
11520
11872
|
const spawnContext = {
|
|
11873
|
+
leaseCoordinator: ctx.leaseCoordinator,
|
|
11521
11874
|
rootTaskId: ctx.rootTaskId,
|
|
11522
11875
|
sessionId: ctx.sessionId ?? "session-default",
|
|
11523
11876
|
branchId: ctx.branchId ?? "main",
|
|
@@ -11796,6 +12149,274 @@ function cancellableGenerator(create, external) {
|
|
|
11796
12149
|
};
|
|
11797
12150
|
}
|
|
11798
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
|
+
|
|
11799
12420
|
// src/context/session/index.ts
|
|
11800
12421
|
var AgentSession = class {
|
|
11801
12422
|
skillRegistry;
|
|
@@ -11880,41 +12501,27 @@ var AgentSession = class {
|
|
|
11880
12501
|
this.cwd = resolvedWorkspace.executionCwd;
|
|
11881
12502
|
this.projectKey = resolvedWorkspace.projectKey;
|
|
11882
12503
|
this.sandbox = config.sandbox ?? new PathSandbox({ workspaceRoot: this.workspaceRoot });
|
|
11883
|
-
this.leaseCoordinator = config.leaseCoordinator ??
|
|
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;
|
|
11884
12518
|
this.tools = new ToolRegistry();
|
|
11885
12519
|
if (config.skills) {
|
|
11886
12520
|
this.skillRegistry = new SkillRegistry(config.skills);
|
|
11887
12521
|
this.skillRuntime = new SkillRuntime(
|
|
11888
12522
|
this.skillRegistry,
|
|
11889
12523
|
this.tools,
|
|
11890
|
-
|
|
11891
|
-
try {
|
|
11892
|
-
await this.eventLogger.recordAudit({
|
|
11893
|
-
level: "info",
|
|
11894
|
-
event: "skill.resource_read",
|
|
11895
|
-
sessionId: this.sessionId,
|
|
11896
|
-
runId: source.runId,
|
|
11897
|
-
fields: {
|
|
11898
|
-
skillName: source.name,
|
|
11899
|
-
contentDigest: source.contentDigest,
|
|
11900
|
-
invocationId: source.invocationId,
|
|
11901
|
-
pathDigest: skillDigest(input.path),
|
|
11902
|
-
resourceDigest: skillDigest(page.text),
|
|
11903
|
-
outputBytes: Buffer.byteLength(page.text),
|
|
11904
|
-
outputTruncated: page.truncated,
|
|
11905
|
-
offset: input.offset,
|
|
11906
|
-
startLine: input.startLine,
|
|
11907
|
-
endLine: input.endLine
|
|
11908
|
-
}
|
|
11909
|
-
});
|
|
11910
|
-
} catch {
|
|
11911
|
-
throw new KapibalaError("Skill resource audit unavailable", {
|
|
11912
|
-
code: "AUDIT_UNAVAILABLE",
|
|
11913
|
-
retryPolicy: "after_user_action",
|
|
11914
|
-
safeMessage: "\u6280\u80FD\u8D44\u6E90\u8BFB\u53D6\u5BA1\u8BA1\u4E0D\u53EF\u7528\uFF0C\u672C\u8F6E\u505C\u6B62"
|
|
11915
|
-
});
|
|
11916
|
-
}
|
|
11917
|
-
}
|
|
12524
|
+
createSkillResourceAuditor(this.eventLogger, this.sessionId)
|
|
11918
12525
|
);
|
|
11919
12526
|
}
|
|
11920
12527
|
this.hooks = new HookRegistry();
|
|
@@ -11927,19 +12534,6 @@ var AgentSession = class {
|
|
|
11927
12534
|
this.mode = config.mode ?? "Approval";
|
|
11928
12535
|
this.permissionRules = config.permissionRules ?? [];
|
|
11929
12536
|
this.approvalChannel = config.approvalChannel;
|
|
11930
|
-
this.userInstructionsPath = config.userInstructionsPath;
|
|
11931
|
-
const sinks = config.eventLogger ? void 0 : createDefaultLogSinks(config.loggingDirectory);
|
|
11932
|
-
const eventLogger = config.eventLogger ?? new StructuredLogger({
|
|
11933
|
-
operationSink: sinks.operationSink,
|
|
11934
|
-
auditSink: sinks.auditSink,
|
|
11935
|
-
debug: config.logger !== void 0,
|
|
11936
|
-
onDiagnostic: config.onDiagnostic ?? config.logger
|
|
11937
|
-
});
|
|
11938
|
-
this.eventLogger = {
|
|
11939
|
-
record: (input) => eventLogger.record({ ...input, conversationId: this.persistentId }),
|
|
11940
|
-
recordAudit: (input) => eventLogger.recordAudit({ ...input, conversationId: this.persistentId })
|
|
11941
|
-
};
|
|
11942
|
-
this.auditRecoveryDirectory = config.auditRecoveryDirectory ?? sinks?.auditSink.directory;
|
|
11943
12537
|
this.rootTaskId = config.rootTaskId;
|
|
11944
12538
|
this.depth = config.depth ?? 0;
|
|
11945
12539
|
this.agentId = config.agentId;
|
|
@@ -12544,71 +13138,21 @@ var AgentSession = class {
|
|
|
12544
13138
|
runId,
|
|
12545
13139
|
modelId: profile.id
|
|
12546
13140
|
}) : this.context.project(history);
|
|
12547
|
-
|
|
12548
|
-
const original = sourceView.find((source) => source.id === message.id);
|
|
12549
|
-
const privateBag = /* @__PURE__ */ new Map();
|
|
12550
|
-
const blockBag = /* @__PURE__ */ new Map();
|
|
12551
|
-
for (const block of original?.content ?? []) {
|
|
12552
|
-
const serialized = stableSerialize(block);
|
|
12553
|
-
if (block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && block.origin)
|
|
12554
|
-
privateBag.set(serialized, (privateBag.get(serialized) ?? 0) + 1);
|
|
12555
|
-
blockBag.set(serialized, (blockBag.get(serialized) ?? 0) + 1);
|
|
12556
|
-
}
|
|
12557
|
-
const take = (bag, serialized) => {
|
|
12558
|
-
const count = bag.get(serialized) ?? 0;
|
|
12559
|
-
if (!count) return false;
|
|
12560
|
-
if (count === 1) bag.delete(serialized);
|
|
12561
|
-
else bag.set(serialized, count - 1);
|
|
12562
|
-
return true;
|
|
12563
|
-
};
|
|
12564
|
-
for (const block of message.content) {
|
|
12565
|
-
if (block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && block.origin) {
|
|
12566
|
-
if (!original || !take(privateBag, stableSerialize(block)))
|
|
12567
|
-
throw new ModelError("Hook changed signed or private request provenance", {
|
|
12568
|
-
stage: "request",
|
|
12569
|
-
retryable: false
|
|
12570
|
-
});
|
|
12571
|
-
}
|
|
12572
|
-
if (block.type !== "text" && block.type !== "tool_use") continue;
|
|
12573
|
-
if (block.protocolMeta && !take(blockBag, stableSerialize(block)))
|
|
12574
|
-
block.protocolMeta = void 0;
|
|
12575
|
-
}
|
|
12576
|
-
}
|
|
12577
|
-
const frozen = Boolean(
|
|
12578
|
-
previousRequest && history.some(
|
|
12579
|
-
(message) => message.interactionId === runId && message.content.some(
|
|
12580
|
-
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.signature && block.origin?.runId === runId)
|
|
12581
|
-
)
|
|
12582
|
-
)
|
|
12583
|
-
);
|
|
13141
|
+
verifyPrivateBlockIntegrity(projected, sourceView);
|
|
12584
13142
|
const candidate = {
|
|
12585
13143
|
...request,
|
|
12586
13144
|
messages: projected,
|
|
12587
13145
|
context: { runId, modelId: profile.id },
|
|
12588
13146
|
signal: options?.signal
|
|
12589
13147
|
};
|
|
12590
|
-
|
|
12591
|
-
|
|
12592
|
-
|
|
12593
|
-
|
|
12594
|
-
|
|
12595
|
-
|
|
12596
|
-
|
|
12597
|
-
|
|
12598
|
-
for (const original of generated) {
|
|
12599
|
-
const matches = candidate.messages.filter((message) => message.id === original.id);
|
|
12600
|
-
if (matches.length !== 1 || stableSerialize(matches[0]) !== stableSerialize(original))
|
|
12601
|
-
throw new ModelError("Signed continuation prefix or tool chain changed", {
|
|
12602
|
-
stage: "request",
|
|
12603
|
-
retryable: false
|
|
12604
|
-
});
|
|
12605
|
-
}
|
|
12606
|
-
}
|
|
12607
|
-
if (frozen && previousRequest && (stableSerialize(candidate.messages.slice(0, previousRequest.messages.length)) !== stableSerialize(previousRequest.messages) || candidate.systemPrompt !== previousRequest.systemPrompt || stableSerialize(candidate.tools) !== stableSerialize(previousRequest.tools)))
|
|
12608
|
-
throw new ModelError("Signed continuation prefix changed", {
|
|
12609
|
-
stage: "request",
|
|
12610
|
-
retryable: false
|
|
12611
|
-
});
|
|
13148
|
+
const frozen = assertSignedContinuationStable({
|
|
13149
|
+
candidate,
|
|
13150
|
+
previousRequest,
|
|
13151
|
+
history,
|
|
13152
|
+
runId,
|
|
13153
|
+
nativeBinding,
|
|
13154
|
+
projectHistory: (projectedHistory) => this.context.project(projectedHistory)
|
|
13155
|
+
});
|
|
12612
13156
|
const context = this.context;
|
|
12613
13157
|
const skills = this.skillRuntime;
|
|
12614
13158
|
const preparation = context.prepare(history, candidate, profile, runId, reason, !frozen);
|
|
@@ -12657,6 +13201,7 @@ var AgentSession = class {
|
|
|
12657
13201
|
let persistedHistoryLength = this.history.length;
|
|
12658
13202
|
let loopCompleted = false;
|
|
12659
13203
|
let loopFailed = false;
|
|
13204
|
+
let consumerInterrupted = false;
|
|
12660
13205
|
try {
|
|
12661
13206
|
for await (const event of loop.run(this.history)) {
|
|
12662
13207
|
await this.logContextEvent(event, runId);
|
|
@@ -12731,7 +13276,12 @@ var AgentSession = class {
|
|
|
12731
13276
|
if (event.type === "message_stop" || event.type === "tool_messages") {
|
|
12732
13277
|
persistedHistoryLength = this.history.length;
|
|
12733
13278
|
}
|
|
12734
|
-
|
|
13279
|
+
try {
|
|
13280
|
+
yield event;
|
|
13281
|
+
} catch (error) {
|
|
13282
|
+
consumerInterrupted = true;
|
|
13283
|
+
throw error;
|
|
13284
|
+
}
|
|
12735
13285
|
}
|
|
12736
13286
|
loopCompleted = true;
|
|
12737
13287
|
} catch (error) {
|
|
@@ -12748,7 +13298,7 @@ var AgentSession = class {
|
|
|
12748
13298
|
}
|
|
12749
13299
|
throw error;
|
|
12750
13300
|
} finally {
|
|
12751
|
-
if (!loopCompleted && !loopFailed) {
|
|
13301
|
+
if (!loopCompleted && (!loopFailed || consumerInterrupted)) {
|
|
12752
13302
|
await executor.rollback();
|
|
12753
13303
|
if (this.store) {
|
|
12754
13304
|
for (const message of this.history.slice(persistedHistoryLength)) {
|
|
@@ -12880,102 +13430,44 @@ var AgentSession = class {
|
|
|
12880
13430
|
}
|
|
12881
13431
|
/** 完整消息提交后完成技能审计;审计失败保留真实历史,停止本轮且不发布成功事件。 */
|
|
12882
13432
|
async commitSkills(runId) {
|
|
12883
|
-
|
|
12884
|
-
|
|
12885
|
-
|
|
12886
|
-
|
|
12887
|
-
|
|
12888
|
-
|
|
12889
|
-
|
|
12890
|
-
sessionId: this.sessionId,
|
|
12891
|
-
runId,
|
|
12892
|
-
fields: {
|
|
12893
|
-
skillName: source.name,
|
|
12894
|
-
skillScope: source.scope,
|
|
12895
|
-
contentDigest: source.contentDigest,
|
|
12896
|
-
renderedDigest: source.renderedDigest,
|
|
12897
|
-
pathDigest: skillDigest(source.filePath),
|
|
12898
|
-
caller: source.caller,
|
|
12899
|
-
invocationId: source.invocationId,
|
|
12900
|
-
messageId
|
|
12901
|
-
}
|
|
12902
|
-
});
|
|
12903
|
-
} catch {
|
|
12904
|
-
throw new KapibalaError("Skill commit audit unavailable", {
|
|
12905
|
-
code: "AUDIT_UNAVAILABLE",
|
|
12906
|
-
retryPolicy: "after_user_action",
|
|
12907
|
-
safeMessage: "\u6280\u80FD\u63D0\u4EA4\u5BA1\u8BA1\u4E0D\u53EF\u7528\uFF0C\u672C\u8F6E\u505C\u6B62"
|
|
12908
|
-
});
|
|
12909
|
-
}
|
|
12910
|
-
},
|
|
12911
|
-
this.store ? "disk" : "memory"
|
|
12912
|
-
);
|
|
13433
|
+
return commitSkillEvents({
|
|
13434
|
+
skillRuntime: this.skillRuntime,
|
|
13435
|
+
eventLogger: this.eventLogger,
|
|
13436
|
+
sessionId: this.sessionId,
|
|
13437
|
+
runId,
|
|
13438
|
+
persistence: this.store ? "disk" : "memory"
|
|
13439
|
+
});
|
|
12913
13440
|
}
|
|
12914
13441
|
/** 显式加载仍按同一读取策略、精确缓存与宿主审批裁决;拒绝/取消/审计失败时不追加用户任务。 */
|
|
12915
|
-
|
|
12916
|
-
|
|
12917
|
-
|
|
12918
|
-
|
|
12919
|
-
|
|
13442
|
+
authorizeSkill(name, runId, signal) {
|
|
13443
|
+
return authorizeExplicitSkill({
|
|
13444
|
+
name,
|
|
13445
|
+
runId,
|
|
13446
|
+
signal,
|
|
12920
13447
|
rootDir: this.rootDir,
|
|
12921
|
-
sessionAllowed: true
|
|
12922
|
-
};
|
|
12923
|
-
const cached = this.approvalCache.get(request);
|
|
12924
|
-
const decision = new PermissionPolicy().decide({
|
|
12925
|
-
toolName: request.toolName,
|
|
12926
|
-
capabilities: request.capabilities,
|
|
12927
13448
|
mode: this.mode,
|
|
12928
|
-
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
let approvalChoice;
|
|
12934
|
-
if (decision.decision === "ask") {
|
|
12935
|
-
try {
|
|
12936
|
-
approvalChoice = await this.approvalChannel?.requestApproval(request, signal) ?? "deny_once";
|
|
12937
|
-
} catch {
|
|
12938
|
-
approvalChoice = "cancel";
|
|
12939
|
-
}
|
|
12940
|
-
permitted = approvalChoice === "allow_once" || approvalChoice === "allow_session";
|
|
12941
|
-
if (approvalChoice === "allow_session") this.approvalCache.set(request, "allow");
|
|
12942
|
-
if (approvalChoice === "deny_session") this.approvalCache.set(request, "deny");
|
|
12943
|
-
}
|
|
12944
|
-
await this.eventLogger.recordAudit({
|
|
12945
|
-
level: "info",
|
|
12946
|
-
event: "skill.decided",
|
|
12947
|
-
sessionId: this.sessionId,
|
|
12948
|
-
runId,
|
|
12949
|
-
fields: {
|
|
12950
|
-
skillName: name,
|
|
12951
|
-
caller: "user",
|
|
12952
|
-
decision: permitted && !signal?.aborted ? "allow" : "deny",
|
|
12953
|
-
decisionSource: decision.decision === "ask" ? this.approvalChannel ? "human" : "non_interactive" : decision.source,
|
|
12954
|
-
approvalChoice
|
|
12955
|
-
}
|
|
13449
|
+
permissionRules: this.permissionRules,
|
|
13450
|
+
approvalChannel: this.approvalChannel,
|
|
13451
|
+
approvalCache: this.approvalCache,
|
|
13452
|
+
eventLogger: this.eventLogger,
|
|
13453
|
+
sessionId: this.sessionId
|
|
12956
13454
|
});
|
|
12957
|
-
if (!permitted) throw skillFailure("SKILL_INVOCATION_FORBIDDEN", "\u5BBF\u4E3B\u6743\u9650\u7981\u6B62\u663E\u5F0F\u6280\u80FD\u8BFB\u53D6");
|
|
12958
|
-
signal?.throwIfAborted();
|
|
12959
13455
|
}
|
|
12960
13456
|
/**
|
|
12961
13457
|
* 计算最近一次内部模型请求的上下文占用;无 usage 时显式标注预算估算。
|
|
12962
13458
|
* run 累计 promptTokens 会重复计算每一步都重发的历史,不能用于窗口占用率。
|
|
12963
13459
|
*/
|
|
12964
13460
|
createContextUsage(usedTokens, estimatedUsage = false) {
|
|
12965
|
-
|
|
12966
|
-
|
|
12967
|
-
);
|
|
12968
|
-
return {
|
|
13461
|
+
return computeContextUsage(
|
|
13462
|
+
this.lastPrimaryProfile ?? this.getActiveProfile(),
|
|
12969
13463
|
usedTokens,
|
|
12970
|
-
|
|
12971
|
-
|
|
12972
|
-
estimatedLimit: contextWindow.estimated,
|
|
12973
|
-
...estimatedUsage ? { estimatedUsage: true } : {}
|
|
12974
|
-
};
|
|
13464
|
+
estimatedUsage
|
|
13465
|
+
);
|
|
12975
13466
|
}
|
|
12976
13467
|
async persistAssistant(event) {
|
|
12977
|
-
|
|
12978
|
-
|
|
13468
|
+
await persistAssistantMessage(event, this.store, () => {
|
|
13469
|
+
this.usageKnown = false;
|
|
13470
|
+
});
|
|
12979
13471
|
}
|
|
12980
13472
|
/** 创建上下文协调器并注入当前路由;摘要不经过主任务 Loop,插件结果不默认为可剪裁。 */
|
|
12981
13473
|
createContextManager() {
|
|
@@ -12994,17 +13486,8 @@ var AgentSession = class {
|
|
|
12994
13486
|
}
|
|
12995
13487
|
/** 摘要实际消耗按 attempt 身份独立累加;缺失消耗保留未知,不计入主任务指标。 */
|
|
12996
13488
|
accountSummaryUsage(attemptId, usage) {
|
|
12997
|
-
|
|
12998
|
-
this.
|
|
12999
|
-
if (!usage || ![usage.promptTokens, usage.completionTokens, usage.totalTokens].every(
|
|
13000
|
-
(n) => Number.isSafeInteger(n) && n >= 0
|
|
13001
|
-
)) {
|
|
13002
|
-
this.summaryUsageKnown = false;
|
|
13003
|
-
return;
|
|
13004
|
-
}
|
|
13005
|
-
this.summaryUsage.promptTokens += usage.promptTokens;
|
|
13006
|
-
this.summaryUsage.completionTokens += usage.completionTokens;
|
|
13007
|
-
this.summaryUsage.totalTokens += usage.totalTokens;
|
|
13489
|
+
const outcome = recordSummaryUsage(attemptId, usage, this.summaryAttempts, this.summaryUsage);
|
|
13490
|
+
if (outcome === "unknown") this.summaryUsageKnown = false;
|
|
13008
13491
|
}
|
|
13009
13492
|
/**
|
|
13010
13493
|
* 空闲时手动摘要旧完整前缀,不创建用户任务、不执行工具,不突破当前与最近成功交互。
|
|
@@ -13077,61 +13560,21 @@ var AgentSession = class {
|
|
|
13077
13560
|
}
|
|
13078
13561
|
/** 以模型 attempt 身份幂等累加真实消耗;非法或缺失 usage 保持未知。 */
|
|
13079
13562
|
accountUsage(attemptId, usage) {
|
|
13080
|
-
|
|
13081
|
-
if (
|
|
13082
|
-
(n) => Number.isSafeInteger(n) && n >= 0
|
|
13083
|
-
)) {
|
|
13084
|
-
this.usageKnown = false;
|
|
13085
|
-
return;
|
|
13086
|
-
}
|
|
13087
|
-
this.accountedAttempts.add(attemptId);
|
|
13088
|
-
this.usageStats.promptTokens += usage.promptTokens;
|
|
13089
|
-
this.usageStats.completionTokens += usage.completionTokens;
|
|
13090
|
-
this.usageStats.totalTokens += usage.totalTokens;
|
|
13563
|
+
const outcome = recordUsage(attemptId, usage, this.accountedAttempts, this.usageStats);
|
|
13564
|
+
if (outcome === "unknown") this.usageKnown = false;
|
|
13091
13565
|
}
|
|
13092
13566
|
async persistToolMessages(messages) {
|
|
13093
|
-
|
|
13094
|
-
for (const message of messages) await this.store.append(message);
|
|
13567
|
+
await persistToolMessages(messages, this.store);
|
|
13095
13568
|
}
|
|
13096
13569
|
/** 中止时为缓冲 assistant 的每个 tool_use 生成 OUTCOME_UNKNOWN 结果并入历史,保持事务闭合。 */
|
|
13097
13570
|
closeInterruptedToolCalls(assistantEvent, error) {
|
|
13098
|
-
const message =
|
|
13099
|
-
id: createMessageId(),
|
|
13100
|
-
role: "tool",
|
|
13101
|
-
content: assistantEvent.message.content.filter((block) => block.type === "tool_use").map((toolUse) => ({
|
|
13102
|
-
type: "tool_result",
|
|
13103
|
-
toolUseId: toolUse.id,
|
|
13104
|
-
content: JSON.stringify({
|
|
13105
|
-
code: "OUTCOME_UNKNOWN",
|
|
13106
|
-
retryPolicy: "after_user_action",
|
|
13107
|
-
message: "Tool execution was interrupted; outcome is unknown"
|
|
13108
|
-
}),
|
|
13109
|
-
isError: true,
|
|
13110
|
-
errorCode: "OUTCOME_UNKNOWN",
|
|
13111
|
-
retryPolicy: "after_user_action"
|
|
13112
|
-
})),
|
|
13113
|
-
timestamp: Date.now()
|
|
13114
|
-
};
|
|
13571
|
+
const message = buildInterruptedToolMessage(assistantEvent);
|
|
13115
13572
|
this.history.push(message);
|
|
13116
13573
|
return [message];
|
|
13117
13574
|
}
|
|
13118
13575
|
/** 收集历史中紧跟该 assistant 的连续 tool 消息;仅当覆盖全部 tool_use 时视为已闭合。 */
|
|
13119
13576
|
findClosedToolMessages(assistantEvent) {
|
|
13120
|
-
|
|
13121
|
-
if (assistantIndex < 0) return [];
|
|
13122
|
-
const expected = new Set(
|
|
13123
|
-
assistantEvent.message.content.filter((block) => block.type === "tool_use").map((toolUse) => toolUse.id)
|
|
13124
|
-
);
|
|
13125
|
-
const messages = [];
|
|
13126
|
-
for (let index2 = assistantIndex + 1; index2 < this.history.length; index2++) {
|
|
13127
|
-
const message = this.history[index2];
|
|
13128
|
-
if (message.role !== "tool") break;
|
|
13129
|
-
messages.push(message);
|
|
13130
|
-
for (const block of message.content) {
|
|
13131
|
-
if (block.type === "tool_result") expected.delete(block.toolUseId);
|
|
13132
|
-
}
|
|
13133
|
-
}
|
|
13134
|
-
return expected.size === 0 ? messages : [];
|
|
13577
|
+
return findClosedToolMessages(this.history, assistantEvent);
|
|
13135
13578
|
}
|
|
13136
13579
|
};
|
|
13137
13580
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -13209,6 +13652,7 @@ var AgentSession = class {
|
|
|
13209
13652
|
createLogId,
|
|
13210
13653
|
createMessageId,
|
|
13211
13654
|
createRunCommandTool,
|
|
13655
|
+
createSessionError,
|
|
13212
13656
|
createSpawnAgentTool,
|
|
13213
13657
|
createWaitAgentTool,
|
|
13214
13658
|
defineTool,
|
|
@@ -13264,7 +13708,6 @@ var AgentSession = class {
|
|
|
13264
13708
|
sanitizeModelErrorMessage,
|
|
13265
13709
|
sanitizeSessionTitle,
|
|
13266
13710
|
scanSkillRoot,
|
|
13267
|
-
sharedWorkspaceLeaseCoordinator,
|
|
13268
13711
|
skillDigest,
|
|
13269
13712
|
skillFailure,
|
|
13270
13713
|
skillPathKey,
|