@kiturone/kapibala 0.0.10 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1217 -827
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -22
- package/dist/index.d.ts +82 -22
- package/dist/index.js +1217 -827
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -532,15 +532,50 @@ import path from "path";
|
|
|
532
532
|
|
|
533
533
|
// src/context/jsonl-lines.ts
|
|
534
534
|
import fs from "fs";
|
|
535
|
-
function
|
|
535
|
+
function assertHistoryReadBudget(bytes, options) {
|
|
536
|
+
const limit = options.maxReadBytes;
|
|
537
|
+
if (limit === void 0) return;
|
|
538
|
+
if (!Number.isSafeInteger(limit) || limit <= 0)
|
|
539
|
+
throw new KapibalaError("Invalid history read budget", { code: "CONFIG_INVALID" });
|
|
540
|
+
if (bytes > limit)
|
|
541
|
+
throw new KapibalaError("History exceeds the configured read budget", {
|
|
542
|
+
code: "HISTORY_READ_LIMIT",
|
|
543
|
+
retryPolicy: "after_user_action",
|
|
544
|
+
safeMessage: "\u5386\u53F2\u6587\u4EF6\u8D85\u8FC7\u8BFB\u53D6\u9884\u7B97\uFF0C\u539F\u6587\u4EF6\u4FDD\u6301\u4E0D\u53D8"
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
function readHistoryBytes(file, options = {}) {
|
|
548
|
+
const fd = fs.openSync(file, "r");
|
|
549
|
+
try {
|
|
550
|
+
assertHistoryReadBudget(fs.fstatSync(fd).size, options);
|
|
551
|
+
const chunks = [];
|
|
552
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
553
|
+
let total = 0;
|
|
554
|
+
while (true) {
|
|
555
|
+
const length = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
556
|
+
if (!length) break;
|
|
557
|
+
total += length;
|
|
558
|
+
assertHistoryReadBudget(total, options);
|
|
559
|
+
chunks.push(Buffer.from(buffer.subarray(0, length)));
|
|
560
|
+
}
|
|
561
|
+
return Buffer.concat(chunks, total);
|
|
562
|
+
} finally {
|
|
563
|
+
fs.closeSync(fd);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function* readJsonlLines(file, options = {}) {
|
|
536
567
|
const fd = fs.openSync(file, "r");
|
|
537
568
|
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
538
569
|
let pending = Buffer.alloc(0);
|
|
539
570
|
let offset = 0;
|
|
571
|
+
let total = 0;
|
|
540
572
|
try {
|
|
573
|
+
assertHistoryReadBudget(fs.fstatSync(fd).size, options);
|
|
541
574
|
while (true) {
|
|
542
575
|
const length = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
543
576
|
if (!length) break;
|
|
577
|
+
total += length;
|
|
578
|
+
assertHistoryReadBudget(total, options);
|
|
544
579
|
pending = Buffer.concat([pending, buffer.subarray(0, length)]);
|
|
545
580
|
let start = 0;
|
|
546
581
|
let end = pending.indexOf(10, start);
|
|
@@ -574,7 +609,7 @@ var SESSION_SCHEMA_VERSION_V2 = 2;
|
|
|
574
609
|
var SESSION_SCHEMA_VERSION = SESSION_SCHEMA_VERSION_V2;
|
|
575
610
|
var MAIN_BRANCH_ID = "main";
|
|
576
611
|
function isGlobalControlRecordType(type) {
|
|
577
|
-
return type === "header" || type === "branch_created" || type === "branch_selected" || type === "title" || type === "archive_state" || type === "usage" || type === "legacy_import";
|
|
612
|
+
return type === "header" || type === "branch_created" || type === "branch_selected" || type === "title" || type === "archive_state" || type === "pin_state" || type === "usage" || type === "legacy_import";
|
|
578
613
|
}
|
|
579
614
|
var types = /* @__PURE__ */ new Set([
|
|
580
615
|
"header",
|
|
@@ -593,7 +628,8 @@ var types = /* @__PURE__ */ new Set([
|
|
|
593
628
|
"branch_created",
|
|
594
629
|
"branch_selected",
|
|
595
630
|
"branch_summary",
|
|
596
|
-
"archive_state"
|
|
631
|
+
"archive_state",
|
|
632
|
+
"pin_state"
|
|
597
633
|
]);
|
|
598
634
|
function supportsSessionState(store) {
|
|
599
635
|
return "appendRecord" in store && typeof store.appendRecord === "function" && "loadState" in store && typeof store.loadState === "function";
|
|
@@ -676,6 +712,13 @@ function assertClosedToolTransaction(records, targetRecordId) {
|
|
|
676
712
|
}
|
|
677
713
|
}
|
|
678
714
|
var SessionStore = class _SessionStore {
|
|
715
|
+
constructor(filePath, schemaVersion = SESSION_SCHEMA_VERSION_V2, readOptions = {}) {
|
|
716
|
+
this.readOptions = readOptions;
|
|
717
|
+
assertHistoryReadBudget(0, readOptions);
|
|
718
|
+
this.filePath = path.resolve(filePath);
|
|
719
|
+
this.schemaVersion = schemaVersion;
|
|
720
|
+
}
|
|
721
|
+
readOptions;
|
|
679
722
|
filePath;
|
|
680
723
|
queue = Promise.resolve();
|
|
681
724
|
records = [];
|
|
@@ -685,10 +728,6 @@ var SessionStore = class _SessionStore {
|
|
|
685
728
|
schemaVersion = SESSION_SCHEMA_VERSION_V2;
|
|
686
729
|
activeBranchId = "main";
|
|
687
730
|
branchLeaves = /* @__PURE__ */ new Map();
|
|
688
|
-
constructor(filePath, schemaVersion = SESSION_SCHEMA_VERSION_V2) {
|
|
689
|
-
this.filePath = path.resolve(filePath);
|
|
690
|
-
this.schemaVersion = schemaVersion;
|
|
691
|
-
}
|
|
692
731
|
/** 获取当前会话文件使用的 Schema 版本(1 或 2)。 */
|
|
693
732
|
getSchemaVersion() {
|
|
694
733
|
return this.schemaVersion;
|
|
@@ -711,8 +750,8 @@ var SessionStore = class _SessionStore {
|
|
|
711
750
|
this.onDiagnostic = onDiagnostic;
|
|
712
751
|
}
|
|
713
752
|
/** 以独占创建方式安装新头部(默认生成 Schema v2 格式),已有文件明确失败,绝不覆盖其它会话。 */
|
|
714
|
-
static async create(filePath, header) {
|
|
715
|
-
const store = new _SessionStore(filePath, SESSION_SCHEMA_VERSION_V2);
|
|
753
|
+
static async create(filePath, header, options = {}) {
|
|
754
|
+
const store = new _SessionStore(filePath, SESSION_SCHEMA_VERSION_V2, { ...options });
|
|
716
755
|
fs2.mkdirSync(path.dirname(store.filePath), { recursive: true });
|
|
717
756
|
const record = store.makeRecord("header", {
|
|
718
757
|
...header,
|
|
@@ -733,8 +772,8 @@ var SessionStore = class _SessionStore {
|
|
|
733
772
|
return store;
|
|
734
773
|
}
|
|
735
774
|
/** 打开并验证已有会话;尾部残片先保存到隔离文件,中部损坏与未知版本拒绝恢复。 */
|
|
736
|
-
static async open(filePath) {
|
|
737
|
-
const store = new _SessionStore(filePath);
|
|
775
|
+
static async open(filePath, options = {}) {
|
|
776
|
+
const store = new _SessionStore(filePath, SESSION_SCHEMA_VERSION_V2, { ...options });
|
|
738
777
|
store.records = store.readFile(true);
|
|
739
778
|
store.refreshStamp();
|
|
740
779
|
return store;
|
|
@@ -828,6 +867,24 @@ var SessionStore = class _SessionStore {
|
|
|
828
867
|
}
|
|
829
868
|
return false;
|
|
830
869
|
}
|
|
870
|
+
/**
|
|
871
|
+
* 追加会话置顶控制记录。
|
|
872
|
+
*
|
|
873
|
+
* 约束:
|
|
874
|
+
* - 置顶状态记录为全局控制记录(parentId 为 null,不推进分支上下文叶子);
|
|
875
|
+
* - payload 结构为 { pinned: boolean },最后一次写入生效;
|
|
876
|
+
* - 置顶只影响列表排序,不改变正文活动时间,因此不会让会话挤进「最近」;
|
|
877
|
+
* - v1 历史会话禁止变更置顶状态。
|
|
878
|
+
*/
|
|
879
|
+
async setPinnedState(pinned) {
|
|
880
|
+
if (this.isReadOnly()) {
|
|
881
|
+
throw new KapibalaError("Cannot modify pin state of legacy v1 session", {
|
|
882
|
+
code: "LEGACY_SESSION_READ_ONLY",
|
|
883
|
+
safeMessage: "\u65E0\u6CD5\u53D8\u66F4\u53EA\u8BFB\u5386\u53F2\u4F1A\u8BDD\u7684\u7F6E\u9876\u72B6\u6001"
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
return this.appendRecord("pin_state", { pinned });
|
|
887
|
+
}
|
|
831
888
|
/**
|
|
832
889
|
* 获取当前会话中所有已知分支的元数据与状态列表。
|
|
833
890
|
*/
|
|
@@ -1119,14 +1176,15 @@ ${summaryText}` }]
|
|
|
1119
1176
|
}
|
|
1120
1177
|
/** 校验每个完整记录,只有最后非空行的 JSON 残片可隔离,语义损坏绝不作为残片吞掉。 */
|
|
1121
1178
|
readFile(recover) {
|
|
1122
|
-
const iterator = readJsonlLines(this.filePath);
|
|
1179
|
+
const iterator = readJsonlLines(this.filePath, this.readOptions);
|
|
1123
1180
|
let lastTerminated = true;
|
|
1124
1181
|
let blankSeen = false;
|
|
1125
1182
|
const records = [];
|
|
1126
1183
|
const identities = /* @__PURE__ */ new Set();
|
|
1127
1184
|
const messages = /* @__PURE__ */ new Set();
|
|
1128
|
-
|
|
1129
|
-
|
|
1185
|
+
const branchLeaves = /* @__PURE__ */ new Map();
|
|
1186
|
+
let activeBranchId = MAIN_BRANCH_ID;
|
|
1187
|
+
let schemaVersion = this.schemaVersion;
|
|
1130
1188
|
let detectedSchemaVersion = null;
|
|
1131
1189
|
let i = -1;
|
|
1132
1190
|
for (const line of iterator) {
|
|
@@ -1141,7 +1199,7 @@ ${summaryText}` }]
|
|
|
1141
1199
|
try {
|
|
1142
1200
|
record = JSON.parse(line.text);
|
|
1143
1201
|
} catch {
|
|
1144
|
-
if (!recover ||
|
|
1202
|
+
if (!recover || schemaVersion === SESSION_SCHEMA_VERSION_V1 || !records.length || Array.from(iterator).some((next) => next.text.trim()))
|
|
1145
1203
|
throw new Error(`Session corrupt at line ${i + 1}`);
|
|
1146
1204
|
fs2.writeFileSync(
|
|
1147
1205
|
`${this.filePath}.corrupt-${createMessageId()}`,
|
|
@@ -1165,7 +1223,7 @@ ${summaryText}` }]
|
|
|
1165
1223
|
throw new Error("Unsupported session schema version");
|
|
1166
1224
|
}
|
|
1167
1225
|
detectedSchemaVersion = record.schemaVersion;
|
|
1168
|
-
|
|
1226
|
+
schemaVersion = detectedSchemaVersion;
|
|
1169
1227
|
} else if (record.schemaVersion !== detectedSchemaVersion) {
|
|
1170
1228
|
throw new Error(`Session corrupt schema version mismatch at line ${i + 1}`);
|
|
1171
1229
|
}
|
|
@@ -1182,11 +1240,11 @@ ${summaryText}` }]
|
|
|
1182
1240
|
} else if (record.type === "header") {
|
|
1183
1241
|
throw new Error("Session corrupt: repeated header");
|
|
1184
1242
|
}
|
|
1185
|
-
if (
|
|
1243
|
+
if (schemaVersion === SESSION_SCHEMA_VERSION_V1) {
|
|
1186
1244
|
if (record.parentId !== (records.at(-1)?.recordId ?? null)) {
|
|
1187
1245
|
throw new Error(`Session corrupt parent at line ${i + 1}`);
|
|
1188
1246
|
}
|
|
1189
|
-
|
|
1247
|
+
branchLeaves.set(MAIN_BRANCH_ID, record.recordId);
|
|
1190
1248
|
} else {
|
|
1191
1249
|
if (typeof record.seq !== "number" || record.seq !== records.length) {
|
|
1192
1250
|
throw new Error(`Session corrupt seq at line ${i + 1}`);
|
|
@@ -1200,37 +1258,37 @@ ${summaryText}` }]
|
|
|
1200
1258
|
throw new Error(`Session corrupt control record parent at line ${i + 1}`);
|
|
1201
1259
|
}
|
|
1202
1260
|
if (record.type === "header") {
|
|
1203
|
-
|
|
1204
|
-
|
|
1261
|
+
branchLeaves.set(MAIN_BRANCH_ID, record.recordId);
|
|
1262
|
+
activeBranchId = MAIN_BRANCH_ID;
|
|
1205
1263
|
} else if (record.type === "branch_created") {
|
|
1206
1264
|
const bId = record.payload.branchId;
|
|
1207
1265
|
const baseId = record.payload.baseRecordId;
|
|
1208
1266
|
if (typeof bId !== "string" || !bId) {
|
|
1209
1267
|
throw new Error(`Session corrupt branch_created branchId at line ${i + 1}`);
|
|
1210
1268
|
}
|
|
1211
|
-
if (
|
|
1269
|
+
if (branchLeaves.has(bId)) {
|
|
1212
1270
|
throw new Error(`Duplicate branch ID: ${bId}`);
|
|
1213
1271
|
}
|
|
1214
1272
|
if (typeof baseId !== "string" || !identities.has(baseId)) {
|
|
1215
1273
|
throw new Error(`Session corrupt branch_created baseRecordId at line ${i + 1}`);
|
|
1216
1274
|
}
|
|
1217
|
-
|
|
1275
|
+
branchLeaves.set(bId, baseId);
|
|
1218
1276
|
} else if (record.type === "branch_selected") {
|
|
1219
1277
|
const bId = record.payload.branchId;
|
|
1220
|
-
if (typeof bId !== "string" || !
|
|
1278
|
+
if (typeof bId !== "string" || !branchLeaves.has(bId)) {
|
|
1221
1279
|
throw new Error(`Selected branch not found: ${bId}`);
|
|
1222
1280
|
}
|
|
1223
|
-
|
|
1281
|
+
activeBranchId = bId;
|
|
1224
1282
|
}
|
|
1225
1283
|
} else {
|
|
1226
1284
|
const bId = record.branchId;
|
|
1227
|
-
if (typeof bId !== "string" || !
|
|
1285
|
+
if (typeof bId !== "string" || !branchLeaves.has(bId)) {
|
|
1228
1286
|
throw new Error(`Session corrupt branchId at line ${i + 1}`);
|
|
1229
1287
|
}
|
|
1230
1288
|
if (typeof record.parentId !== "string" || !identities.has(record.parentId)) {
|
|
1231
1289
|
throw new Error(`Session corrupt parent at line ${i + 1}`);
|
|
1232
1290
|
}
|
|
1233
|
-
|
|
1291
|
+
branchLeaves.set(bId, record.recordId);
|
|
1234
1292
|
}
|
|
1235
1293
|
}
|
|
1236
1294
|
identities.add(record.recordId);
|
|
@@ -1249,7 +1307,7 @@ ${summaryText}` }]
|
|
|
1249
1307
|
if (!records.length) {
|
|
1250
1308
|
throw new Error("Session corrupt header");
|
|
1251
1309
|
}
|
|
1252
|
-
if (recover && !lastTerminated && records.length === i + 1 &&
|
|
1310
|
+
if (recover && !lastTerminated && records.length === i + 1 && schemaVersion !== SESSION_SCHEMA_VERSION_V1) {
|
|
1253
1311
|
const fd = fs2.openSync(this.filePath, "a");
|
|
1254
1312
|
try {
|
|
1255
1313
|
fs2.writeFileSync(fd, "\n");
|
|
@@ -1258,6 +1316,9 @@ ${summaryText}` }]
|
|
|
1258
1316
|
fs2.closeSync(fd);
|
|
1259
1317
|
}
|
|
1260
1318
|
}
|
|
1319
|
+
this.branchLeaves = branchLeaves;
|
|
1320
|
+
this.activeBranchId = activeBranchId;
|
|
1321
|
+
this.schemaVersion = schemaVersion;
|
|
1261
1322
|
return records;
|
|
1262
1323
|
}
|
|
1263
1324
|
};
|
|
@@ -1545,18 +1606,21 @@ function resolveWorkspaceContext(input) {
|
|
|
1545
1606
|
function normalizedProjectRoot(root) {
|
|
1546
1607
|
return process.platform === "win32" ? root.replaceAll("\\", "/").toLowerCase() : root;
|
|
1547
1608
|
}
|
|
1548
|
-
function resolveSessionProject(cwd) {
|
|
1609
|
+
function resolveSessionProject(cwd, mode = "git") {
|
|
1549
1610
|
const realCwd = toNativePhysicalPath(cwd);
|
|
1550
1611
|
let root = realCwd;
|
|
1551
1612
|
let gitBranch;
|
|
1552
1613
|
let gitWorktree = false;
|
|
1553
1614
|
try {
|
|
1554
|
-
|
|
1615
|
+
const gitRoot = toNativePhysicalPath(
|
|
1555
1616
|
execFileSync("git", ["-C", realCwd, "rev-parse", "--show-toplevel"], {
|
|
1556
1617
|
encoding: "utf8",
|
|
1557
1618
|
stdio: ["ignore", "pipe", "ignore"]
|
|
1558
1619
|
}).trim()
|
|
1559
1620
|
);
|
|
1621
|
+
if (mode === "git") root = gitRoot;
|
|
1622
|
+
if (normalizedProjectRoot(gitRoot) !== normalizedProjectRoot(root))
|
|
1623
|
+
return { projectRoot: root, cwd: realCwd, projectKey: computeProjectKey(root) };
|
|
1560
1624
|
gitWorktree = true;
|
|
1561
1625
|
gitBranch = execFileSync("git", ["-C", realCwd, "symbolic-ref", "--short", "-q", "HEAD"], {
|
|
1562
1626
|
encoding: "utf8",
|
|
@@ -1564,11 +1628,10 @@ function resolveSessionProject(cwd) {
|
|
|
1564
1628
|
}).trim() || void 0;
|
|
1565
1629
|
} catch {
|
|
1566
1630
|
}
|
|
1567
|
-
const normalized = normalizedProjectRoot(root);
|
|
1568
1631
|
return {
|
|
1569
1632
|
projectRoot: root,
|
|
1570
1633
|
cwd: realCwd,
|
|
1571
|
-
projectKey:
|
|
1634
|
+
projectKey: computeProjectKey(root),
|
|
1572
1635
|
gitBranch,
|
|
1573
1636
|
gitWorktree
|
|
1574
1637
|
};
|
|
@@ -1586,7 +1649,10 @@ var SessionManager = class {
|
|
|
1586
1649
|
directory;
|
|
1587
1650
|
handles = /* @__PURE__ */ new Map();
|
|
1588
1651
|
diagnostic;
|
|
1652
|
+
readOptions;
|
|
1589
1653
|
constructor(options = {}) {
|
|
1654
|
+
this.readOptions = { maxReadBytes: options.maxReadBytes };
|
|
1655
|
+
assertHistoryReadBudget(0, this.readOptions);
|
|
1590
1656
|
const rootCandidate = options.workspaceRoot ?? options.projectRoot ?? options.cwd;
|
|
1591
1657
|
if (!rootCandidate) {
|
|
1592
1658
|
throw new KapibalaError("Workspace root is required for SessionManager", {
|
|
@@ -1594,7 +1660,7 @@ var SessionManager = class {
|
|
|
1594
1660
|
safeMessage: "\u5FC5\u987B\u663E\u5F0F\u6307\u5B9A\u5DE5\u4F5C\u533A\u6839\u8DEF\u5F84\u6216\u9879\u76EE\u76EE\u5F55"
|
|
1595
1661
|
});
|
|
1596
1662
|
}
|
|
1597
|
-
this.project = resolveSessionProject(rootCandidate);
|
|
1663
|
+
this.project = resolveSessionProject(rootCandidate, options.projectRootMode);
|
|
1598
1664
|
this.directory = path4.join(
|
|
1599
1665
|
options.homeDirectory ?? os.homedir(),
|
|
1600
1666
|
".kapibala",
|
|
@@ -1610,12 +1676,16 @@ var SessionManager = class {
|
|
|
1610
1676
|
const file = path4.join(this.directory, `${Date.now()}_${conversationId}.jsonl`);
|
|
1611
1677
|
const lock = await SessionLock.acquire(this.lockPath(conversationId));
|
|
1612
1678
|
try {
|
|
1613
|
-
const store = await SessionStore.create(
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1679
|
+
const store = await SessionStore.create(
|
|
1680
|
+
file,
|
|
1681
|
+
{
|
|
1682
|
+
conversationId,
|
|
1683
|
+
projectRoot: this.project.projectRoot,
|
|
1684
|
+
initialCwd: this.project.cwd,
|
|
1685
|
+
gitBranch: this.project.gitBranch
|
|
1686
|
+
},
|
|
1687
|
+
this.readOptions
|
|
1688
|
+
);
|
|
1619
1689
|
return await this.attach(store, lock);
|
|
1620
1690
|
} catch (error) {
|
|
1621
1691
|
await lock.release();
|
|
@@ -1624,19 +1694,25 @@ var SessionManager = class {
|
|
|
1624
1694
|
}
|
|
1625
1695
|
/**
|
|
1626
1696
|
* 按正文活动排序分页;有效 sidecar 只读元数据,不读取完整 JSONL。默认排除已归档会话。
|
|
1627
|
-
* pageSize 由调用方显式给出时必须在 1–100
|
|
1697
|
+
* pageSize 由调用方显式给出时必须在 1–100 之间;filter 在分页前筛选元数据,调用方须自行处理敏感信息。
|
|
1698
|
+
* 置顶项整体前置、组内仍按活动时间排序;排序只作用于本方法的返回值,不影响 continueRecent。
|
|
1628
1699
|
*/
|
|
1629
1700
|
async list(page = 1, options) {
|
|
1630
1701
|
const pageSize = options?.pageSize ?? 20;
|
|
1631
1702
|
if (!Number.isSafeInteger(page) || page < 1 || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100)
|
|
1632
1703
|
throw createSessionError("HISTORY_PAGE_INVALID", "History page must be a positive integer");
|
|
1633
1704
|
const all = await this.allMetadata();
|
|
1634
|
-
const
|
|
1705
|
+
const visible = options?.includeArchived ? all : all.filter((m) => !m.archived);
|
|
1706
|
+
const filtered = options?.filter ? visible.filter(options.filter) : visible;
|
|
1707
|
+
const ordered = [
|
|
1708
|
+
...filtered.filter((item) => item.pinned),
|
|
1709
|
+
...filtered.filter((item) => !item.pinned)
|
|
1710
|
+
];
|
|
1635
1711
|
return {
|
|
1636
|
-
items:
|
|
1637
|
-
total:
|
|
1712
|
+
items: ordered.slice((page - 1) * pageSize, page * pageSize),
|
|
1713
|
+
total: ordered.length,
|
|
1638
1714
|
page,
|
|
1639
|
-
pages: Math.max(1, Math.ceil(
|
|
1715
|
+
pages: Math.max(1, Math.ceil(ordered.length / pageSize))
|
|
1640
1716
|
};
|
|
1641
1717
|
}
|
|
1642
1718
|
/** 解析完整 ID 或唯一前缀对应的会话文件与 ID。 */
|
|
@@ -1655,60 +1731,109 @@ var SessionManager = class {
|
|
|
1655
1731
|
return matches[0];
|
|
1656
1732
|
}
|
|
1657
1733
|
/**
|
|
1658
|
-
*
|
|
1734
|
+
* 归档、恢复、重命名与置顶都向正文追加一条全局控制记录,共用同一套前置约束。
|
|
1659
1735
|
*
|
|
1660
1736
|
* 约束:
|
|
1661
1737
|
* - 必须取得会话独占锁;
|
|
1662
|
-
* -
|
|
1663
|
-
* -
|
|
1738
|
+
* - 若会话当前正处于活动打开状态,拒绝并抛出 SESSION_IN_USE(调用方先关闭句柄);
|
|
1739
|
+
* - v1 只读历史不能变更,明确以 LEGACY_SESSION_READ_ONLY 失败而不是静默忽略;
|
|
1740
|
+
* - 写入后按正文重建 sidecar,状态只以正文记录为准。
|
|
1664
1741
|
*/
|
|
1665
|
-
async
|
|
1742
|
+
async appendControlRecord(id, subject, append) {
|
|
1666
1743
|
const { fileName, conversationId } = this.resolveSession(id);
|
|
1667
1744
|
if (this.handles.has(conversationId)) {
|
|
1668
|
-
throw new KapibalaError("Session is currently active or in use
|
|
1745
|
+
throw new KapibalaError("Session is currently active or in use", {
|
|
1669
1746
|
code: "SESSION_IN_USE",
|
|
1670
|
-
safeMessage:
|
|
1747
|
+
safeMessage: `\u4F1A\u8BDD\u6B63\u5728\u4F7F\u7528\u4E2D\uFF0C\u65E0\u6CD5${subject}`
|
|
1671
1748
|
});
|
|
1672
1749
|
}
|
|
1673
1750
|
const targetFile = path4.join(this.directory, fileName);
|
|
1674
1751
|
const lock = await SessionLock.acquire(this.lockPath(conversationId));
|
|
1675
1752
|
try {
|
|
1676
|
-
const store = await SessionStore.open(targetFile);
|
|
1753
|
+
const store = await SessionStore.open(targetFile, this.readOptions);
|
|
1677
1754
|
if (store.isReadOnly()) {
|
|
1678
|
-
throw new KapibalaError("Cannot
|
|
1755
|
+
throw new KapibalaError("Cannot modify legacy v1 session state", {
|
|
1679
1756
|
code: "LEGACY_SESSION_READ_ONLY",
|
|
1680
|
-
safeMessage:
|
|
1757
|
+
safeMessage: `\u65E0\u6CD5\u53D8\u66F4\u53EA\u8BFB\u5386\u53F2\u4F1A\u8BDD\u7684${subject}\u72B6\u6001`
|
|
1681
1758
|
});
|
|
1682
1759
|
}
|
|
1683
|
-
await store
|
|
1684
|
-
|
|
1685
|
-
const meta = this.buildMetadata(targetFile, records);
|
|
1686
|
-
meta.archived = true;
|
|
1687
|
-
this.writeMetadata(targetFile, meta);
|
|
1760
|
+
await append(store);
|
|
1761
|
+
this.writeMetadata(targetFile, this.buildMetadata(targetFile, await store.readRecords()));
|
|
1688
1762
|
} finally {
|
|
1689
1763
|
await lock.release();
|
|
1690
1764
|
}
|
|
1691
1765
|
}
|
|
1692
1766
|
/**
|
|
1693
|
-
*
|
|
1767
|
+
* 逻辑归档指定会话:向正文追加 archive_state { archived: true } 并刷新 sidecar 缓存。
|
|
1768
|
+
*/
|
|
1769
|
+
async archiveSession(id) {
|
|
1770
|
+
await this.appendControlRecord(id, "\u5F52\u6863", (store) => store.setArchiveState(true));
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* 恢复已逻辑归档的会话:追加 archive_state { archived: false },正文保持不动。
|
|
1694
1774
|
*/
|
|
1695
1775
|
async restoreSession(id) {
|
|
1776
|
+
await this.appendControlRecord(id, "\u5F52\u6863", (store) => store.setArchiveState(false));
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* 单独重命名指定会话,不保留句柄:追加 title 记录后立即释放写锁。
|
|
1780
|
+
*
|
|
1781
|
+
* 与 `rename` 的分工:本方法面向「没有打开该会话」的宿主(桌面侧栏、设置页),
|
|
1782
|
+
* 因此不接受活动句柄;已打开的会话由宿主经自己的 Store 追加,避免同进程锁冲突。
|
|
1783
|
+
*/
|
|
1784
|
+
async renameSession(id, title) {
|
|
1785
|
+
const value = title.trim();
|
|
1786
|
+
if (!value) throw new Error("Session title cannot be empty");
|
|
1787
|
+
await this.appendControlRecord(
|
|
1788
|
+
id,
|
|
1789
|
+
"\u91CD\u547D\u540D",
|
|
1790
|
+
(store) => store.appendRecord("title", { title: sanitizeSessionTitle(value) })
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* 切换会话置顶:追加 pin_state { pinned } 控制记录,只在列表排序时前置。
|
|
1795
|
+
*
|
|
1796
|
+
* 置顶不是「最近活动」,因此不写 run_started、也不改 lastActivityAt;
|
|
1797
|
+
* 关闭置顶后会话回到按活动时间排序的原有位置。
|
|
1798
|
+
*/
|
|
1799
|
+
async pinSession(id, pinned) {
|
|
1800
|
+
await this.appendControlRecord(id, "\u7F6E\u9876", (store) => store.setPinnedState(pinned));
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* 物理删除指定会话的正文与 sidecar 缓存。
|
|
1804
|
+
*
|
|
1805
|
+
* 约束与边界:
|
|
1806
|
+
* - 删除不可逆,本方法不提供回收站或软删除,调用方必须先取得用户确认;
|
|
1807
|
+
* - 本 Manager 已持有句柄的会话拒绝(SESSION_IN_USE),避免删掉正在追加的文件;
|
|
1808
|
+
* - 其它进程持锁时以 SESSION_LOCKED 失败,不抢占他人写入权,也不做「先等一会儿」的重试;
|
|
1809
|
+
* - 只删该会话自己的 `.jsonl` 与 `.meta.json`;桶目录内其它文件(含 `*.tmp-*` 残片)不动;
|
|
1810
|
+
* - 删除前复核首条头部身份与桶归属:文件名匹配但正文身份不符的残留文件删除会放大错误;
|
|
1811
|
+
* - 缓存先删、正文后删:缓存本可重建,若正文删除失败该会话会在下次浏览时自动重建缓存。
|
|
1812
|
+
*/
|
|
1813
|
+
async deleteSession(id) {
|
|
1696
1814
|
const { fileName, conversationId } = this.resolveSession(id);
|
|
1815
|
+
if (this.handles.has(conversationId)) {
|
|
1816
|
+
throw new KapibalaError("Session is currently active or in use, cannot delete", {
|
|
1817
|
+
code: "SESSION_IN_USE",
|
|
1818
|
+
safeMessage: "\u4F1A\u8BDD\u6B63\u5728\u4F7F\u7528\u4E2D\uFF0C\u65E0\u6CD5\u5220\u9664"
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1697
1821
|
const targetFile = path4.join(this.directory, fileName);
|
|
1698
1822
|
const lock = await SessionLock.acquire(this.lockPath(conversationId));
|
|
1699
1823
|
try {
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1824
|
+
const headerLines = this.readDiscoveryRecords(targetFile);
|
|
1825
|
+
let header;
|
|
1826
|
+
try {
|
|
1827
|
+
header = headerLines.next().value.payload;
|
|
1828
|
+
} finally {
|
|
1829
|
+
headerLines.return(void 0);
|
|
1706
1830
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
const
|
|
1710
|
-
|
|
1711
|
-
|
|
1831
|
+
if (header.conversationId !== conversationId || normalizedProjectRoot(header.projectRoot) !== normalizedProjectRoot(this.project.projectRoot))
|
|
1832
|
+
throw new Error("Foreign session identity");
|
|
1833
|
+
const metadata = this.metadataPath(targetFile);
|
|
1834
|
+
if (fs5.existsSync(metadata)) fs5.unlinkSync(metadata);
|
|
1835
|
+
fs5.unlinkSync(targetFile);
|
|
1836
|
+
syncSessionDirectory(this.directory);
|
|
1712
1837
|
} finally {
|
|
1713
1838
|
await lock.release();
|
|
1714
1839
|
}
|
|
@@ -1720,6 +1845,11 @@ var SessionManager = class {
|
|
|
1720
1845
|
const file = path4.join(this.directory, fileName);
|
|
1721
1846
|
try {
|
|
1722
1847
|
let meta = this.readMetadata(file);
|
|
1848
|
+
if (this.readOptions.maxReadBytes !== void 0 && fs5.statSync(file).size > this.readOptions.maxReadBytes) {
|
|
1849
|
+
meta ??= this.readOversizedMetadata(file);
|
|
1850
|
+
all.push({ ...meta, unavailableReason: "HISTORY_READ_LIMIT" });
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1723
1853
|
if (!meta) {
|
|
1724
1854
|
const records = this.readDiscoveryRecords(file);
|
|
1725
1855
|
meta = this.buildMetadata(file, records);
|
|
@@ -1730,7 +1860,17 @@ var SessionManager = class {
|
|
|
1730
1860
|
}
|
|
1731
1861
|
}
|
|
1732
1862
|
all.push(meta);
|
|
1733
|
-
} catch {
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
if (error instanceof KapibalaError && error.code === "HISTORY_READ_LIMIT") {
|
|
1865
|
+
try {
|
|
1866
|
+
all.push({
|
|
1867
|
+
...this.readOversizedMetadata(file),
|
|
1868
|
+
unavailableReason: "HISTORY_READ_LIMIT"
|
|
1869
|
+
});
|
|
1870
|
+
continue;
|
|
1871
|
+
} catch {
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1734
1874
|
this.report(`Cannot discover session: ${fileName}`);
|
|
1735
1875
|
}
|
|
1736
1876
|
}
|
|
@@ -1780,7 +1920,10 @@ var SessionManager = class {
|
|
|
1780
1920
|
}
|
|
1781
1921
|
if (header.conversationId !== meta.conversationId || normalizedProjectRoot(header.projectRoot) !== normalizedProjectRoot(this.project.projectRoot))
|
|
1782
1922
|
throw new Error("Foreign session identity");
|
|
1783
|
-
const store = await SessionStore.open(
|
|
1923
|
+
const store = await SessionStore.open(
|
|
1924
|
+
path4.join(this.directory, meta.fileName),
|
|
1925
|
+
this.readOptions
|
|
1926
|
+
);
|
|
1784
1927
|
if ((await store.loadState()).header.conversationId !== meta.conversationId)
|
|
1785
1928
|
throw new Error("Session identity mismatch");
|
|
1786
1929
|
await store.load();
|
|
@@ -1792,7 +1935,9 @@ var SessionManager = class {
|
|
|
1792
1935
|
}
|
|
1793
1936
|
/** 继续最近有正文的会话;没有历史才新建,显式打开失败绝不回退为空会话。 */
|
|
1794
1937
|
async continueRecent(options) {
|
|
1795
|
-
const latest = (await this.allMetadata()).find(
|
|
1938
|
+
const latest = (await this.allMetadata()).find(
|
|
1939
|
+
(m) => m.messageCount > 0 || m.unavailableReason
|
|
1940
|
+
);
|
|
1796
1941
|
if (latest)
|
|
1797
1942
|
return this.open(latest.conversationId, {
|
|
1798
1943
|
autoUpgradeLegacy: options?.autoUpgradeLegacy ?? true
|
|
@@ -1800,11 +1945,21 @@ var SessionManager = class {
|
|
|
1800
1945
|
this.report("No previous session with content; created a new session");
|
|
1801
1946
|
return this.create();
|
|
1802
1947
|
}
|
|
1803
|
-
/**
|
|
1948
|
+
/**
|
|
1949
|
+
* 重命名已打开或待打开的会话;标题作为独立耐久记录,标题或浏览都不改变正文活动排序。
|
|
1950
|
+
*
|
|
1951
|
+
* 未持有句柄时临时打开并在追加后释放:只改标题不该把会话一直挂在写锁上,
|
|
1952
|
+
* 否则同进程后续的归档、删除与置顶都会被自己占住的锁挡掉。
|
|
1953
|
+
*/
|
|
1804
1954
|
async rename(id, title) {
|
|
1805
1955
|
if (!title.trim()) throw new Error("Session title cannot be empty");
|
|
1956
|
+
const alreadyOpen = this.handles.has(this.resolveSession(id).conversationId);
|
|
1806
1957
|
const handle = await this.open(id, { autoUpgradeLegacy: true });
|
|
1807
|
-
|
|
1958
|
+
try {
|
|
1959
|
+
await handle.store.appendRecord("title", { title: sanitizeSessionTitle(title) });
|
|
1960
|
+
} finally {
|
|
1961
|
+
if (!alreadyOpen) await handle.release();
|
|
1962
|
+
}
|
|
1808
1963
|
}
|
|
1809
1964
|
/** 等候所有存储队列后释放句柄;宿主必须先结束 AgentSession 的工具/摘要清理。 */
|
|
1810
1965
|
async close() {
|
|
@@ -1824,7 +1979,7 @@ var SessionManager = class {
|
|
|
1824
1979
|
for (const source of sources) {
|
|
1825
1980
|
if (!fs5.existsSync(source)) continue;
|
|
1826
1981
|
const before = fs5.statSync(source);
|
|
1827
|
-
const bytes =
|
|
1982
|
+
const bytes = readHistoryBytes(source, this.readOptions);
|
|
1828
1983
|
const identity = createHash4("sha256").update(fs5.realpathSync.native(source)).update("\0").update(bytes).digest("hex");
|
|
1829
1984
|
const importLock = await SessionLock.acquire(
|
|
1830
1985
|
path4.join(this.directory, `import-${identity}.lock`)
|
|
@@ -1850,19 +2005,23 @@ var SessionManager = class {
|
|
|
1850
2005
|
const id = createMessageId();
|
|
1851
2006
|
const target = path4.join(this.directory, `${Date.now()}_${id}.jsonl`);
|
|
1852
2007
|
temp = `${target}.tmp-${createMessageId()}`;
|
|
1853
|
-
const store = await SessionStore.create(
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
2008
|
+
const store = await SessionStore.create(
|
|
2009
|
+
temp,
|
|
2010
|
+
{
|
|
2011
|
+
conversationId: id,
|
|
2012
|
+
projectRoot: this.project.projectRoot,
|
|
2013
|
+
initialCwd: this.project.cwd,
|
|
2014
|
+
gitBranch: this.project.gitBranch,
|
|
2015
|
+
importIdentity: identity
|
|
2016
|
+
},
|
|
2017
|
+
this.readOptions
|
|
2018
|
+
);
|
|
1860
2019
|
await store.appendRecord("legacy_import", { source, identity });
|
|
1861
2020
|
await store.appendRecord("title", { title: "\u65E7\u5386\u53F2\u5BFC\u5165" });
|
|
1862
2021
|
for (const message of identifyHistory(raw, identity)) await store.append(message);
|
|
1863
2022
|
await store.load();
|
|
1864
2023
|
const after = fs5.statSync(source);
|
|
1865
|
-
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || !
|
|
2024
|
+
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || !readHistoryBytes(source, this.readOptions).equals(bytes)) {
|
|
1866
2025
|
this.report("Legacy history is changing; import postponed");
|
|
1867
2026
|
continue;
|
|
1868
2027
|
}
|
|
@@ -1925,7 +2084,7 @@ var SessionManager = class {
|
|
|
1925
2084
|
}
|
|
1926
2085
|
const nativeSource = toNativePhysicalPath(sourcePath);
|
|
1927
2086
|
const beforeStat = fs5.statSync(nativeSource);
|
|
1928
|
-
const bytes =
|
|
2087
|
+
const bytes = readHistoryBytes(nativeSource, this.readOptions);
|
|
1929
2088
|
const upgradeIdentity = createHash4("sha256").update(nativeSource).update("\0").update(bytes).digest("hex");
|
|
1930
2089
|
const existingMeta = (await this.allMetadata()).find(
|
|
1931
2090
|
(m) => m.upgradeIdentity === upgradeIdentity
|
|
@@ -1944,7 +2103,7 @@ var SessionManager = class {
|
|
|
1944
2103
|
if (doubleCheck) {
|
|
1945
2104
|
return this.open(doubleCheck.conversationId);
|
|
1946
2105
|
}
|
|
1947
|
-
const sourceStore = await SessionStore.open(nativeSource);
|
|
2106
|
+
const sourceStore = await SessionStore.open(nativeSource, this.readOptions);
|
|
1948
2107
|
if (sourceStore.getSchemaVersion() !== SESSION_SCHEMA_VERSION_V1) {
|
|
1949
2108
|
const state = await sourceStore.loadState();
|
|
1950
2109
|
return this.open(state.header.conversationId);
|
|
@@ -2011,7 +2170,7 @@ var SessionManager = class {
|
|
|
2011
2170
|
fs5.closeSync(fd);
|
|
2012
2171
|
}
|
|
2013
2172
|
const afterStat = fs5.statSync(nativeSource);
|
|
2014
|
-
if (beforeStat.size !== afterStat.size || beforeStat.mtimeMs !== afterStat.mtimeMs || !
|
|
2173
|
+
if (beforeStat.size !== afterStat.size || beforeStat.mtimeMs !== afterStat.mtimeMs || !readHistoryBytes(nativeSource, this.readOptions).equals(bytes)) {
|
|
2015
2174
|
throw new Error("Source legacy session changed during upgrade; retry aborted");
|
|
2016
2175
|
}
|
|
2017
2176
|
fs5.renameSync(tempPath, targetPath);
|
|
@@ -2174,7 +2333,7 @@ var SessionManager = class {
|
|
|
2174
2333
|
store.setAppendObserver(
|
|
2175
2334
|
(record) => {
|
|
2176
2335
|
const previous = JSON.parse(
|
|
2177
|
-
|
|
2336
|
+
readHistoryBytes(this.metadataPath(store.filePath), this.readOptions).toString("utf8")
|
|
2178
2337
|
);
|
|
2179
2338
|
const currentPrev = record.prevRecordId ?? record.parentId;
|
|
2180
2339
|
if (previous.conversationId !== header.conversationId || currentPrev !== null && currentPrev !== (previous.logRevision ?? previous.revision))
|
|
@@ -2219,10 +2378,28 @@ var SessionManager = class {
|
|
|
2219
2378
|
this.handles.set(header.conversationId, handle);
|
|
2220
2379
|
return handle;
|
|
2221
2380
|
}
|
|
2381
|
+
/** 超限列表只解析有界头部并核对桶身份,不读取正文、不生成可误用的耐久缓存。 */
|
|
2382
|
+
readOversizedMetadata(file) {
|
|
2383
|
+
const fd = fs5.openSync(file, "r");
|
|
2384
|
+
try {
|
|
2385
|
+
const buffer = Buffer.alloc(Math.min(this.readOptions.maxReadBytes, 64 * 1024));
|
|
2386
|
+
const length = fs5.readSync(fd, buffer, 0, buffer.length, 0);
|
|
2387
|
+
const end = buffer.subarray(0, length).indexOf(10);
|
|
2388
|
+
if (end < 0) throw new Error("Session header exceeds discovery budget");
|
|
2389
|
+
const header = JSON.parse(buffer.subarray(0, end).toString("utf8"));
|
|
2390
|
+
if (header.type !== "header" || ![1, 2].includes(header.schemaVersion))
|
|
2391
|
+
throw new Error("Session corrupt header");
|
|
2392
|
+
return { ...this.buildMetadata(file, [header]), title: "\u5386\u53F2\u8D85\u8FC7\u8BFB\u53D6\u9884\u7B97" };
|
|
2393
|
+
} finally {
|
|
2394
|
+
fs5.closeSync(fd);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2222
2397
|
/** 缓存通过文件名、身份、统计与正文 stat 校验;失败交由正文重建。 */
|
|
2223
2398
|
readMetadata(file) {
|
|
2224
2399
|
try {
|
|
2225
|
-
const meta = JSON.parse(
|
|
2400
|
+
const meta = JSON.parse(
|
|
2401
|
+
readHistoryBytes(this.metadataPath(file), this.readOptions).toString("utf8")
|
|
2402
|
+
);
|
|
2226
2403
|
const stat = fs5.statSync(file);
|
|
2227
2404
|
if ((meta.schemaVersion === 1 || meta.schemaVersion === 2) && meta.fileName === path4.basename(file) && typeof meta.conversationId === "string" && path4.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)
|
|
2228
2405
|
return meta;
|
|
@@ -2232,7 +2409,7 @@ var SessionManager = class {
|
|
|
2232
2409
|
}
|
|
2233
2410
|
/** 浏览仅解析完整记录;严格打开由 SessionStore 验证,浏览不能改写残片。 */
|
|
2234
2411
|
*readDiscoveryRecords(file) {
|
|
2235
|
-
const iterator = readJsonlLines(file);
|
|
2412
|
+
const iterator = readJsonlLines(file, this.readOptions);
|
|
2236
2413
|
let first = true;
|
|
2237
2414
|
let blankSeen = false;
|
|
2238
2415
|
for (const line of iterator) {
|
|
@@ -2287,7 +2464,8 @@ var SessionManager = class {
|
|
|
2287
2464
|
contentRevision: first.value.recordId,
|
|
2288
2465
|
activeBranchId: MAIN_BRANCH_ID,
|
|
2289
2466
|
activeLeafId: first.value.recordId,
|
|
2290
|
-
archived: false
|
|
2467
|
+
archived: false,
|
|
2468
|
+
pinned: false
|
|
2291
2469
|
};
|
|
2292
2470
|
const branchLeaves = /* @__PURE__ */ new Map();
|
|
2293
2471
|
branchLeaves.set(MAIN_BRANCH_ID, first.value.recordId);
|
|
@@ -2340,6 +2518,8 @@ var SessionManager = class {
|
|
|
2340
2518
|
if (bId) meta.activeBranchId = bId;
|
|
2341
2519
|
} else if (record.type === "archive_state") {
|
|
2342
2520
|
meta.archived = Boolean(record.payload.archived);
|
|
2521
|
+
} else if (record.type === "pin_state") {
|
|
2522
|
+
meta.pinned = Boolean(record.payload.pinned);
|
|
2343
2523
|
}
|
|
2344
2524
|
if (branchLeaves && !isGlobalControlRecordType(record.type)) {
|
|
2345
2525
|
const bId = record.branchId ?? meta.activeBranchId ?? MAIN_BRANCH_ID;
|
|
@@ -2846,6 +3026,7 @@ var CrossProjectSessionCatalog = class {
|
|
|
2846
3026
|
let title = "\u65B0\u4F1A\u8BDD";
|
|
2847
3027
|
let activeBranchId = MAIN_BRANCH_ID;
|
|
2848
3028
|
let archived = false;
|
|
3029
|
+
let pinned = false;
|
|
2849
3030
|
let lastRecordId = headerRecord.recordId;
|
|
2850
3031
|
for (const line of iterator) {
|
|
2851
3032
|
if (!line.text.trim()) continue;
|
|
@@ -2866,6 +3047,8 @@ var CrossProjectSessionCatalog = class {
|
|
|
2866
3047
|
title = String(rec.payload.title);
|
|
2867
3048
|
} else if (rec.type === "archive_state") {
|
|
2868
3049
|
archived = Boolean(rec.payload.archived);
|
|
3050
|
+
} else if (rec.type === "pin_state") {
|
|
3051
|
+
pinned = Boolean(rec.payload.pinned);
|
|
2869
3052
|
} else if (rec.type === "branch_selected") {
|
|
2870
3053
|
activeBranchId = String(rec.payload.branchId);
|
|
2871
3054
|
}
|
|
@@ -2884,6 +3067,7 @@ var CrossProjectSessionCatalog = class {
|
|
|
2884
3067
|
title,
|
|
2885
3068
|
activeBranchId,
|
|
2886
3069
|
archived,
|
|
3070
|
+
pinned,
|
|
2887
3071
|
size: stat.size,
|
|
2888
3072
|
mtimeMs: stat.mtimeMs,
|
|
2889
3073
|
revision: lastRecordId,
|
|
@@ -6021,7 +6205,8 @@ function stopProcessTree(child, nativeGroup) {
|
|
|
6021
6205
|
try {
|
|
6022
6206
|
process.kill(-child.pid, "SIGTERM");
|
|
6023
6207
|
return true;
|
|
6024
|
-
} catch {
|
|
6208
|
+
} catch (error) {
|
|
6209
|
+
if (error.code === "ESRCH") return true;
|
|
6025
6210
|
try {
|
|
6026
6211
|
child.kill("SIGTERM");
|
|
6027
6212
|
} catch {
|
|
@@ -6042,7 +6227,8 @@ function forceProcessTree(child, nativeGroup) {
|
|
|
6042
6227
|
try {
|
|
6043
6228
|
process.kill(-child.pid, "SIGKILL");
|
|
6044
6229
|
return true;
|
|
6045
|
-
} catch {
|
|
6230
|
+
} catch (error) {
|
|
6231
|
+
if (error.code === "ESRCH") return true;
|
|
6046
6232
|
try {
|
|
6047
6233
|
child.kill("SIGKILL");
|
|
6048
6234
|
} catch {
|
|
@@ -6063,13 +6249,31 @@ async function confirmProcessTreeStopped(child, terminationSucceeded, nativeGrou
|
|
|
6063
6249
|
if (process.platform === "win32") return true;
|
|
6064
6250
|
const deadline = Date.now() + 1e3;
|
|
6065
6251
|
while (Date.now() < deadline) {
|
|
6252
|
+
let exists;
|
|
6066
6253
|
try {
|
|
6067
6254
|
process.kill(-child.pid, 0);
|
|
6255
|
+
exists = true;
|
|
6068
6256
|
} catch (error) {
|
|
6069
|
-
|
|
6070
|
-
return
|
|
6257
|
+
const code = error.code;
|
|
6258
|
+
if (code === "ESRCH") return true;
|
|
6259
|
+
if (code !== "EPERM") return false;
|
|
6260
|
+
exists = true;
|
|
6071
6261
|
}
|
|
6072
|
-
|
|
6262
|
+
if (groupHasLiveProcess(child.pid)) {
|
|
6263
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
6264
|
+
continue;
|
|
6265
|
+
}
|
|
6266
|
+
return true;
|
|
6267
|
+
}
|
|
6268
|
+
return false;
|
|
6269
|
+
}
|
|
6270
|
+
function groupHasLiveProcess(pgid) {
|
|
6271
|
+
const result = spawnSync2("ps", ["-axo", "pgid=", "stat="], { timeout: 2e3, encoding: "utf8" });
|
|
6272
|
+
if (result.status !== 0 || result.error) return true;
|
|
6273
|
+
for (const row of result.stdout.split("\n")) {
|
|
6274
|
+
const columns = row.trim().split(/\s+/);
|
|
6275
|
+
if (columns.length < 2 || Number(columns[0]) !== pgid) continue;
|
|
6276
|
+
if (!columns[1].startsWith("Z")) return true;
|
|
6073
6277
|
}
|
|
6074
6278
|
return false;
|
|
6075
6279
|
}
|
|
@@ -7437,133 +7641,462 @@ var HookRegistry = class {
|
|
|
7437
7641
|
};
|
|
7438
7642
|
|
|
7439
7643
|
// src/runtime/executor/index.ts
|
|
7440
|
-
import { createHash as createHash12 } from "crypto";
|
|
7441
7644
|
import path17 from "path";
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
this.rootDir = options.rootDir;
|
|
7482
|
-
this.signal = options.signal ? AbortSignal.any([options.signal, this.cancellation.signal]) : this.cancellation.signal;
|
|
7483
|
-
this.logger = options.logger;
|
|
7484
|
-
this.toolTimeoutMs = options.toolTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
7485
|
-
this.eventLogger = options.eventLogger;
|
|
7486
|
-
this.sessionId = options.sessionId;
|
|
7487
|
-
this.runId = options.runId;
|
|
7488
|
-
this.branchId = options.branchId;
|
|
7489
|
-
this.rootTaskId = options.rootTaskId;
|
|
7490
|
-
this.depth = options.depth;
|
|
7491
|
-
this.agentId = options.agentId;
|
|
7492
|
-
this.mode = options.mode ?? "Approval";
|
|
7493
|
-
this.permissionRules = options.permissionRules ?? [];
|
|
7494
|
-
this.approvalChannel = options.approvalChannel;
|
|
7495
|
-
this.approvalCache = options.approvalCache ?? new SessionApprovalCache();
|
|
7496
|
-
}
|
|
7497
|
-
/**
|
|
7498
|
-
* 父工具事务提交成功后调用,按注册顺序执行所有提交动作。
|
|
7499
|
-
* 动作执行成功后逐项清理对应回滚动作,若某项失败保留剩余回滚动作。
|
|
7500
|
-
*/
|
|
7501
|
-
async commit() {
|
|
7502
|
-
while (this.committedActions.length > 0) {
|
|
7503
|
-
const action = this.committedActions.shift();
|
|
7504
|
-
await action();
|
|
7505
|
-
this.rollbackActions.shift();
|
|
7506
|
-
}
|
|
7507
|
-
this.rollbackActions.length = 0;
|
|
7508
|
-
}
|
|
7509
|
-
/**
|
|
7510
|
-
* 父工具事务提交失败或取消时调用,按逆序执行所有回滚清理动作。
|
|
7511
|
-
*/
|
|
7512
|
-
async rollback() {
|
|
7513
|
-
const actions = [...this.rollbackActions].reverse();
|
|
7514
|
-
this.committedActions.length = 0;
|
|
7515
|
-
this.rollbackActions.length = 0;
|
|
7516
|
-
for (const action of actions) {
|
|
7645
|
+
|
|
7646
|
+
// src/runtime/executor/approval.ts
|
|
7647
|
+
import { createHash as createHash12 } from "crypto";
|
|
7648
|
+
async function resolveToolApproval(deps) {
|
|
7649
|
+
const {
|
|
7650
|
+
call,
|
|
7651
|
+
finalInput,
|
|
7652
|
+
tool,
|
|
7653
|
+
rootDir,
|
|
7654
|
+
executionCwd,
|
|
7655
|
+
mode,
|
|
7656
|
+
permissionRules,
|
|
7657
|
+
approvalChannel,
|
|
7658
|
+
approvalCache,
|
|
7659
|
+
hardDenial,
|
|
7660
|
+
neverFailures,
|
|
7661
|
+
policy,
|
|
7662
|
+
signal,
|
|
7663
|
+
audit,
|
|
7664
|
+
failure
|
|
7665
|
+
} = deps;
|
|
7666
|
+
let shell = deps.shell;
|
|
7667
|
+
const executionIdentity = deps.executionIdentity;
|
|
7668
|
+
let auditFields = {};
|
|
7669
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
7670
|
+
const request = {
|
|
7671
|
+
toolName: call.name,
|
|
7672
|
+
capabilities: tool.metadata?.permissions ?? [],
|
|
7673
|
+
input: finalInput,
|
|
7674
|
+
rootDir,
|
|
7675
|
+
...shell ? { shell } : {},
|
|
7676
|
+
...executionIdentity ? { executionIdentity } : {},
|
|
7677
|
+
// 解释器内建查询与内容已绑定摘要的工作区直引脚本可确认不受外部脚本变更影响,
|
|
7678
|
+
// 允许会话级批准;其余 shell 命令仅允许单次批准。
|
|
7679
|
+
// 非 shell 工具(含深度绑定 executionIdentity 的外部工具)允许会话级批准。
|
|
7680
|
+
sessionAllowed: !shell || /^(?:pwd|get-location)$/i.test(shell.command.trim()) || shell.scriptDigest !== void 0
|
|
7681
|
+
};
|
|
7682
|
+
const repeatKey = createHash12("sha256").update(stableJsonStringify([call.name, finalInput])).digest("hex");
|
|
7683
|
+
if (neverFailures.has(repeatKey)) {
|
|
7517
7684
|
try {
|
|
7518
|
-
await
|
|
7685
|
+
await audit("tool.decided", { decision: "deny", decisionSource: "repeat_block" });
|
|
7686
|
+
await audit("tool.finished", { status: "blocked", errorCode: "REPEAT_BLOCKED" });
|
|
7519
7687
|
} catch {
|
|
7688
|
+
return {
|
|
7689
|
+
result: failure(
|
|
7690
|
+
"Audit unavailable; tool was not started",
|
|
7691
|
+
"AUDIT_UNAVAILABLE",
|
|
7692
|
+
"after_user_action",
|
|
7693
|
+
{
|
|
7694
|
+
version: 1,
|
|
7695
|
+
dispatchOutcome: "not_sent",
|
|
7696
|
+
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7697
|
+
}
|
|
7698
|
+
)
|
|
7699
|
+
};
|
|
7520
7700
|
}
|
|
7701
|
+
return {
|
|
7702
|
+
result: failure(
|
|
7703
|
+
"Identical failed tool call cannot be repeated in this run",
|
|
7704
|
+
"REPEAT_BLOCKED"
|
|
7705
|
+
)
|
|
7706
|
+
};
|
|
7521
7707
|
}
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
for (const call of calls) {
|
|
7535
|
-
const result = await this.executeOne(call, onProgress, requiresUserAction);
|
|
7536
|
-
results.push(result);
|
|
7537
|
-
onResult?.(structuredClone(result));
|
|
7538
|
-
if (result.retryPolicy === "after_user_action") requiresUserAction = true;
|
|
7539
|
-
}
|
|
7540
|
-
return results;
|
|
7541
|
-
}
|
|
7542
|
-
/**
|
|
7543
|
-
* 在最终参数上裁决并审计一次调用,所有拒绝和异常均闭合为工具结果。
|
|
7544
|
-
* 为调用记录本次绝对工具根目录,供跨目录恢复后的文件事实解析;不使用历史目录授权。
|
|
7545
|
-
*/
|
|
7546
|
-
async executeOne(call, onProgress, forcedDeny = false) {
|
|
7547
|
-
call.executionRoot = path17.resolve(this.workspaceRoot);
|
|
7548
|
-
const startedAt = Date.now();
|
|
7549
|
-
const operationId = createLogId();
|
|
7550
|
-
const failure = (message, errorCode, retryPolicy = "never", execution) => ({
|
|
7551
|
-
type: "tool_result",
|
|
7552
|
-
toolUseId: call.id,
|
|
7553
|
-
content: JSON.stringify({ code: errorCode, retryPolicy, message }),
|
|
7554
|
-
isError: true,
|
|
7555
|
-
errorCode,
|
|
7556
|
-
retryPolicy,
|
|
7557
|
-
durationMs: Math.max(0, Date.now() - startedAt),
|
|
7558
|
-
execution: execution ?? {
|
|
7559
|
-
version: 1,
|
|
7560
|
-
dispatchOutcome: "not_sent"
|
|
7561
|
-
}
|
|
7708
|
+
const hardDenied = hardDenial?.(tool, finalInput);
|
|
7709
|
+
const cached = approvalCache.get(request);
|
|
7710
|
+
const decision = policy.decide({
|
|
7711
|
+
hardDenied: hardDenied !== void 0,
|
|
7712
|
+
mode: typeof mode === "function" ? mode() : mode,
|
|
7713
|
+
toolName: call.name,
|
|
7714
|
+
capabilities: request.capabilities,
|
|
7715
|
+
dangerous: tool.metadata?.dangerous,
|
|
7716
|
+
cachedApproval: cached === "allow",
|
|
7717
|
+
cachedDenial: cached === "deny",
|
|
7718
|
+
rules: permissionRules,
|
|
7719
|
+
...shell ? { shell } : {}
|
|
7562
7720
|
});
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7721
|
+
let permitted = decision.decision === "allow";
|
|
7722
|
+
let source = decision.source;
|
|
7723
|
+
let approvalChoice;
|
|
7724
|
+
if (decision.decision === "ask") {
|
|
7725
|
+
let choice = "deny_once";
|
|
7726
|
+
try {
|
|
7727
|
+
choice = await approvalChannel?.requestApproval(request, signal) ?? "deny_once";
|
|
7728
|
+
} catch {
|
|
7729
|
+
choice = "cancel";
|
|
7730
|
+
}
|
|
7731
|
+
approvalChoice = choice;
|
|
7732
|
+
if (choice === "allow_session" && request.sessionAllowed) approvalCache.set(request, "allow");
|
|
7733
|
+
if (choice === "deny_session") approvalCache.set(request, "deny");
|
|
7734
|
+
permitted = choice === "allow_once" || choice === "allow_session" && request.sessionAllowed;
|
|
7735
|
+
source = approvalChannel ? "human" : "non_interactive";
|
|
7736
|
+
}
|
|
7737
|
+
auditFields = shell ? {
|
|
7738
|
+
shell: shell.interpreter,
|
|
7739
|
+
cwdDigest: createHash12("sha256").update(shell.cwd).digest("hex"),
|
|
7740
|
+
executableDigest: shell.executableDigest,
|
|
7741
|
+
executionFingerprint: shell.executionFingerprint,
|
|
7742
|
+
...commandAuditSummary(shell.command)
|
|
7743
|
+
} : executionIdentity ? {
|
|
7744
|
+
serverIdentity: executionIdentity.serverIdentity,
|
|
7745
|
+
connectionGeneration: executionIdentity.connectionGeneration,
|
|
7746
|
+
catalogEpoch: executionIdentity.catalogEpoch,
|
|
7747
|
+
toolDigest: executionIdentity.toolDigest
|
|
7748
|
+
} : {};
|
|
7749
|
+
try {
|
|
7750
|
+
await audit("tool.decided", {
|
|
7751
|
+
decision: permitted ? "allow" : "deny",
|
|
7752
|
+
decisionSource: source,
|
|
7753
|
+
...approvalChoice ? { approvalChoice } : {},
|
|
7754
|
+
...hardDenied?.fields ?? {},
|
|
7755
|
+
...auditFields
|
|
7756
|
+
});
|
|
7757
|
+
} catch {
|
|
7758
|
+
return {
|
|
7759
|
+
result: failure(
|
|
7760
|
+
"Audit unavailable; tool was not started",
|
|
7761
|
+
"AUDIT_UNAVAILABLE",
|
|
7762
|
+
"after_user_action",
|
|
7763
|
+
{
|
|
7764
|
+
version: 1,
|
|
7765
|
+
dispatchOutcome: "not_sent",
|
|
7766
|
+
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7767
|
+
}
|
|
7768
|
+
)
|
|
7769
|
+
};
|
|
7770
|
+
}
|
|
7771
|
+
if (!permitted || signal?.aborted) {
|
|
7772
|
+
const code = signal?.aborted ? "ABORTED" : hardDenied?.code ?? "PERMISSION_DENIED";
|
|
7773
|
+
try {
|
|
7774
|
+
await audit("tool.finished", { status: "denied", errorCode: code });
|
|
7775
|
+
} catch {
|
|
7776
|
+
return {
|
|
7777
|
+
result: failure("Audit result unavailable", "AUDIT_UNAVAILABLE", "after_user_action", {
|
|
7778
|
+
version: 1,
|
|
7779
|
+
dispatchOutcome: "not_sent",
|
|
7780
|
+
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7781
|
+
})
|
|
7782
|
+
};
|
|
7783
|
+
}
|
|
7784
|
+
return {
|
|
7785
|
+
result: failure(
|
|
7786
|
+
signal?.aborted ? "Operation cancelled" : hardDenied?.message ?? "Tool permission denied",
|
|
7787
|
+
code,
|
|
7788
|
+
signal?.aborted ? "after_user_action" : hardDenied?.retryPolicy ?? "after_user_action"
|
|
7789
|
+
)
|
|
7790
|
+
};
|
|
7791
|
+
}
|
|
7792
|
+
if (shell) {
|
|
7793
|
+
try {
|
|
7794
|
+
const next = tool.approvalScope?.(finalInput, rootDir, executionCwd);
|
|
7795
|
+
if (next?.executionFingerprint !== shell.executionFingerprint) {
|
|
7796
|
+
if (attempt === 0 && next) {
|
|
7797
|
+
try {
|
|
7798
|
+
await audit("tool.target_changed", { status: "redeciding" });
|
|
7799
|
+
} catch {
|
|
7800
|
+
return {
|
|
7801
|
+
result: failure(
|
|
7802
|
+
"Audit unavailable; tool was not started",
|
|
7803
|
+
"AUDIT_UNAVAILABLE",
|
|
7804
|
+
"after_user_action",
|
|
7805
|
+
{
|
|
7806
|
+
version: 1,
|
|
7807
|
+
dispatchOutcome: "not_sent",
|
|
7808
|
+
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7809
|
+
}
|
|
7810
|
+
)
|
|
7811
|
+
};
|
|
7812
|
+
}
|
|
7813
|
+
shell = next;
|
|
7814
|
+
continue;
|
|
7815
|
+
}
|
|
7816
|
+
await audit("tool.finished", { status: "target_changed", errorCode: "TARGET_CHANGED" });
|
|
7817
|
+
return {
|
|
7818
|
+
result: failure(
|
|
7819
|
+
"Execution target changed after approval",
|
|
7820
|
+
"TARGET_CHANGED",
|
|
7821
|
+
"after_user_action"
|
|
7822
|
+
)
|
|
7823
|
+
};
|
|
7824
|
+
}
|
|
7825
|
+
} catch {
|
|
7826
|
+
return {
|
|
7827
|
+
result: failure(
|
|
7828
|
+
"Execution target changed after approval",
|
|
7829
|
+
"TARGET_CHANGED",
|
|
7830
|
+
"after_user_action"
|
|
7831
|
+
)
|
|
7832
|
+
};
|
|
7833
|
+
}
|
|
7834
|
+
}
|
|
7835
|
+
if (executionIdentity) {
|
|
7836
|
+
try {
|
|
7837
|
+
const nextIdentity = tool.executionIdentity?.(finalInput, rootDir);
|
|
7838
|
+
if (!nextIdentity || nextIdentity.serverIdentity !== executionIdentity.serverIdentity || nextIdentity.connectionGeneration !== executionIdentity.connectionGeneration || nextIdentity.catalogEpoch !== executionIdentity.catalogEpoch || nextIdentity.toolDigest !== executionIdentity.toolDigest) {
|
|
7839
|
+
await audit("tool.finished", {
|
|
7840
|
+
status: "identity_changed",
|
|
7841
|
+
errorCode: "MCP_TOOL_CHANGED"
|
|
7842
|
+
});
|
|
7843
|
+
return {
|
|
7844
|
+
result: failure(
|
|
7845
|
+
"Tool execution identity changed after approval",
|
|
7846
|
+
"MCP_TOOL_CHANGED",
|
|
7847
|
+
"after_user_action",
|
|
7848
|
+
{
|
|
7849
|
+
version: 1,
|
|
7850
|
+
dispatchOutcome: "not_sent",
|
|
7851
|
+
hostError: { phase: "validation", code: "MCP_TOOL_CHANGED" }
|
|
7852
|
+
}
|
|
7853
|
+
)
|
|
7854
|
+
};
|
|
7855
|
+
}
|
|
7856
|
+
} catch {
|
|
7857
|
+
return {
|
|
7858
|
+
result: failure(
|
|
7859
|
+
"Tool execution identity check failed after approval",
|
|
7860
|
+
"MCP_TOOL_CHANGED",
|
|
7861
|
+
"after_user_action",
|
|
7862
|
+
{
|
|
7863
|
+
version: 1,
|
|
7864
|
+
dispatchOutcome: "not_sent"
|
|
7865
|
+
}
|
|
7866
|
+
)
|
|
7867
|
+
};
|
|
7868
|
+
}
|
|
7869
|
+
}
|
|
7870
|
+
break;
|
|
7871
|
+
}
|
|
7872
|
+
return { shell, auditFields };
|
|
7873
|
+
}
|
|
7874
|
+
|
|
7875
|
+
// src/runtime/executor/result.ts
|
|
7876
|
+
import { createHash as createHash13 } from "crypto";
|
|
7877
|
+
async function closeToolResult(deps) {
|
|
7878
|
+
const { call, tool, finalInput, hookCtx, hooks, neverFailures, audit, onToolResult, startedAt } = deps;
|
|
7879
|
+
const resultBlock = {
|
|
7880
|
+
type: "tool_result",
|
|
7881
|
+
toolUseId: call.id,
|
|
7882
|
+
content: deps.output.content,
|
|
7883
|
+
isError: deps.output.isError,
|
|
7884
|
+
...deps.output.errorCode ? { errorCode: deps.output.errorCode, retryPolicy: deps.output.retryPolicy } : {},
|
|
7885
|
+
execution: deps.output.execution
|
|
7886
|
+
};
|
|
7887
|
+
if (resultBlock.isError && resultBlock.errorCode) {
|
|
7888
|
+
resultBlock.content = JSON.stringify({
|
|
7889
|
+
code: resultBlock.errorCode,
|
|
7890
|
+
retryPolicy: resultBlock.retryPolicy,
|
|
7891
|
+
message: deps.output.content
|
|
7892
|
+
});
|
|
7893
|
+
}
|
|
7894
|
+
try {
|
|
7895
|
+
for (const hook of hooks.get("tool:after")) {
|
|
7896
|
+
await hook(
|
|
7897
|
+
hookCtx,
|
|
7898
|
+
{ id: call.id, name: call.name, input: finalInput },
|
|
7899
|
+
{ output: deps.output.content, isError: deps.output.isError }
|
|
7900
|
+
);
|
|
7901
|
+
}
|
|
7902
|
+
} catch {
|
|
7903
|
+
resultBlock.content = JSON.stringify({
|
|
7904
|
+
code: "HOOK_ERROR",
|
|
7905
|
+
retryPolicy: "after_user_action",
|
|
7906
|
+
message: "Tool finished but result processing failed"
|
|
7907
|
+
});
|
|
7908
|
+
resultBlock.isError = true;
|
|
7909
|
+
resultBlock.errorCode = "HOOK_ERROR";
|
|
7910
|
+
resultBlock.retryPolicy = "after_user_action";
|
|
7911
|
+
resultBlock.execution = {
|
|
7912
|
+
version: 1,
|
|
7913
|
+
dispatchOutcome: resultBlock.execution?.dispatchOutcome ?? "failed",
|
|
7914
|
+
hostError: {
|
|
7915
|
+
phase: "hook",
|
|
7916
|
+
code: "HOOK_ERROR"
|
|
7917
|
+
}
|
|
7918
|
+
};
|
|
7919
|
+
}
|
|
7920
|
+
resultBlock.durationMs = Math.max(0, Date.now() - startedAt);
|
|
7921
|
+
if (resultBlock.isError && resultBlock.retryPolicy === "never") {
|
|
7922
|
+
neverFailures.add(
|
|
7923
|
+
createHash13("sha256").update(stableJsonStringify([call.name, finalInput])).digest("hex")
|
|
7924
|
+
);
|
|
7925
|
+
}
|
|
7926
|
+
try {
|
|
7927
|
+
await audit("tool.finished", {
|
|
7928
|
+
status: resultBlock.isError ? "failed" : "completed",
|
|
7929
|
+
...resultBlock.errorCode ? { errorCode: resultBlock.errorCode } : {},
|
|
7930
|
+
durationMs: resultBlock.durationMs
|
|
7931
|
+
});
|
|
7932
|
+
} catch {
|
|
7933
|
+
resultBlock.isError = true;
|
|
7934
|
+
resultBlock.errorCode = "OUTCOME_UNKNOWN";
|
|
7935
|
+
resultBlock.retryPolicy = "after_user_action";
|
|
7936
|
+
resultBlock.content = JSON.stringify({
|
|
7937
|
+
code: "OUTCOME_UNKNOWN",
|
|
7938
|
+
retryPolicy: "after_user_action",
|
|
7939
|
+
message: "Tool outcome is unknown because the audit result could not be written"
|
|
7940
|
+
});
|
|
7941
|
+
resultBlock.execution = {
|
|
7942
|
+
version: 1,
|
|
7943
|
+
dispatchOutcome: resultBlock.execution?.dispatchOutcome ?? "succeeded",
|
|
7944
|
+
hostError: {
|
|
7945
|
+
phase: "audit",
|
|
7946
|
+
code: "AUDIT_UNAVAILABLE"
|
|
7947
|
+
}
|
|
7948
|
+
};
|
|
7949
|
+
return resultBlock;
|
|
7950
|
+
}
|
|
7951
|
+
await onToolResult?.(tool, finalInput, resultBlock);
|
|
7952
|
+
return resultBlock;
|
|
7953
|
+
}
|
|
7954
|
+
|
|
7955
|
+
// src/runtime/executor/timeout.ts
|
|
7956
|
+
function withToolTimeout(promise, toolName, timeoutMs, executionController) {
|
|
7957
|
+
let timer;
|
|
7958
|
+
const timeout = new Promise((_, reject) => {
|
|
7959
|
+
timer = setTimeout(() => {
|
|
7960
|
+
const error = new KapibalaError(`Tool '${toolName}' timed out after ${timeoutMs}ms`, {
|
|
7961
|
+
code: "OUTCOME_UNKNOWN",
|
|
7962
|
+
retryPolicy: "after_user_action",
|
|
7963
|
+
safeMessage: "Tool timed out; completion of side effects is unknown"
|
|
7964
|
+
});
|
|
7965
|
+
reject(error);
|
|
7966
|
+
executionController.abort(error);
|
|
7967
|
+
}, timeoutMs);
|
|
7968
|
+
});
|
|
7969
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
7970
|
+
}
|
|
7971
|
+
|
|
7972
|
+
// src/runtime/executor/types.ts
|
|
7973
|
+
var DEFAULT_TOOL_TIMEOUT_MS = 12e4;
|
|
7974
|
+
|
|
7975
|
+
// src/runtime/executor/index.ts
|
|
7976
|
+
var ToolExecutor = class {
|
|
7977
|
+
hardDenial;
|
|
7978
|
+
onToolResult;
|
|
7979
|
+
tools;
|
|
7980
|
+
hooks;
|
|
7981
|
+
workspaceRoot;
|
|
7982
|
+
executionCwd;
|
|
7983
|
+
sandbox;
|
|
7984
|
+
leaseCoordinator;
|
|
7985
|
+
rootDir;
|
|
7986
|
+
signal;
|
|
7987
|
+
logger;
|
|
7988
|
+
toolTimeoutMs;
|
|
7989
|
+
eventLogger;
|
|
7990
|
+
sessionId;
|
|
7991
|
+
runId;
|
|
7992
|
+
branchId;
|
|
7993
|
+
rootTaskId;
|
|
7994
|
+
depth;
|
|
7995
|
+
agentId;
|
|
7996
|
+
committedActions = [];
|
|
7997
|
+
rollbackActions = [];
|
|
7998
|
+
mode;
|
|
7999
|
+
permissionRules;
|
|
8000
|
+
approvalChannel;
|
|
8001
|
+
approvalCache;
|
|
8002
|
+
policy = new PermissionPolicy();
|
|
8003
|
+
neverFailures = /* @__PURE__ */ new Set();
|
|
8004
|
+
cancellation = new AbortController();
|
|
8005
|
+
constructor(options) {
|
|
8006
|
+
this.hardDenial = options.hardDenial;
|
|
8007
|
+
this.onToolResult = options.onToolResult;
|
|
8008
|
+
this.tools = options.tools;
|
|
8009
|
+
this.hooks = options.hooks;
|
|
8010
|
+
this.workspaceRoot = options.workspaceRoot ?? options.rootDir;
|
|
8011
|
+
this.executionCwd = options.executionCwd ?? options.rootDir;
|
|
8012
|
+
this.sandbox = options.sandbox;
|
|
8013
|
+
this.leaseCoordinator = options.leaseCoordinator;
|
|
8014
|
+
this.rootDir = options.rootDir;
|
|
8015
|
+
this.signal = options.signal ? AbortSignal.any([options.signal, this.cancellation.signal]) : this.cancellation.signal;
|
|
8016
|
+
this.logger = options.logger;
|
|
8017
|
+
this.toolTimeoutMs = options.toolTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
8018
|
+
this.eventLogger = options.eventLogger;
|
|
8019
|
+
this.sessionId = options.sessionId;
|
|
8020
|
+
this.runId = options.runId;
|
|
8021
|
+
this.branchId = options.branchId;
|
|
8022
|
+
this.rootTaskId = options.rootTaskId;
|
|
8023
|
+
this.depth = options.depth;
|
|
8024
|
+
this.agentId = options.agentId;
|
|
8025
|
+
this.mode = options.mode ?? "Approval";
|
|
8026
|
+
this.permissionRules = options.permissionRules ?? [];
|
|
8027
|
+
this.approvalChannel = options.approvalChannel;
|
|
8028
|
+
this.approvalCache = options.approvalCache ?? new SessionApprovalCache();
|
|
8029
|
+
}
|
|
8030
|
+
/**
|
|
8031
|
+
* 父工具事务提交成功后调用,按注册顺序执行所有提交动作。
|
|
8032
|
+
* 动作执行成功后逐项清理对应回滚动作,若某项失败保留剩余回滚动作。
|
|
8033
|
+
*/
|
|
8034
|
+
async commit() {
|
|
8035
|
+
while (this.committedActions.length > 0) {
|
|
8036
|
+
const action = this.committedActions.shift();
|
|
8037
|
+
await action();
|
|
8038
|
+
this.rollbackActions.shift();
|
|
8039
|
+
}
|
|
8040
|
+
this.rollbackActions.length = 0;
|
|
8041
|
+
}
|
|
8042
|
+
/**
|
|
8043
|
+
* 父工具事务提交失败或取消时调用,按逆序执行所有回滚清理动作。
|
|
8044
|
+
*/
|
|
8045
|
+
async rollback() {
|
|
8046
|
+
const actions = [...this.rollbackActions].reverse();
|
|
8047
|
+
this.committedActions.length = 0;
|
|
8048
|
+
this.rollbackActions.length = 0;
|
|
8049
|
+
for (const action of actions) {
|
|
8050
|
+
try {
|
|
8051
|
+
await action();
|
|
8052
|
+
} catch {
|
|
8053
|
+
}
|
|
8054
|
+
}
|
|
8055
|
+
}
|
|
8056
|
+
/** 中止本轮正在等待或执行的工具;调用方仍须等待 runAll 完成资源回收及结果审计。 */
|
|
8057
|
+
cancel() {
|
|
8058
|
+
this.cancellation.abort();
|
|
8059
|
+
}
|
|
8060
|
+
/**
|
|
8061
|
+
* 串行执行全部调用并返回与入参顺序一致的结果;某个结果要求用户介入后,其余调用直接拒绝。
|
|
8062
|
+
* onResult 在每个最终结果(含拒绝/失败)产生后提供独立快照,供在途预算估算,不代表历史已提交;回调不得抛错。
|
|
8063
|
+
*/
|
|
8064
|
+
async runAll(calls, onProgress, onResult) {
|
|
8065
|
+
const results = [];
|
|
8066
|
+
let requiresUserAction = false;
|
|
8067
|
+
for (const call of calls) {
|
|
8068
|
+
const result = await this.executeOne(call, onProgress, requiresUserAction);
|
|
8069
|
+
results.push(result);
|
|
8070
|
+
onResult?.(structuredClone(result));
|
|
8071
|
+
if (result.retryPolicy === "after_user_action") requiresUserAction = true;
|
|
8072
|
+
}
|
|
8073
|
+
return results;
|
|
8074
|
+
}
|
|
8075
|
+
/**
|
|
8076
|
+
* 在最终参数上裁决并审计一次调用,所有拒绝和异常均闭合为工具结果。
|
|
8077
|
+
* 为调用记录本次绝对工具根目录,供跨目录恢复后的文件事实解析;不使用历史目录授权。
|
|
8078
|
+
*/
|
|
8079
|
+
async executeOne(call, onProgress, forcedDeny = false) {
|
|
8080
|
+
call.executionRoot = path17.resolve(this.workspaceRoot);
|
|
8081
|
+
const startedAt = Date.now();
|
|
8082
|
+
const operationId = createLogId();
|
|
8083
|
+
const failure = (message, errorCode, retryPolicy = "never", execution) => ({
|
|
8084
|
+
type: "tool_result",
|
|
8085
|
+
toolUseId: call.id,
|
|
8086
|
+
content: JSON.stringify({ code: errorCode, retryPolicy, message }),
|
|
8087
|
+
isError: true,
|
|
8088
|
+
errorCode,
|
|
8089
|
+
retryPolicy,
|
|
8090
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
8091
|
+
execution: execution ?? {
|
|
8092
|
+
version: 1,
|
|
8093
|
+
dispatchOutcome: "not_sent"
|
|
8094
|
+
}
|
|
8095
|
+
});
|
|
8096
|
+
const audit = async (event, fields = {}) => {
|
|
8097
|
+
if (!this.eventLogger || !this.sessionId) return;
|
|
8098
|
+
await this.eventLogger.recordAudit({
|
|
8099
|
+
level: "info",
|
|
7567
8100
|
event,
|
|
7568
8101
|
sessionId: this.sessionId,
|
|
7569
8102
|
runId: this.runId,
|
|
@@ -7755,191 +8288,28 @@ var ToolExecutor = class {
|
|
|
7755
8288
|
}
|
|
7756
8289
|
return failure("Invalid tool execution identity", "INVALID_TARGET");
|
|
7757
8290
|
}
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
"AUDIT_UNAVAILABLE",
|
|
7781
|
-
"after_user_action",
|
|
7782
|
-
{
|
|
7783
|
-
version: 1,
|
|
7784
|
-
dispatchOutcome: "not_sent",
|
|
7785
|
-
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7786
|
-
}
|
|
7787
|
-
);
|
|
7788
|
-
}
|
|
7789
|
-
return failure(
|
|
7790
|
-
"Identical failed tool call cannot be repeated in this run",
|
|
7791
|
-
"REPEAT_BLOCKED"
|
|
7792
|
-
);
|
|
7793
|
-
}
|
|
7794
|
-
const hardDenied = this.hardDenial?.(tool, finalInput);
|
|
7795
|
-
const cached = this.approvalCache.get(request);
|
|
7796
|
-
const decision = this.policy.decide({
|
|
7797
|
-
hardDenied: hardDenied !== void 0,
|
|
7798
|
-
mode: typeof this.mode === "function" ? this.mode() : this.mode,
|
|
7799
|
-
toolName: call.name,
|
|
7800
|
-
capabilities: request.capabilities,
|
|
7801
|
-
dangerous: tool.metadata?.dangerous,
|
|
7802
|
-
cachedApproval: cached === "allow",
|
|
7803
|
-
cachedDenial: cached === "deny",
|
|
7804
|
-
rules: this.permissionRules,
|
|
7805
|
-
...shell ? { shell } : {}
|
|
7806
|
-
});
|
|
7807
|
-
let permitted = decision.decision === "allow";
|
|
7808
|
-
let source = decision.source;
|
|
7809
|
-
let approvalChoice;
|
|
7810
|
-
if (decision.decision === "ask") {
|
|
7811
|
-
let choice = "deny_once";
|
|
7812
|
-
try {
|
|
7813
|
-
choice = await this.approvalChannel?.requestApproval(request, this.signal) ?? "deny_once";
|
|
7814
|
-
} catch {
|
|
7815
|
-
choice = "cancel";
|
|
7816
|
-
}
|
|
7817
|
-
approvalChoice = choice;
|
|
7818
|
-
if (choice === "allow_session" && request.sessionAllowed)
|
|
7819
|
-
this.approvalCache.set(request, "allow");
|
|
7820
|
-
if (choice === "deny_session") this.approvalCache.set(request, "deny");
|
|
7821
|
-
permitted = choice === "allow_once" || choice === "allow_session" && request.sessionAllowed;
|
|
7822
|
-
source = this.approvalChannel ? "human" : "non_interactive";
|
|
7823
|
-
}
|
|
7824
|
-
auditFields = shell ? {
|
|
7825
|
-
shell: shell.interpreter,
|
|
7826
|
-
cwdDigest: createHash12("sha256").update(shell.cwd).digest("hex"),
|
|
7827
|
-
executableDigest: shell.executableDigest,
|
|
7828
|
-
executionFingerprint: shell.executionFingerprint,
|
|
7829
|
-
...commandAuditSummary(shell.command)
|
|
7830
|
-
} : executionIdentity ? {
|
|
7831
|
-
serverIdentity: executionIdentity.serverIdentity,
|
|
7832
|
-
connectionGeneration: executionIdentity.connectionGeneration,
|
|
7833
|
-
catalogEpoch: executionIdentity.catalogEpoch,
|
|
7834
|
-
toolDigest: executionIdentity.toolDigest
|
|
7835
|
-
} : {};
|
|
7836
|
-
try {
|
|
7837
|
-
await audit("tool.decided", {
|
|
7838
|
-
decision: permitted ? "allow" : "deny",
|
|
7839
|
-
decisionSource: source,
|
|
7840
|
-
...approvalChoice ? { approvalChoice } : {},
|
|
7841
|
-
...hardDenied?.fields ?? {},
|
|
7842
|
-
...auditFields
|
|
7843
|
-
});
|
|
7844
|
-
} catch {
|
|
7845
|
-
return failure(
|
|
7846
|
-
"Audit unavailable; tool was not started",
|
|
7847
|
-
"AUDIT_UNAVAILABLE",
|
|
7848
|
-
"after_user_action",
|
|
7849
|
-
{
|
|
7850
|
-
version: 1,
|
|
7851
|
-
dispatchOutcome: "not_sent",
|
|
7852
|
-
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7853
|
-
}
|
|
7854
|
-
);
|
|
7855
|
-
}
|
|
7856
|
-
if (!permitted || this.signal?.aborted) {
|
|
7857
|
-
const code = this.signal?.aborted ? "ABORTED" : hardDenied?.code ?? "PERMISSION_DENIED";
|
|
7858
|
-
try {
|
|
7859
|
-
await audit("tool.finished", { status: "denied", errorCode: code });
|
|
7860
|
-
} catch {
|
|
7861
|
-
return failure("Audit result unavailable", "AUDIT_UNAVAILABLE", "after_user_action", {
|
|
7862
|
-
version: 1,
|
|
7863
|
-
dispatchOutcome: "not_sent",
|
|
7864
|
-
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7865
|
-
});
|
|
7866
|
-
}
|
|
7867
|
-
return failure(
|
|
7868
|
-
this.signal?.aborted ? "Operation cancelled" : hardDenied?.message ?? "Tool permission denied",
|
|
7869
|
-
code,
|
|
7870
|
-
this.signal?.aborted ? "after_user_action" : hardDenied?.retryPolicy ?? "after_user_action"
|
|
7871
|
-
);
|
|
7872
|
-
}
|
|
7873
|
-
if (shell) {
|
|
7874
|
-
try {
|
|
7875
|
-
const next = tool.approvalScope?.(finalInput, this.rootDir, this.executionCwd);
|
|
7876
|
-
if (next?.executionFingerprint !== shell.executionFingerprint) {
|
|
7877
|
-
if (attempt === 0 && next) {
|
|
7878
|
-
try {
|
|
7879
|
-
await audit("tool.target_changed", { status: "redeciding" });
|
|
7880
|
-
} catch {
|
|
7881
|
-
return failure(
|
|
7882
|
-
"Audit unavailable; tool was not started",
|
|
7883
|
-
"AUDIT_UNAVAILABLE",
|
|
7884
|
-
"after_user_action",
|
|
7885
|
-
{
|
|
7886
|
-
version: 1,
|
|
7887
|
-
dispatchOutcome: "not_sent",
|
|
7888
|
-
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7889
|
-
}
|
|
7890
|
-
);
|
|
7891
|
-
}
|
|
7892
|
-
shell = next;
|
|
7893
|
-
continue;
|
|
7894
|
-
}
|
|
7895
|
-
await audit("tool.finished", { status: "target_changed", errorCode: "TARGET_CHANGED" });
|
|
7896
|
-
return failure(
|
|
7897
|
-
"Execution target changed after approval",
|
|
7898
|
-
"TARGET_CHANGED",
|
|
7899
|
-
"after_user_action"
|
|
7900
|
-
);
|
|
7901
|
-
}
|
|
7902
|
-
} catch {
|
|
7903
|
-
return failure(
|
|
7904
|
-
"Execution target changed after approval",
|
|
7905
|
-
"TARGET_CHANGED",
|
|
7906
|
-
"after_user_action"
|
|
7907
|
-
);
|
|
7908
|
-
}
|
|
7909
|
-
}
|
|
7910
|
-
if (executionIdentity) {
|
|
7911
|
-
try {
|
|
7912
|
-
const nextIdentity = tool.executionIdentity?.(finalInput, this.rootDir);
|
|
7913
|
-
if (!nextIdentity || nextIdentity.serverIdentity !== executionIdentity.serverIdentity || nextIdentity.connectionGeneration !== executionIdentity.connectionGeneration || nextIdentity.catalogEpoch !== executionIdentity.catalogEpoch || nextIdentity.toolDigest !== executionIdentity.toolDigest) {
|
|
7914
|
-
await audit("tool.finished", {
|
|
7915
|
-
status: "identity_changed",
|
|
7916
|
-
errorCode: "MCP_TOOL_CHANGED"
|
|
7917
|
-
});
|
|
7918
|
-
return failure(
|
|
7919
|
-
"Tool execution identity changed after approval",
|
|
7920
|
-
"MCP_TOOL_CHANGED",
|
|
7921
|
-
"after_user_action",
|
|
7922
|
-
{
|
|
7923
|
-
version: 1,
|
|
7924
|
-
dispatchOutcome: "not_sent",
|
|
7925
|
-
hostError: { phase: "validation", code: "MCP_TOOL_CHANGED" }
|
|
7926
|
-
}
|
|
7927
|
-
);
|
|
7928
|
-
}
|
|
7929
|
-
} catch {
|
|
7930
|
-
return failure(
|
|
7931
|
-
"Tool execution identity check failed after approval",
|
|
7932
|
-
"MCP_TOOL_CHANGED",
|
|
7933
|
-
"after_user_action",
|
|
7934
|
-
{
|
|
7935
|
-
version: 1,
|
|
7936
|
-
dispatchOutcome: "not_sent"
|
|
7937
|
-
}
|
|
7938
|
-
);
|
|
7939
|
-
}
|
|
7940
|
-
}
|
|
7941
|
-
break;
|
|
7942
|
-
}
|
|
8291
|
+
const approval = await resolveToolApproval({
|
|
8292
|
+
call,
|
|
8293
|
+
finalInput,
|
|
8294
|
+
tool,
|
|
8295
|
+
shell,
|
|
8296
|
+
executionIdentity,
|
|
8297
|
+
rootDir: this.rootDir,
|
|
8298
|
+
executionCwd: this.executionCwd,
|
|
8299
|
+
mode: this.mode,
|
|
8300
|
+
permissionRules: this.permissionRules,
|
|
8301
|
+
approvalChannel: this.approvalChannel,
|
|
8302
|
+
approvalCache: this.approvalCache,
|
|
8303
|
+
hardDenial: this.hardDenial,
|
|
8304
|
+
neverFailures: this.neverFailures,
|
|
8305
|
+
policy: this.policy,
|
|
8306
|
+
signal: this.signal,
|
|
8307
|
+
audit,
|
|
8308
|
+
failure
|
|
8309
|
+
});
|
|
8310
|
+
if (approval.result) return approval.result;
|
|
8311
|
+
shell = approval.shell;
|
|
8312
|
+
const auditFields = approval.auditFields ?? {};
|
|
7943
8313
|
try {
|
|
7944
8314
|
await audit("tool.started", { status: "started", ...auditFields });
|
|
7945
8315
|
} catch {
|
|
@@ -7999,9 +8369,10 @@ var ToolExecutor = class {
|
|
|
7999
8369
|
{ signal: executionController.signal }
|
|
8000
8370
|
);
|
|
8001
8371
|
}
|
|
8002
|
-
const rawResult = tool.metadata?.managesTimeout ? await tool.execute(finalInput, toolCtx) : await
|
|
8372
|
+
const rawResult = tool.metadata?.managesTimeout ? await tool.execute(finalInput, toolCtx) : await withToolTimeout(
|
|
8003
8373
|
tool.execute(finalInput, toolCtx),
|
|
8004
8374
|
call.name,
|
|
8375
|
+
this.toolTimeoutMs,
|
|
8005
8376
|
executionController
|
|
8006
8377
|
);
|
|
8007
8378
|
if (typeof tool.toExecutionResult === "function") {
|
|
@@ -8062,105 +8433,18 @@ var ToolExecutor = class {
|
|
|
8062
8433
|
dispatchOutcome: outcome
|
|
8063
8434
|
};
|
|
8064
8435
|
}
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
retryPolicy: resultBlock.retryPolicy,
|
|
8077
|
-
message: outputStr
|
|
8078
|
-
});
|
|
8079
|
-
}
|
|
8080
|
-
try {
|
|
8081
|
-
for (const hook of this.hooks.get("tool:after")) {
|
|
8082
|
-
await hook(
|
|
8083
|
-
hookCtx,
|
|
8084
|
-
{ id: call.id, name: call.name, input: finalInput },
|
|
8085
|
-
{ output: outputStr, isError }
|
|
8086
|
-
);
|
|
8087
|
-
}
|
|
8088
|
-
} catch {
|
|
8089
|
-
resultBlock.content = JSON.stringify({
|
|
8090
|
-
code: "HOOK_ERROR",
|
|
8091
|
-
retryPolicy: "after_user_action",
|
|
8092
|
-
message: "Tool finished but result processing failed"
|
|
8093
|
-
});
|
|
8094
|
-
resultBlock.isError = true;
|
|
8095
|
-
resultBlock.errorCode = "HOOK_ERROR";
|
|
8096
|
-
resultBlock.retryPolicy = "after_user_action";
|
|
8097
|
-
resultBlock.execution = {
|
|
8098
|
-
version: 1,
|
|
8099
|
-
dispatchOutcome: resultBlock.execution?.dispatchOutcome ?? "failed",
|
|
8100
|
-
hostError: {
|
|
8101
|
-
phase: "hook",
|
|
8102
|
-
code: "HOOK_ERROR"
|
|
8103
|
-
}
|
|
8104
|
-
};
|
|
8105
|
-
}
|
|
8106
|
-
resultBlock.durationMs = Math.max(0, Date.now() - startedAt);
|
|
8107
|
-
if (resultBlock.isError && resultBlock.retryPolicy === "never") {
|
|
8108
|
-
this.neverFailures.add(
|
|
8109
|
-
createHash12("sha256").update(stableJsonStringify([call.name, finalInput])).digest("hex")
|
|
8110
|
-
);
|
|
8111
|
-
}
|
|
8112
|
-
try {
|
|
8113
|
-
await audit("tool.finished", {
|
|
8114
|
-
status: resultBlock.isError ? "failed" : "completed",
|
|
8115
|
-
...resultBlock.errorCode ? { errorCode: resultBlock.errorCode } : {},
|
|
8116
|
-
durationMs: resultBlock.durationMs
|
|
8117
|
-
});
|
|
8118
|
-
} catch {
|
|
8119
|
-
resultBlock.isError = true;
|
|
8120
|
-
resultBlock.errorCode = "OUTCOME_UNKNOWN";
|
|
8121
|
-
resultBlock.retryPolicy = "after_user_action";
|
|
8122
|
-
resultBlock.content = JSON.stringify({
|
|
8123
|
-
code: "OUTCOME_UNKNOWN",
|
|
8124
|
-
retryPolicy: "after_user_action",
|
|
8125
|
-
message: "Tool outcome is unknown because the audit result could not be written"
|
|
8126
|
-
});
|
|
8127
|
-
resultBlock.execution = {
|
|
8128
|
-
version: 1,
|
|
8129
|
-
dispatchOutcome: resultBlock.execution?.dispatchOutcome ?? "succeeded",
|
|
8130
|
-
hostError: {
|
|
8131
|
-
phase: "audit",
|
|
8132
|
-
code: "AUDIT_UNAVAILABLE"
|
|
8133
|
-
}
|
|
8134
|
-
};
|
|
8135
|
-
return resultBlock;
|
|
8136
|
-
}
|
|
8137
|
-
await this.onToolResult?.(tool, finalInput, resultBlock);
|
|
8138
|
-
return resultBlock;
|
|
8139
|
-
}
|
|
8140
|
-
/**
|
|
8141
|
-
* 给工具执行加超时护栏:Promise.race 在超时后reject,循环拿到 isError 结果继续运转,
|
|
8142
|
-
* 不会因某个异步工具(如未来接入的网络/exec 工具)挂起而永久卡死。
|
|
8143
|
-
*
|
|
8144
|
-
* 局限:同步 CPU 密集型实现会阻塞事件循环,timer 同样无法触发;
|
|
8145
|
-
* 因此内置工具仍需避免执行用户可控的同步高复杂度计算。
|
|
8146
|
-
*/
|
|
8147
|
-
withTimeout(promise, toolName, executionController) {
|
|
8148
|
-
let timer;
|
|
8149
|
-
const timeout = new Promise((_, reject) => {
|
|
8150
|
-
timer = setTimeout(() => {
|
|
8151
|
-
const error = new KapibalaError(
|
|
8152
|
-
`Tool '${toolName}' timed out after ${this.toolTimeoutMs}ms`,
|
|
8153
|
-
{
|
|
8154
|
-
code: "OUTCOME_UNKNOWN",
|
|
8155
|
-
retryPolicy: "after_user_action",
|
|
8156
|
-
safeMessage: "Tool timed out; completion of side effects is unknown"
|
|
8157
|
-
}
|
|
8158
|
-
);
|
|
8159
|
-
reject(error);
|
|
8160
|
-
executionController.abort(error);
|
|
8161
|
-
}, this.toolTimeoutMs);
|
|
8436
|
+
return closeToolResult({
|
|
8437
|
+
call,
|
|
8438
|
+
tool,
|
|
8439
|
+
finalInput,
|
|
8440
|
+
hookCtx,
|
|
8441
|
+
hooks: this.hooks,
|
|
8442
|
+
neverFailures: this.neverFailures,
|
|
8443
|
+
audit,
|
|
8444
|
+
onToolResult: this.onToolResult,
|
|
8445
|
+
startedAt,
|
|
8446
|
+
output: { content: outputStr, isError, errorCode, retryPolicy, execution }
|
|
8162
8447
|
});
|
|
8163
|
-
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
8164
8448
|
}
|
|
8165
8449
|
/**
|
|
8166
8450
|
* 构建当前会话在派生时刻的父权限策略快照。
|
|
@@ -8759,7 +9043,7 @@ var OpenAICompatibleProvider = class {
|
|
|
8759
9043
|
};
|
|
8760
9044
|
|
|
8761
9045
|
// src/models/anthropic/index.ts
|
|
8762
|
-
import { createHash as
|
|
9046
|
+
import { createHash as createHash14 } from "crypto";
|
|
8763
9047
|
function invalidResponse2() {
|
|
8764
9048
|
throw new ModelError("Invalid Anthropic response state", {
|
|
8765
9049
|
code: "MODEL_INVALID_RESPONSE",
|
|
@@ -8774,7 +9058,7 @@ function wireToolId(id) {
|
|
|
8774
9058
|
stage: "request",
|
|
8775
9059
|
retryable: false
|
|
8776
9060
|
});
|
|
8777
|
-
return /^[A-Za-z0-9_-]{1,64}$/.test(id) ? id : `call_${
|
|
9061
|
+
return /^[A-Za-z0-9_-]{1,64}$/.test(id) ? id : `call_${createHash14("sha256").update(id).digest("hex").slice(0, 48)}`;
|
|
8778
9062
|
}
|
|
8779
9063
|
var AnthropicProvider = class {
|
|
8780
9064
|
name = "anthropic";
|
|
@@ -9140,7 +9424,7 @@ var AnthropicProvider = class {
|
|
|
9140
9424
|
};
|
|
9141
9425
|
|
|
9142
9426
|
// src/models/openai-responses/index.ts
|
|
9143
|
-
import { createHash as
|
|
9427
|
+
import { createHash as createHash15 } from "crypto";
|
|
9144
9428
|
function invalid(message = "Invalid Responses output Item") {
|
|
9145
9429
|
throw new ModelError(message, {
|
|
9146
9430
|
code: "MODEL_INVALID_RESPONSE",
|
|
@@ -9161,7 +9445,7 @@ function wireCallId(id) {
|
|
|
9161
9445
|
stage: "request",
|
|
9162
9446
|
retryable: false
|
|
9163
9447
|
});
|
|
9164
|
-
return /^[A-Za-z0-9_-]{1,64}$/.test(id) ? id : `call_${
|
|
9448
|
+
return /^[A-Za-z0-9_-]{1,64}$/.test(id) ? id : `call_${createHash15("sha256").update(id).digest("hex").slice(0, 48)}`;
|
|
9165
9449
|
}
|
|
9166
9450
|
function validateItem(item, final) {
|
|
9167
9451
|
if (!item || typeof item !== "object" || !nonempty(item.id)) invalid();
|
|
@@ -9858,13 +10142,99 @@ Follow these key behavioral rules:
|
|
|
9858
10142
|
}
|
|
9859
10143
|
return lines.join("\n");
|
|
9860
10144
|
}
|
|
9861
|
-
getL3Environment() {
|
|
9862
|
-
const platform = os6.platform();
|
|
9863
|
-
return `# Environment Context:
|
|
9864
|
-
- Current Working Directory (Workspace Root): ${this.rootDir}
|
|
9865
|
-
- Operating System: ${platform}`;
|
|
10145
|
+
getL3Environment() {
|
|
10146
|
+
const platform = os6.platform();
|
|
10147
|
+
return `# Environment Context:
|
|
10148
|
+
- Current Working Directory (Workspace Root): ${this.rootDir}
|
|
10149
|
+
- Operating System: ${platform}`;
|
|
10150
|
+
}
|
|
10151
|
+
};
|
|
10152
|
+
|
|
10153
|
+
// src/runtime/loop/hook-integrity.ts
|
|
10154
|
+
function assertHookToolDefinitionsIntact(request, snapshot) {
|
|
10155
|
+
if (!request.tools) return;
|
|
10156
|
+
for (const t of request.tools) {
|
|
10157
|
+
const original = snapshot.get(t.name);
|
|
10158
|
+
if (!original) {
|
|
10159
|
+
throw new ModelError(`Hook injected tool '${t.name}' not present in snapshot`, {
|
|
10160
|
+
stage: "request",
|
|
10161
|
+
retryable: false
|
|
10162
|
+
});
|
|
10163
|
+
}
|
|
10164
|
+
if (t.description !== original.description || JSON.stringify(t.parameters) !== JSON.stringify(original.parameters)) {
|
|
10165
|
+
throw new ModelError(
|
|
10166
|
+
`Hook mutated tool definition for '${t.name}' (description or parameters drifted from snapshot)`,
|
|
10167
|
+
{
|
|
10168
|
+
stage: "request",
|
|
10169
|
+
retryable: false
|
|
10170
|
+
}
|
|
10171
|
+
);
|
|
10172
|
+
}
|
|
10173
|
+
}
|
|
10174
|
+
}
|
|
10175
|
+
function snapshotAssistantIntegrity(message) {
|
|
10176
|
+
return {
|
|
10177
|
+
identity: stableSerialize({ ...message, content: void 0 }),
|
|
10178
|
+
originalTools: stableSerialize(
|
|
10179
|
+
message.content.filter((block) => block.type === "tool_use" || block.type === "tool_result")
|
|
10180
|
+
),
|
|
10181
|
+
originalContent: stableSerialize(message.content),
|
|
10182
|
+
privateOutput: message.content.some(
|
|
10183
|
+
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.origin || block.signature)
|
|
10184
|
+
),
|
|
10185
|
+
hasToolCalls: message.content.some((block) => block.type === "tool_use")
|
|
10186
|
+
};
|
|
10187
|
+
}
|
|
10188
|
+
function assertAssistantMessageUntampered(message, snapshot) {
|
|
10189
|
+
if (snapshot.identity !== stableSerialize({ ...message, content: void 0 }))
|
|
10190
|
+
throw new ModelError("Hook changed assistant message identity", {
|
|
10191
|
+
stage: "request",
|
|
10192
|
+
retryable: false
|
|
10193
|
+
});
|
|
10194
|
+
if (snapshot.originalTools !== stableSerialize(
|
|
10195
|
+
message.content.filter((block) => block.type === "tool_use" || block.type === "tool_result")
|
|
10196
|
+
))
|
|
10197
|
+
throw new ModelError("Hook changed validated tool model output", {
|
|
10198
|
+
stage: "request",
|
|
10199
|
+
retryable: false
|
|
10200
|
+
});
|
|
10201
|
+
if (snapshot.originalContent !== stableSerialize(message.content)) {
|
|
10202
|
+
if (snapshot.privateOutput || message.content.some(
|
|
10203
|
+
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.origin || block.signature)
|
|
10204
|
+
))
|
|
10205
|
+
throw new ModelError("Hook changed signed or private model output", {
|
|
10206
|
+
stage: "request",
|
|
10207
|
+
retryable: false
|
|
10208
|
+
});
|
|
10209
|
+
if (snapshot.hasToolCalls)
|
|
10210
|
+
throw new ModelError("Hook changed validated tool model output", {
|
|
10211
|
+
stage: "request",
|
|
10212
|
+
retryable: false
|
|
10213
|
+
});
|
|
10214
|
+
for (const block of message.content)
|
|
10215
|
+
if (block.type === "text" || block.type === "tool_use") block.protocolMeta = void 0;
|
|
10216
|
+
}
|
|
10217
|
+
}
|
|
10218
|
+
|
|
10219
|
+
// src/runtime/loop/tool-call-validation.ts
|
|
10220
|
+
function assertToolCallsExecutable(history, toolCalls) {
|
|
10221
|
+
const knownCalls = new Set(
|
|
10222
|
+
history.flatMap(
|
|
10223
|
+
(message) => message.content.filter((block) => block.type === "tool_use").map((block) => block.id)
|
|
10224
|
+
)
|
|
10225
|
+
);
|
|
10226
|
+
for (const call of toolCalls) {
|
|
10227
|
+
if (!call.name?.trim() || !call.input || typeof call.input !== "object" || Array.isArray(call.input))
|
|
10228
|
+
throw new ModelError("Invalid tool call arguments or name; no tools executed", {
|
|
10229
|
+
code: "MODEL_INVALID_RESPONSE",
|
|
10230
|
+
stage: "stream",
|
|
10231
|
+
retryable: false
|
|
10232
|
+
});
|
|
10233
|
+
if (!call.id?.trim() || knownCalls.has(call.id))
|
|
10234
|
+
throw new ModelError("Duplicate tool call ID; no tools executed");
|
|
10235
|
+
knownCalls.add(call.id);
|
|
9866
10236
|
}
|
|
9867
|
-
}
|
|
10237
|
+
}
|
|
9868
10238
|
|
|
9869
10239
|
// src/runtime/loop/index.ts
|
|
9870
10240
|
var AgentLoop = class {
|
|
@@ -9948,26 +10318,7 @@ var AgentLoop = class {
|
|
|
9948
10318
|
request = modified;
|
|
9949
10319
|
}
|
|
9950
10320
|
}
|
|
9951
|
-
|
|
9952
|
-
for (const t of request.tools) {
|
|
9953
|
-
const original = toolSnapshot.get(t.name);
|
|
9954
|
-
if (!original) {
|
|
9955
|
-
throw new ModelError(`Hook injected tool '${t.name}' not present in snapshot`, {
|
|
9956
|
-
stage: "request",
|
|
9957
|
-
retryable: false
|
|
9958
|
-
});
|
|
9959
|
-
}
|
|
9960
|
-
if (t.description !== original.description || JSON.stringify(t.parameters) !== JSON.stringify(original.parameters)) {
|
|
9961
|
-
throw new ModelError(
|
|
9962
|
-
`Hook mutated tool definition for '${t.name}' (description or parameters drifted from snapshot)`,
|
|
9963
|
-
{
|
|
9964
|
-
stage: "request",
|
|
9965
|
-
retryable: false
|
|
9966
|
-
}
|
|
9967
|
-
);
|
|
9968
|
-
}
|
|
9969
|
-
}
|
|
9970
|
-
}
|
|
10321
|
+
assertHookToolDefinitionsIntact(request, toolSnapshot);
|
|
9971
10322
|
if (this.prepareRequest) request = yield* this.prepareRequest(request, history);
|
|
9972
10323
|
yield {
|
|
9973
10324
|
type: "step_log",
|
|
@@ -10118,22 +10469,7 @@ var AgentLoop = class {
|
|
|
10118
10469
|
stage: "stream",
|
|
10119
10470
|
retryable: false
|
|
10120
10471
|
});
|
|
10121
|
-
|
|
10122
|
-
history.flatMap(
|
|
10123
|
-
(message) => message.content.filter((block) => block.type === "tool_use").map((block) => block.id)
|
|
10124
|
-
)
|
|
10125
|
-
);
|
|
10126
|
-
for (const call of toolCalls) {
|
|
10127
|
-
if (!call.name?.trim() || !call.input || typeof call.input !== "object" || Array.isArray(call.input))
|
|
10128
|
-
throw new ModelError("Invalid tool call arguments or name; no tools executed", {
|
|
10129
|
-
code: "MODEL_INVALID_RESPONSE",
|
|
10130
|
-
stage: "stream",
|
|
10131
|
-
retryable: false
|
|
10132
|
-
});
|
|
10133
|
-
if (!call.id?.trim() || knownCalls.has(call.id))
|
|
10134
|
-
throw new ModelError("Duplicate tool call ID; no tools executed");
|
|
10135
|
-
knownCalls.add(call.id);
|
|
10136
|
-
}
|
|
10472
|
+
assertToolCallsExecutable(history, toolCalls);
|
|
10137
10473
|
break;
|
|
10138
10474
|
} catch (error) {
|
|
10139
10475
|
if (!(error instanceof ContextOverflowError) || outputSeen || accumulatedText || accumulatedThinking || toolCalls.length || overflowRetried || !this.prepareRequest || this.signal?.aborted)
|
|
@@ -10246,47 +10582,9 @@ var AgentLoop = class {
|
|
|
10246
10582
|
timestamp: Date.now()
|
|
10247
10583
|
};
|
|
10248
10584
|
validateProtocolContent(assistantMessage.content);
|
|
10249
|
-
const
|
|
10250
|
-
const identity = stableSerialize({ ...assistantMessage, content: void 0 });
|
|
10251
|
-
const originalTools = stableSerialize(
|
|
10252
|
-
assistantMessage.content.filter(
|
|
10253
|
-
(block) => block.type === "tool_use" || block.type === "tool_result"
|
|
10254
|
-
)
|
|
10255
|
-
);
|
|
10256
|
-
const privateOutput = assistantMessage.content.some(
|
|
10257
|
-
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.origin || block.signature)
|
|
10258
|
-
);
|
|
10585
|
+
const integrity = snapshotAssistantIntegrity(assistantMessage);
|
|
10259
10586
|
await this.hooks.emit("model:after", hookCtx, { message: assistantMessage });
|
|
10260
|
-
|
|
10261
|
-
throw new ModelError("Hook changed assistant message identity", {
|
|
10262
|
-
stage: "request",
|
|
10263
|
-
retryable: false
|
|
10264
|
-
});
|
|
10265
|
-
if (originalTools !== stableSerialize(
|
|
10266
|
-
assistantMessage.content.filter(
|
|
10267
|
-
(block) => block.type === "tool_use" || block.type === "tool_result"
|
|
10268
|
-
)
|
|
10269
|
-
))
|
|
10270
|
-
throw new ModelError("Hook changed validated tool model output", {
|
|
10271
|
-
stage: "request",
|
|
10272
|
-
retryable: false
|
|
10273
|
-
});
|
|
10274
|
-
if (originalContent !== stableSerialize(assistantMessage.content)) {
|
|
10275
|
-
if (privateOutput || assistantMessage.content.some(
|
|
10276
|
-
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.origin || block.signature)
|
|
10277
|
-
))
|
|
10278
|
-
throw new ModelError("Hook changed signed or private model output", {
|
|
10279
|
-
stage: "request",
|
|
10280
|
-
retryable: false
|
|
10281
|
-
});
|
|
10282
|
-
if (toolCalls.length)
|
|
10283
|
-
throw new ModelError("Hook changed validated tool model output", {
|
|
10284
|
-
stage: "request",
|
|
10285
|
-
retryable: false
|
|
10286
|
-
});
|
|
10287
|
-
for (const block of assistantMessage.content)
|
|
10288
|
-
if (block.type === "text" || block.type === "tool_use") block.protocolMeta = void 0;
|
|
10289
|
-
}
|
|
10587
|
+
assertAssistantMessageUntampered(assistantMessage, integrity);
|
|
10290
10588
|
validateProtocolContent(assistantMessage.content);
|
|
10291
10589
|
if (toolCalls.length === 0) {
|
|
10292
10590
|
history.push(assistantMessage);
|
|
@@ -11055,7 +11353,6 @@ var AgentRuntime = class {
|
|
|
11055
11353
|
task.status = "failed";
|
|
11056
11354
|
}
|
|
11057
11355
|
task.error = runError.message;
|
|
11058
|
-
await this.cascadeCancelChildren(task.agentId, "Parent task failed");
|
|
11059
11356
|
} finally {
|
|
11060
11357
|
this.runnerContexts.delete(task.agentId);
|
|
11061
11358
|
this.clearTaskTimer(task.agentId);
|
|
@@ -11064,6 +11361,9 @@ var AgentRuntime = class {
|
|
|
11064
11361
|
task.status = "failed";
|
|
11065
11362
|
task.error = runError?.message ?? "Unknown execution failure";
|
|
11066
11363
|
}
|
|
11364
|
+
if (task.status === "failed") {
|
|
11365
|
+
await this.cascadeCancelChildren(task.agentId, task.error ?? "Parent task failed");
|
|
11366
|
+
}
|
|
11067
11367
|
task.finishedAt = Date.now();
|
|
11068
11368
|
this.releaseTaskSlotOnce(task);
|
|
11069
11369
|
this.outbox.enqueueFromTask(task);
|
|
@@ -11670,6 +11970,274 @@ function cancellableGenerator(create, external) {
|
|
|
11670
11970
|
};
|
|
11671
11971
|
}
|
|
11672
11972
|
|
|
11973
|
+
// src/context/session/persistence.ts
|
|
11974
|
+
async function persistAssistantMessage(event, store, markUnknownUsage) {
|
|
11975
|
+
if (store) await store.append(event.message);
|
|
11976
|
+
if (!event.usage) markUnknownUsage();
|
|
11977
|
+
}
|
|
11978
|
+
async function persistToolMessages(messages, store) {
|
|
11979
|
+
if (!store) return;
|
|
11980
|
+
for (const message of messages) await store.append(message);
|
|
11981
|
+
}
|
|
11982
|
+
function buildInterruptedToolMessage(assistantEvent) {
|
|
11983
|
+
return {
|
|
11984
|
+
id: createMessageId(),
|
|
11985
|
+
role: "tool",
|
|
11986
|
+
content: assistantEvent.message.content.filter((block) => block.type === "tool_use").map((toolUse) => ({
|
|
11987
|
+
type: "tool_result",
|
|
11988
|
+
toolUseId: toolUse.id,
|
|
11989
|
+
content: JSON.stringify({
|
|
11990
|
+
code: "OUTCOME_UNKNOWN",
|
|
11991
|
+
retryPolicy: "after_user_action",
|
|
11992
|
+
message: "Tool execution was interrupted; outcome is unknown"
|
|
11993
|
+
}),
|
|
11994
|
+
isError: true,
|
|
11995
|
+
errorCode: "OUTCOME_UNKNOWN",
|
|
11996
|
+
retryPolicy: "after_user_action"
|
|
11997
|
+
})),
|
|
11998
|
+
timestamp: Date.now()
|
|
11999
|
+
};
|
|
12000
|
+
}
|
|
12001
|
+
function findClosedToolMessages(history, assistantEvent) {
|
|
12002
|
+
const assistantIndex = history.lastIndexOf(assistantEvent.message);
|
|
12003
|
+
if (assistantIndex < 0) return [];
|
|
12004
|
+
const expected = new Set(
|
|
12005
|
+
assistantEvent.message.content.filter((block) => block.type === "tool_use").map((toolUse) => toolUse.id)
|
|
12006
|
+
);
|
|
12007
|
+
const messages = [];
|
|
12008
|
+
for (let index2 = assistantIndex + 1; index2 < history.length; index2++) {
|
|
12009
|
+
const message = history[index2];
|
|
12010
|
+
if (message.role !== "tool") break;
|
|
12011
|
+
messages.push(message);
|
|
12012
|
+
for (const block of message.content) {
|
|
12013
|
+
if (block.type === "tool_result") expected.delete(block.toolUseId);
|
|
12014
|
+
}
|
|
12015
|
+
}
|
|
12016
|
+
return expected.size === 0 ? messages : [];
|
|
12017
|
+
}
|
|
12018
|
+
|
|
12019
|
+
// src/context/session/request-prepare.ts
|
|
12020
|
+
function verifyPrivateBlockIntegrity(projected, sourceView) {
|
|
12021
|
+
for (const message of projected) {
|
|
12022
|
+
const original = sourceView.find((source) => source.id === message.id);
|
|
12023
|
+
const privateBag = /* @__PURE__ */ new Map();
|
|
12024
|
+
const blockBag = /* @__PURE__ */ new Map();
|
|
12025
|
+
for (const block of original?.content ?? []) {
|
|
12026
|
+
const serialized = stableSerialize(block);
|
|
12027
|
+
if (block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && block.origin)
|
|
12028
|
+
privateBag.set(serialized, (privateBag.get(serialized) ?? 0) + 1);
|
|
12029
|
+
blockBag.set(serialized, (blockBag.get(serialized) ?? 0) + 1);
|
|
12030
|
+
}
|
|
12031
|
+
const take = (bag, serialized) => {
|
|
12032
|
+
const count = bag.get(serialized) ?? 0;
|
|
12033
|
+
if (!count) return false;
|
|
12034
|
+
if (count === 1) bag.delete(serialized);
|
|
12035
|
+
else bag.set(serialized, count - 1);
|
|
12036
|
+
return true;
|
|
12037
|
+
};
|
|
12038
|
+
for (const block of message.content) {
|
|
12039
|
+
if (block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && block.origin) {
|
|
12040
|
+
if (!original || !take(privateBag, stableSerialize(block)))
|
|
12041
|
+
throw new ModelError("Hook changed signed or private request provenance", {
|
|
12042
|
+
stage: "request",
|
|
12043
|
+
retryable: false
|
|
12044
|
+
});
|
|
12045
|
+
}
|
|
12046
|
+
if (block.type !== "text" && block.type !== "tool_use") continue;
|
|
12047
|
+
if (block.protocolMeta && !take(blockBag, stableSerialize(block)))
|
|
12048
|
+
block.protocolMeta = void 0;
|
|
12049
|
+
}
|
|
12050
|
+
}
|
|
12051
|
+
}
|
|
12052
|
+
function assertSignedContinuationStable(check) {
|
|
12053
|
+
const { candidate, previousRequest, history, runId, nativeBinding, projectHistory } = check;
|
|
12054
|
+
const frozen = Boolean(
|
|
12055
|
+
previousRequest && history.some(
|
|
12056
|
+
(message) => message.interactionId === runId && message.content.some(
|
|
12057
|
+
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.signature && block.origin?.runId === runId)
|
|
12058
|
+
)
|
|
12059
|
+
)
|
|
12060
|
+
);
|
|
12061
|
+
if (frozen) {
|
|
12062
|
+
const generated = nativeBinding ? projectProtocolHistory(
|
|
12063
|
+
projectHistory(history).filter(
|
|
12064
|
+
(message) => message.interactionId === runId && message.role !== "user"
|
|
12065
|
+
),
|
|
12066
|
+
nativeBinding,
|
|
12067
|
+
candidate.context
|
|
12068
|
+
) : [];
|
|
12069
|
+
for (const original of generated) {
|
|
12070
|
+
const matches = candidate.messages.filter((message) => message.id === original.id);
|
|
12071
|
+
if (matches.length !== 1 || stableSerialize(matches[0]) !== stableSerialize(original))
|
|
12072
|
+
throw new ModelError("Signed continuation prefix or tool chain changed", {
|
|
12073
|
+
stage: "request",
|
|
12074
|
+
retryable: false
|
|
12075
|
+
});
|
|
12076
|
+
}
|
|
12077
|
+
}
|
|
12078
|
+
if (frozen && previousRequest && (stableSerialize(candidate.messages.slice(0, previousRequest.messages.length)) !== stableSerialize(previousRequest.messages) || candidate.systemPrompt !== previousRequest.systemPrompt || stableSerialize(candidate.tools) !== stableSerialize(previousRequest.tools)))
|
|
12079
|
+
throw new ModelError("Signed continuation prefix changed", {
|
|
12080
|
+
stage: "request",
|
|
12081
|
+
retryable: false
|
|
12082
|
+
});
|
|
12083
|
+
return frozen;
|
|
12084
|
+
}
|
|
12085
|
+
|
|
12086
|
+
// src/context/session/skills.ts
|
|
12087
|
+
function createSkillResourceAuditor(eventLogger, sessionId) {
|
|
12088
|
+
return async (source, input, page) => {
|
|
12089
|
+
try {
|
|
12090
|
+
await eventLogger.recordAudit({
|
|
12091
|
+
level: "info",
|
|
12092
|
+
event: "skill.resource_read",
|
|
12093
|
+
sessionId,
|
|
12094
|
+
runId: source.runId,
|
|
12095
|
+
fields: {
|
|
12096
|
+
skillName: source.name,
|
|
12097
|
+
contentDigest: source.contentDigest,
|
|
12098
|
+
invocationId: source.invocationId,
|
|
12099
|
+
pathDigest: skillDigest(input.path),
|
|
12100
|
+
resourceDigest: skillDigest(page.text),
|
|
12101
|
+
outputBytes: Buffer.byteLength(page.text),
|
|
12102
|
+
outputTruncated: page.truncated,
|
|
12103
|
+
offset: input.offset,
|
|
12104
|
+
startLine: input.startLine,
|
|
12105
|
+
endLine: input.endLine
|
|
12106
|
+
}
|
|
12107
|
+
});
|
|
12108
|
+
} catch {
|
|
12109
|
+
throw new KapibalaError("Skill resource audit unavailable", {
|
|
12110
|
+
code: "AUDIT_UNAVAILABLE",
|
|
12111
|
+
retryPolicy: "after_user_action",
|
|
12112
|
+
safeMessage: "\u6280\u80FD\u8D44\u6E90\u8BFB\u53D6\u5BA1\u8BA1\u4E0D\u53EF\u7528\uFF0C\u672C\u8F6E\u505C\u6B62"
|
|
12113
|
+
});
|
|
12114
|
+
}
|
|
12115
|
+
};
|
|
12116
|
+
}
|
|
12117
|
+
async function commitSkillEvents(deps) {
|
|
12118
|
+
const { skillRuntime, eventLogger, sessionId, runId, persistence } = deps;
|
|
12119
|
+
if (!skillRuntime) return [];
|
|
12120
|
+
return skillRuntime.commit(async (source, messageId) => {
|
|
12121
|
+
try {
|
|
12122
|
+
await eventLogger.recordAudit({
|
|
12123
|
+
level: "info",
|
|
12124
|
+
event: "skill.loaded",
|
|
12125
|
+
sessionId,
|
|
12126
|
+
runId,
|
|
12127
|
+
fields: {
|
|
12128
|
+
skillName: source.name,
|
|
12129
|
+
skillScope: source.scope,
|
|
12130
|
+
contentDigest: source.contentDigest,
|
|
12131
|
+
renderedDigest: source.renderedDigest,
|
|
12132
|
+
pathDigest: skillDigest(source.filePath),
|
|
12133
|
+
caller: source.caller,
|
|
12134
|
+
invocationId: source.invocationId,
|
|
12135
|
+
messageId
|
|
12136
|
+
}
|
|
12137
|
+
});
|
|
12138
|
+
} catch {
|
|
12139
|
+
throw new KapibalaError("Skill commit audit unavailable", {
|
|
12140
|
+
code: "AUDIT_UNAVAILABLE",
|
|
12141
|
+
retryPolicy: "after_user_action",
|
|
12142
|
+
safeMessage: "\u6280\u80FD\u63D0\u4EA4\u5BA1\u8BA1\u4E0D\u53EF\u7528\uFF0C\u672C\u8F6E\u505C\u6B62"
|
|
12143
|
+
});
|
|
12144
|
+
}
|
|
12145
|
+
}, persistence);
|
|
12146
|
+
}
|
|
12147
|
+
async function authorizeExplicitSkill(deps) {
|
|
12148
|
+
const {
|
|
12149
|
+
name,
|
|
12150
|
+
runId,
|
|
12151
|
+
signal,
|
|
12152
|
+
rootDir,
|
|
12153
|
+
mode,
|
|
12154
|
+
permissionRules,
|
|
12155
|
+
approvalChannel,
|
|
12156
|
+
approvalCache,
|
|
12157
|
+
eventLogger,
|
|
12158
|
+
sessionId
|
|
12159
|
+
} = deps;
|
|
12160
|
+
const request = {
|
|
12161
|
+
toolName: "load_skill",
|
|
12162
|
+
capabilities: ["fs:read"],
|
|
12163
|
+
input: { name },
|
|
12164
|
+
rootDir,
|
|
12165
|
+
sessionAllowed: true
|
|
12166
|
+
};
|
|
12167
|
+
const cached = approvalCache.get(request);
|
|
12168
|
+
const decision = new PermissionPolicy().decide({
|
|
12169
|
+
toolName: request.toolName,
|
|
12170
|
+
capabilities: request.capabilities,
|
|
12171
|
+
mode,
|
|
12172
|
+
rules: permissionRules,
|
|
12173
|
+
cachedApproval: cached === "allow",
|
|
12174
|
+
cachedDenial: cached === "deny"
|
|
12175
|
+
});
|
|
12176
|
+
let permitted = decision.decision === "allow";
|
|
12177
|
+
let approvalChoice;
|
|
12178
|
+
if (decision.decision === "ask") {
|
|
12179
|
+
try {
|
|
12180
|
+
approvalChoice = await approvalChannel?.requestApproval(request, signal) ?? "deny_once";
|
|
12181
|
+
} catch {
|
|
12182
|
+
approvalChoice = "cancel";
|
|
12183
|
+
}
|
|
12184
|
+
permitted = approvalChoice === "allow_once" || approvalChoice === "allow_session";
|
|
12185
|
+
if (approvalChoice === "allow_session") approvalCache.set(request, "allow");
|
|
12186
|
+
if (approvalChoice === "deny_session") approvalCache.set(request, "deny");
|
|
12187
|
+
}
|
|
12188
|
+
await eventLogger.recordAudit({
|
|
12189
|
+
level: "info",
|
|
12190
|
+
event: "skill.decided",
|
|
12191
|
+
sessionId,
|
|
12192
|
+
runId,
|
|
12193
|
+
fields: {
|
|
12194
|
+
skillName: name,
|
|
12195
|
+
caller: "user",
|
|
12196
|
+
decision: permitted && !signal?.aborted ? "allow" : "deny",
|
|
12197
|
+
decisionSource: decision.decision === "ask" ? approvalChannel ? "human" : "non_interactive" : decision.source,
|
|
12198
|
+
approvalChoice
|
|
12199
|
+
}
|
|
12200
|
+
});
|
|
12201
|
+
if (!permitted) throw skillFailure("SKILL_INVOCATION_FORBIDDEN", "\u5BBF\u4E3B\u6743\u9650\u7981\u6B62\u663E\u5F0F\u6280\u80FD\u8BFB\u53D6");
|
|
12202
|
+
signal?.throwIfAborted();
|
|
12203
|
+
}
|
|
12204
|
+
|
|
12205
|
+
// src/context/session/usage.ts
|
|
12206
|
+
function recordUsage(attemptId, usage, accountedAttempts, usageStats) {
|
|
12207
|
+
if (accountedAttempts.has(attemptId)) return "duplicate";
|
|
12208
|
+
if (!usage || ![usage.promptTokens, usage.completionTokens, usage.totalTokens].every(
|
|
12209
|
+
(n) => Number.isSafeInteger(n) && n >= 0
|
|
12210
|
+
))
|
|
12211
|
+
return "unknown";
|
|
12212
|
+
accountedAttempts.add(attemptId);
|
|
12213
|
+
usageStats.promptTokens += usage.promptTokens;
|
|
12214
|
+
usageStats.completionTokens += usage.completionTokens;
|
|
12215
|
+
usageStats.totalTokens += usage.totalTokens;
|
|
12216
|
+
return "recorded";
|
|
12217
|
+
}
|
|
12218
|
+
function recordSummaryUsage(attemptId, usage, summaryAttempts, summaryUsage) {
|
|
12219
|
+
if (summaryAttempts.has(attemptId)) return "duplicate";
|
|
12220
|
+
if (!usage || ![usage.promptTokens, usage.completionTokens, usage.totalTokens].every(
|
|
12221
|
+
(n) => Number.isSafeInteger(n) && n >= 0
|
|
12222
|
+
))
|
|
12223
|
+
return "unknown";
|
|
12224
|
+
summaryAttempts.add(attemptId);
|
|
12225
|
+
summaryUsage.promptTokens += usage.promptTokens;
|
|
12226
|
+
summaryUsage.completionTokens += usage.completionTokens;
|
|
12227
|
+
summaryUsage.totalTokens += usage.totalTokens;
|
|
12228
|
+
return "recorded";
|
|
12229
|
+
}
|
|
12230
|
+
function computeContextUsage(profile, usedTokens, estimatedUsage = false) {
|
|
12231
|
+
const contextWindow = resolveContextWindow(profile.contextWindow);
|
|
12232
|
+
return {
|
|
12233
|
+
usedTokens,
|
|
12234
|
+
limitTokens: contextWindow.tokens,
|
|
12235
|
+
percent: usedTokens === void 0 ? void 0 : usedTokens / contextWindow.tokens * 100,
|
|
12236
|
+
estimatedLimit: contextWindow.estimated,
|
|
12237
|
+
...estimatedUsage ? { estimatedUsage: true } : {}
|
|
12238
|
+
};
|
|
12239
|
+
}
|
|
12240
|
+
|
|
11673
12241
|
// src/context/session/index.ts
|
|
11674
12242
|
var AgentSession = class {
|
|
11675
12243
|
skillRegistry;
|
|
@@ -11755,40 +12323,26 @@ var AgentSession = class {
|
|
|
11755
12323
|
this.projectKey = resolvedWorkspace.projectKey;
|
|
11756
12324
|
this.sandbox = config.sandbox ?? new PathSandbox({ workspaceRoot: this.workspaceRoot });
|
|
11757
12325
|
this.leaseCoordinator = config.leaseCoordinator ?? new WorkspaceLeaseCoordinator();
|
|
12326
|
+
this.userInstructionsPath = config.userInstructionsPath;
|
|
12327
|
+
const sinks = config.eventLogger ? void 0 : createDefaultLogSinks(config.loggingDirectory);
|
|
12328
|
+
const eventLogger = config.eventLogger ?? new StructuredLogger({
|
|
12329
|
+
operationSink: sinks.operationSink,
|
|
12330
|
+
auditSink: sinks.auditSink,
|
|
12331
|
+
debug: config.logger !== void 0,
|
|
12332
|
+
onDiagnostic: config.onDiagnostic ?? config.logger
|
|
12333
|
+
});
|
|
12334
|
+
this.eventLogger = {
|
|
12335
|
+
record: (input) => eventLogger.record({ ...input, conversationId: this.persistentId }),
|
|
12336
|
+
recordAudit: (input) => eventLogger.recordAudit({ ...input, conversationId: this.persistentId })
|
|
12337
|
+
};
|
|
12338
|
+
this.auditRecoveryDirectory = config.auditRecoveryDirectory ?? sinks?.auditSink.directory;
|
|
11758
12339
|
this.tools = new ToolRegistry();
|
|
11759
12340
|
if (config.skills) {
|
|
11760
12341
|
this.skillRegistry = new SkillRegistry(config.skills);
|
|
11761
12342
|
this.skillRuntime = new SkillRuntime(
|
|
11762
12343
|
this.skillRegistry,
|
|
11763
12344
|
this.tools,
|
|
11764
|
-
|
|
11765
|
-
try {
|
|
11766
|
-
await this.eventLogger.recordAudit({
|
|
11767
|
-
level: "info",
|
|
11768
|
-
event: "skill.resource_read",
|
|
11769
|
-
sessionId: this.sessionId,
|
|
11770
|
-
runId: source.runId,
|
|
11771
|
-
fields: {
|
|
11772
|
-
skillName: source.name,
|
|
11773
|
-
contentDigest: source.contentDigest,
|
|
11774
|
-
invocationId: source.invocationId,
|
|
11775
|
-
pathDigest: skillDigest(input.path),
|
|
11776
|
-
resourceDigest: skillDigest(page.text),
|
|
11777
|
-
outputBytes: Buffer.byteLength(page.text),
|
|
11778
|
-
outputTruncated: page.truncated,
|
|
11779
|
-
offset: input.offset,
|
|
11780
|
-
startLine: input.startLine,
|
|
11781
|
-
endLine: input.endLine
|
|
11782
|
-
}
|
|
11783
|
-
});
|
|
11784
|
-
} catch {
|
|
11785
|
-
throw new KapibalaError("Skill resource audit unavailable", {
|
|
11786
|
-
code: "AUDIT_UNAVAILABLE",
|
|
11787
|
-
retryPolicy: "after_user_action",
|
|
11788
|
-
safeMessage: "\u6280\u80FD\u8D44\u6E90\u8BFB\u53D6\u5BA1\u8BA1\u4E0D\u53EF\u7528\uFF0C\u672C\u8F6E\u505C\u6B62"
|
|
11789
|
-
});
|
|
11790
|
-
}
|
|
11791
|
-
}
|
|
12345
|
+
createSkillResourceAuditor(this.eventLogger, this.sessionId)
|
|
11792
12346
|
);
|
|
11793
12347
|
}
|
|
11794
12348
|
this.hooks = new HookRegistry();
|
|
@@ -11801,19 +12355,6 @@ var AgentSession = class {
|
|
|
11801
12355
|
this.mode = config.mode ?? "Approval";
|
|
11802
12356
|
this.permissionRules = config.permissionRules ?? [];
|
|
11803
12357
|
this.approvalChannel = config.approvalChannel;
|
|
11804
|
-
this.userInstructionsPath = config.userInstructionsPath;
|
|
11805
|
-
const sinks = config.eventLogger ? void 0 : createDefaultLogSinks(config.loggingDirectory);
|
|
11806
|
-
const eventLogger = config.eventLogger ?? new StructuredLogger({
|
|
11807
|
-
operationSink: sinks.operationSink,
|
|
11808
|
-
auditSink: sinks.auditSink,
|
|
11809
|
-
debug: config.logger !== void 0,
|
|
11810
|
-
onDiagnostic: config.onDiagnostic ?? config.logger
|
|
11811
|
-
});
|
|
11812
|
-
this.eventLogger = {
|
|
11813
|
-
record: (input) => eventLogger.record({ ...input, conversationId: this.persistentId }),
|
|
11814
|
-
recordAudit: (input) => eventLogger.recordAudit({ ...input, conversationId: this.persistentId })
|
|
11815
|
-
};
|
|
11816
|
-
this.auditRecoveryDirectory = config.auditRecoveryDirectory ?? sinks?.auditSink.directory;
|
|
11817
12358
|
this.rootTaskId = config.rootTaskId;
|
|
11818
12359
|
this.depth = config.depth ?? 0;
|
|
11819
12360
|
this.agentId = config.agentId;
|
|
@@ -12418,71 +12959,21 @@ var AgentSession = class {
|
|
|
12418
12959
|
runId,
|
|
12419
12960
|
modelId: profile.id
|
|
12420
12961
|
}) : this.context.project(history);
|
|
12421
|
-
|
|
12422
|
-
const original = sourceView.find((source) => source.id === message.id);
|
|
12423
|
-
const privateBag = /* @__PURE__ */ new Map();
|
|
12424
|
-
const blockBag = /* @__PURE__ */ new Map();
|
|
12425
|
-
for (const block of original?.content ?? []) {
|
|
12426
|
-
const serialized = stableSerialize(block);
|
|
12427
|
-
if (block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && block.origin)
|
|
12428
|
-
privateBag.set(serialized, (privateBag.get(serialized) ?? 0) + 1);
|
|
12429
|
-
blockBag.set(serialized, (blockBag.get(serialized) ?? 0) + 1);
|
|
12430
|
-
}
|
|
12431
|
-
const take = (bag, serialized) => {
|
|
12432
|
-
const count = bag.get(serialized) ?? 0;
|
|
12433
|
-
if (!count) return false;
|
|
12434
|
-
if (count === 1) bag.delete(serialized);
|
|
12435
|
-
else bag.set(serialized, count - 1);
|
|
12436
|
-
return true;
|
|
12437
|
-
};
|
|
12438
|
-
for (const block of message.content) {
|
|
12439
|
-
if (block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && block.origin) {
|
|
12440
|
-
if (!original || !take(privateBag, stableSerialize(block)))
|
|
12441
|
-
throw new ModelError("Hook changed signed or private request provenance", {
|
|
12442
|
-
stage: "request",
|
|
12443
|
-
retryable: false
|
|
12444
|
-
});
|
|
12445
|
-
}
|
|
12446
|
-
if (block.type !== "text" && block.type !== "tool_use") continue;
|
|
12447
|
-
if (block.protocolMeta && !take(blockBag, stableSerialize(block)))
|
|
12448
|
-
block.protocolMeta = void 0;
|
|
12449
|
-
}
|
|
12450
|
-
}
|
|
12451
|
-
const frozen = Boolean(
|
|
12452
|
-
previousRequest && history.some(
|
|
12453
|
-
(message) => message.interactionId === runId && message.content.some(
|
|
12454
|
-
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.signature && block.origin?.runId === runId)
|
|
12455
|
-
)
|
|
12456
|
-
)
|
|
12457
|
-
);
|
|
12962
|
+
verifyPrivateBlockIntegrity(projected, sourceView);
|
|
12458
12963
|
const candidate = {
|
|
12459
12964
|
...request,
|
|
12460
12965
|
messages: projected,
|
|
12461
12966
|
context: { runId, modelId: profile.id },
|
|
12462
12967
|
signal: options?.signal
|
|
12463
12968
|
};
|
|
12464
|
-
|
|
12465
|
-
|
|
12466
|
-
|
|
12467
|
-
|
|
12468
|
-
|
|
12469
|
-
|
|
12470
|
-
|
|
12471
|
-
|
|
12472
|
-
for (const original of generated) {
|
|
12473
|
-
const matches = candidate.messages.filter((message) => message.id === original.id);
|
|
12474
|
-
if (matches.length !== 1 || stableSerialize(matches[0]) !== stableSerialize(original))
|
|
12475
|
-
throw new ModelError("Signed continuation prefix or tool chain changed", {
|
|
12476
|
-
stage: "request",
|
|
12477
|
-
retryable: false
|
|
12478
|
-
});
|
|
12479
|
-
}
|
|
12480
|
-
}
|
|
12481
|
-
if (frozen && previousRequest && (stableSerialize(candidate.messages.slice(0, previousRequest.messages.length)) !== stableSerialize(previousRequest.messages) || candidate.systemPrompt !== previousRequest.systemPrompt || stableSerialize(candidate.tools) !== stableSerialize(previousRequest.tools)))
|
|
12482
|
-
throw new ModelError("Signed continuation prefix changed", {
|
|
12483
|
-
stage: "request",
|
|
12484
|
-
retryable: false
|
|
12485
|
-
});
|
|
12969
|
+
const frozen = assertSignedContinuationStable({
|
|
12970
|
+
candidate,
|
|
12971
|
+
previousRequest,
|
|
12972
|
+
history,
|
|
12973
|
+
runId,
|
|
12974
|
+
nativeBinding,
|
|
12975
|
+
projectHistory: (projectedHistory) => this.context.project(projectedHistory)
|
|
12976
|
+
});
|
|
12486
12977
|
const context = this.context;
|
|
12487
12978
|
const skills = this.skillRuntime;
|
|
12488
12979
|
const preparation = context.prepare(history, candidate, profile, runId, reason, !frozen);
|
|
@@ -12531,6 +13022,7 @@ var AgentSession = class {
|
|
|
12531
13022
|
let persistedHistoryLength = this.history.length;
|
|
12532
13023
|
let loopCompleted = false;
|
|
12533
13024
|
let loopFailed = false;
|
|
13025
|
+
let consumerInterrupted = false;
|
|
12534
13026
|
try {
|
|
12535
13027
|
for await (const event of loop.run(this.history)) {
|
|
12536
13028
|
await this.logContextEvent(event, runId);
|
|
@@ -12605,7 +13097,12 @@ var AgentSession = class {
|
|
|
12605
13097
|
if (event.type === "message_stop" || event.type === "tool_messages") {
|
|
12606
13098
|
persistedHistoryLength = this.history.length;
|
|
12607
13099
|
}
|
|
12608
|
-
|
|
13100
|
+
try {
|
|
13101
|
+
yield event;
|
|
13102
|
+
} catch (error) {
|
|
13103
|
+
consumerInterrupted = true;
|
|
13104
|
+
throw error;
|
|
13105
|
+
}
|
|
12609
13106
|
}
|
|
12610
13107
|
loopCompleted = true;
|
|
12611
13108
|
} catch (error) {
|
|
@@ -12622,7 +13119,7 @@ var AgentSession = class {
|
|
|
12622
13119
|
}
|
|
12623
13120
|
throw error;
|
|
12624
13121
|
} finally {
|
|
12625
|
-
if (!loopCompleted && !loopFailed) {
|
|
13122
|
+
if (!loopCompleted && (!loopFailed || consumerInterrupted)) {
|
|
12626
13123
|
await executor.rollback();
|
|
12627
13124
|
if (this.store) {
|
|
12628
13125
|
for (const message of this.history.slice(persistedHistoryLength)) {
|
|
@@ -12754,102 +13251,44 @@ var AgentSession = class {
|
|
|
12754
13251
|
}
|
|
12755
13252
|
/** 完整消息提交后完成技能审计;审计失败保留真实历史,停止本轮且不发布成功事件。 */
|
|
12756
13253
|
async commitSkills(runId) {
|
|
12757
|
-
|
|
12758
|
-
|
|
12759
|
-
|
|
12760
|
-
|
|
12761
|
-
|
|
12762
|
-
|
|
12763
|
-
|
|
12764
|
-
sessionId: this.sessionId,
|
|
12765
|
-
runId,
|
|
12766
|
-
fields: {
|
|
12767
|
-
skillName: source.name,
|
|
12768
|
-
skillScope: source.scope,
|
|
12769
|
-
contentDigest: source.contentDigest,
|
|
12770
|
-
renderedDigest: source.renderedDigest,
|
|
12771
|
-
pathDigest: skillDigest(source.filePath),
|
|
12772
|
-
caller: source.caller,
|
|
12773
|
-
invocationId: source.invocationId,
|
|
12774
|
-
messageId
|
|
12775
|
-
}
|
|
12776
|
-
});
|
|
12777
|
-
} catch {
|
|
12778
|
-
throw new KapibalaError("Skill commit audit unavailable", {
|
|
12779
|
-
code: "AUDIT_UNAVAILABLE",
|
|
12780
|
-
retryPolicy: "after_user_action",
|
|
12781
|
-
safeMessage: "\u6280\u80FD\u63D0\u4EA4\u5BA1\u8BA1\u4E0D\u53EF\u7528\uFF0C\u672C\u8F6E\u505C\u6B62"
|
|
12782
|
-
});
|
|
12783
|
-
}
|
|
12784
|
-
},
|
|
12785
|
-
this.store ? "disk" : "memory"
|
|
12786
|
-
);
|
|
13254
|
+
return commitSkillEvents({
|
|
13255
|
+
skillRuntime: this.skillRuntime,
|
|
13256
|
+
eventLogger: this.eventLogger,
|
|
13257
|
+
sessionId: this.sessionId,
|
|
13258
|
+
runId,
|
|
13259
|
+
persistence: this.store ? "disk" : "memory"
|
|
13260
|
+
});
|
|
12787
13261
|
}
|
|
12788
13262
|
/** 显式加载仍按同一读取策略、精确缓存与宿主审批裁决;拒绝/取消/审计失败时不追加用户任务。 */
|
|
12789
|
-
|
|
12790
|
-
|
|
12791
|
-
|
|
12792
|
-
|
|
12793
|
-
|
|
13263
|
+
authorizeSkill(name, runId, signal) {
|
|
13264
|
+
return authorizeExplicitSkill({
|
|
13265
|
+
name,
|
|
13266
|
+
runId,
|
|
13267
|
+
signal,
|
|
12794
13268
|
rootDir: this.rootDir,
|
|
12795
|
-
sessionAllowed: true
|
|
12796
|
-
};
|
|
12797
|
-
const cached = this.approvalCache.get(request);
|
|
12798
|
-
const decision = new PermissionPolicy().decide({
|
|
12799
|
-
toolName: request.toolName,
|
|
12800
|
-
capabilities: request.capabilities,
|
|
12801
13269
|
mode: this.mode,
|
|
12802
|
-
|
|
12803
|
-
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
|
|
12807
|
-
let approvalChoice;
|
|
12808
|
-
if (decision.decision === "ask") {
|
|
12809
|
-
try {
|
|
12810
|
-
approvalChoice = await this.approvalChannel?.requestApproval(request, signal) ?? "deny_once";
|
|
12811
|
-
} catch {
|
|
12812
|
-
approvalChoice = "cancel";
|
|
12813
|
-
}
|
|
12814
|
-
permitted = approvalChoice === "allow_once" || approvalChoice === "allow_session";
|
|
12815
|
-
if (approvalChoice === "allow_session") this.approvalCache.set(request, "allow");
|
|
12816
|
-
if (approvalChoice === "deny_session") this.approvalCache.set(request, "deny");
|
|
12817
|
-
}
|
|
12818
|
-
await this.eventLogger.recordAudit({
|
|
12819
|
-
level: "info",
|
|
12820
|
-
event: "skill.decided",
|
|
12821
|
-
sessionId: this.sessionId,
|
|
12822
|
-
runId,
|
|
12823
|
-
fields: {
|
|
12824
|
-
skillName: name,
|
|
12825
|
-
caller: "user",
|
|
12826
|
-
decision: permitted && !signal?.aborted ? "allow" : "deny",
|
|
12827
|
-
decisionSource: decision.decision === "ask" ? this.approvalChannel ? "human" : "non_interactive" : decision.source,
|
|
12828
|
-
approvalChoice
|
|
12829
|
-
}
|
|
13270
|
+
permissionRules: this.permissionRules,
|
|
13271
|
+
approvalChannel: this.approvalChannel,
|
|
13272
|
+
approvalCache: this.approvalCache,
|
|
13273
|
+
eventLogger: this.eventLogger,
|
|
13274
|
+
sessionId: this.sessionId
|
|
12830
13275
|
});
|
|
12831
|
-
if (!permitted) throw skillFailure("SKILL_INVOCATION_FORBIDDEN", "\u5BBF\u4E3B\u6743\u9650\u7981\u6B62\u663E\u5F0F\u6280\u80FD\u8BFB\u53D6");
|
|
12832
|
-
signal?.throwIfAborted();
|
|
12833
13276
|
}
|
|
12834
13277
|
/**
|
|
12835
13278
|
* 计算最近一次内部模型请求的上下文占用;无 usage 时显式标注预算估算。
|
|
12836
13279
|
* run 累计 promptTokens 会重复计算每一步都重发的历史,不能用于窗口占用率。
|
|
12837
13280
|
*/
|
|
12838
13281
|
createContextUsage(usedTokens, estimatedUsage = false) {
|
|
12839
|
-
|
|
12840
|
-
|
|
12841
|
-
);
|
|
12842
|
-
return {
|
|
13282
|
+
return computeContextUsage(
|
|
13283
|
+
this.lastPrimaryProfile ?? this.getActiveProfile(),
|
|
12843
13284
|
usedTokens,
|
|
12844
|
-
|
|
12845
|
-
|
|
12846
|
-
estimatedLimit: contextWindow.estimated,
|
|
12847
|
-
...estimatedUsage ? { estimatedUsage: true } : {}
|
|
12848
|
-
};
|
|
13285
|
+
estimatedUsage
|
|
13286
|
+
);
|
|
12849
13287
|
}
|
|
12850
13288
|
async persistAssistant(event) {
|
|
12851
|
-
|
|
12852
|
-
|
|
13289
|
+
await persistAssistantMessage(event, this.store, () => {
|
|
13290
|
+
this.usageKnown = false;
|
|
13291
|
+
});
|
|
12853
13292
|
}
|
|
12854
13293
|
/** 创建上下文协调器并注入当前路由;摘要不经过主任务 Loop,插件结果不默认为可剪裁。 */
|
|
12855
13294
|
createContextManager() {
|
|
@@ -12868,17 +13307,8 @@ var AgentSession = class {
|
|
|
12868
13307
|
}
|
|
12869
13308
|
/** 摘要实际消耗按 attempt 身份独立累加;缺失消耗保留未知,不计入主任务指标。 */
|
|
12870
13309
|
accountSummaryUsage(attemptId, usage) {
|
|
12871
|
-
|
|
12872
|
-
this.
|
|
12873
|
-
if (!usage || ![usage.promptTokens, usage.completionTokens, usage.totalTokens].every(
|
|
12874
|
-
(n) => Number.isSafeInteger(n) && n >= 0
|
|
12875
|
-
)) {
|
|
12876
|
-
this.summaryUsageKnown = false;
|
|
12877
|
-
return;
|
|
12878
|
-
}
|
|
12879
|
-
this.summaryUsage.promptTokens += usage.promptTokens;
|
|
12880
|
-
this.summaryUsage.completionTokens += usage.completionTokens;
|
|
12881
|
-
this.summaryUsage.totalTokens += usage.totalTokens;
|
|
13310
|
+
const outcome = recordSummaryUsage(attemptId, usage, this.summaryAttempts, this.summaryUsage);
|
|
13311
|
+
if (outcome === "unknown") this.summaryUsageKnown = false;
|
|
12882
13312
|
}
|
|
12883
13313
|
/**
|
|
12884
13314
|
* 空闲时手动摘要旧完整前缀,不创建用户任务、不执行工具,不突破当前与最近成功交互。
|
|
@@ -12951,61 +13381,21 @@ var AgentSession = class {
|
|
|
12951
13381
|
}
|
|
12952
13382
|
/** 以模型 attempt 身份幂等累加真实消耗;非法或缺失 usage 保持未知。 */
|
|
12953
13383
|
accountUsage(attemptId, usage) {
|
|
12954
|
-
|
|
12955
|
-
if (
|
|
12956
|
-
(n) => Number.isSafeInteger(n) && n >= 0
|
|
12957
|
-
)) {
|
|
12958
|
-
this.usageKnown = false;
|
|
12959
|
-
return;
|
|
12960
|
-
}
|
|
12961
|
-
this.accountedAttempts.add(attemptId);
|
|
12962
|
-
this.usageStats.promptTokens += usage.promptTokens;
|
|
12963
|
-
this.usageStats.completionTokens += usage.completionTokens;
|
|
12964
|
-
this.usageStats.totalTokens += usage.totalTokens;
|
|
13384
|
+
const outcome = recordUsage(attemptId, usage, this.accountedAttempts, this.usageStats);
|
|
13385
|
+
if (outcome === "unknown") this.usageKnown = false;
|
|
12965
13386
|
}
|
|
12966
13387
|
async persistToolMessages(messages) {
|
|
12967
|
-
|
|
12968
|
-
for (const message of messages) await this.store.append(message);
|
|
13388
|
+
await persistToolMessages(messages, this.store);
|
|
12969
13389
|
}
|
|
12970
13390
|
/** 中止时为缓冲 assistant 的每个 tool_use 生成 OUTCOME_UNKNOWN 结果并入历史,保持事务闭合。 */
|
|
12971
13391
|
closeInterruptedToolCalls(assistantEvent, error) {
|
|
12972
|
-
const message =
|
|
12973
|
-
id: createMessageId(),
|
|
12974
|
-
role: "tool",
|
|
12975
|
-
content: assistantEvent.message.content.filter((block) => block.type === "tool_use").map((toolUse) => ({
|
|
12976
|
-
type: "tool_result",
|
|
12977
|
-
toolUseId: toolUse.id,
|
|
12978
|
-
content: JSON.stringify({
|
|
12979
|
-
code: "OUTCOME_UNKNOWN",
|
|
12980
|
-
retryPolicy: "after_user_action",
|
|
12981
|
-
message: "Tool execution was interrupted; outcome is unknown"
|
|
12982
|
-
}),
|
|
12983
|
-
isError: true,
|
|
12984
|
-
errorCode: "OUTCOME_UNKNOWN",
|
|
12985
|
-
retryPolicy: "after_user_action"
|
|
12986
|
-
})),
|
|
12987
|
-
timestamp: Date.now()
|
|
12988
|
-
};
|
|
13392
|
+
const message = buildInterruptedToolMessage(assistantEvent);
|
|
12989
13393
|
this.history.push(message);
|
|
12990
13394
|
return [message];
|
|
12991
13395
|
}
|
|
12992
13396
|
/** 收集历史中紧跟该 assistant 的连续 tool 消息;仅当覆盖全部 tool_use 时视为已闭合。 */
|
|
12993
13397
|
findClosedToolMessages(assistantEvent) {
|
|
12994
|
-
|
|
12995
|
-
if (assistantIndex < 0) return [];
|
|
12996
|
-
const expected = new Set(
|
|
12997
|
-
assistantEvent.message.content.filter((block) => block.type === "tool_use").map((toolUse) => toolUse.id)
|
|
12998
|
-
);
|
|
12999
|
-
const messages = [];
|
|
13000
|
-
for (let index2 = assistantIndex + 1; index2 < this.history.length; index2++) {
|
|
13001
|
-
const message = this.history[index2];
|
|
13002
|
-
if (message.role !== "tool") break;
|
|
13003
|
-
messages.push(message);
|
|
13004
|
-
for (const block of message.content) {
|
|
13005
|
-
if (block.type === "tool_result") expected.delete(block.toolUseId);
|
|
13006
|
-
}
|
|
13007
|
-
}
|
|
13008
|
-
return expected.size === 0 ? messages : [];
|
|
13398
|
+
return findClosedToolMessages(this.history, assistantEvent);
|
|
13009
13399
|
}
|
|
13010
13400
|
};
|
|
13011
13401
|
export {
|