@kiturone/kapibala 0.0.9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1245 -802
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +115 -32
- package/dist/index.d.ts +115 -32
- package/dist/index.js +1244 -801
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -129,6 +129,21 @@ var KapibalaError = class extends Error {
|
|
|
129
129
|
this.dispatchOutcome = options?.dispatchOutcome;
|
|
130
130
|
}
|
|
131
131
|
};
|
|
132
|
+
var SESSION_SAFE_MESSAGES = {
|
|
133
|
+
SESSION_LOCKED: "\u4F1A\u8BDD\u5DF2\u88AB\u5176\u4ED6\u6301\u6709\u8005\u5360\u7528",
|
|
134
|
+
SESSION_RECOVERY_IN_PROGRESS: "\u4F1A\u8BDD\u6B63\u5728\u6062\u590D",
|
|
135
|
+
SESSION_LOCK_OWNER_CHANGED: "\u4F1A\u8BDD\u9501\u6301\u6709\u8005\u5DF2\u53D8\u5316",
|
|
136
|
+
SESSION_NOT_FOUND: "\u672A\u627E\u5230\u6307\u5B9A\u4F1A\u8BDD",
|
|
137
|
+
SESSION_ID_AMBIGUOUS: "\u4F1A\u8BDD\u524D\u7F00\u4E0D\u552F\u4E00",
|
|
138
|
+
HISTORY_PAGE_INVALID: "\u5386\u53F2\u9875\u53C2\u6570\u65E0\u6548"
|
|
139
|
+
};
|
|
140
|
+
function createSessionError(code, message) {
|
|
141
|
+
return new KapibalaError(message, {
|
|
142
|
+
code,
|
|
143
|
+
safeMessage: SESSION_SAFE_MESSAGES[code],
|
|
144
|
+
retryPolicy: "after_user_action"
|
|
145
|
+
});
|
|
146
|
+
}
|
|
132
147
|
var PreDispatchError = class extends KapibalaError {
|
|
133
148
|
constructor(message, options) {
|
|
134
149
|
super(message, {
|
|
@@ -517,15 +532,50 @@ import path from "path";
|
|
|
517
532
|
|
|
518
533
|
// src/context/jsonl-lines.ts
|
|
519
534
|
import fs from "fs";
|
|
520
|
-
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 = {}) {
|
|
521
567
|
const fd = fs.openSync(file, "r");
|
|
522
568
|
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
523
569
|
let pending = Buffer.alloc(0);
|
|
524
570
|
let offset = 0;
|
|
571
|
+
let total = 0;
|
|
525
572
|
try {
|
|
573
|
+
assertHistoryReadBudget(fs.fstatSync(fd).size, options);
|
|
526
574
|
while (true) {
|
|
527
575
|
const length = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
528
576
|
if (!length) break;
|
|
577
|
+
total += length;
|
|
578
|
+
assertHistoryReadBudget(total, options);
|
|
529
579
|
pending = Buffer.concat([pending, buffer.subarray(0, length)]);
|
|
530
580
|
let start = 0;
|
|
531
581
|
let end = pending.indexOf(10, start);
|
|
@@ -559,7 +609,7 @@ var SESSION_SCHEMA_VERSION_V2 = 2;
|
|
|
559
609
|
var SESSION_SCHEMA_VERSION = SESSION_SCHEMA_VERSION_V2;
|
|
560
610
|
var MAIN_BRANCH_ID = "main";
|
|
561
611
|
function isGlobalControlRecordType(type) {
|
|
562
|
-
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";
|
|
563
613
|
}
|
|
564
614
|
var types = /* @__PURE__ */ new Set([
|
|
565
615
|
"header",
|
|
@@ -578,7 +628,8 @@ var types = /* @__PURE__ */ new Set([
|
|
|
578
628
|
"branch_created",
|
|
579
629
|
"branch_selected",
|
|
580
630
|
"branch_summary",
|
|
581
|
-
"archive_state"
|
|
631
|
+
"archive_state",
|
|
632
|
+
"pin_state"
|
|
582
633
|
]);
|
|
583
634
|
function supportsSessionState(store) {
|
|
584
635
|
return "appendRecord" in store && typeof store.appendRecord === "function" && "loadState" in store && typeof store.loadState === "function";
|
|
@@ -661,6 +712,13 @@ function assertClosedToolTransaction(records, targetRecordId) {
|
|
|
661
712
|
}
|
|
662
713
|
}
|
|
663
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;
|
|
664
722
|
filePath;
|
|
665
723
|
queue = Promise.resolve();
|
|
666
724
|
records = [];
|
|
@@ -670,10 +728,6 @@ var SessionStore = class _SessionStore {
|
|
|
670
728
|
schemaVersion = SESSION_SCHEMA_VERSION_V2;
|
|
671
729
|
activeBranchId = "main";
|
|
672
730
|
branchLeaves = /* @__PURE__ */ new Map();
|
|
673
|
-
constructor(filePath, schemaVersion = SESSION_SCHEMA_VERSION_V2) {
|
|
674
|
-
this.filePath = path.resolve(filePath);
|
|
675
|
-
this.schemaVersion = schemaVersion;
|
|
676
|
-
}
|
|
677
731
|
/** 获取当前会话文件使用的 Schema 版本(1 或 2)。 */
|
|
678
732
|
getSchemaVersion() {
|
|
679
733
|
return this.schemaVersion;
|
|
@@ -696,8 +750,8 @@ var SessionStore = class _SessionStore {
|
|
|
696
750
|
this.onDiagnostic = onDiagnostic;
|
|
697
751
|
}
|
|
698
752
|
/** 以独占创建方式安装新头部(默认生成 Schema v2 格式),已有文件明确失败,绝不覆盖其它会话。 */
|
|
699
|
-
static async create(filePath, header) {
|
|
700
|
-
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 });
|
|
701
755
|
fs2.mkdirSync(path.dirname(store.filePath), { recursive: true });
|
|
702
756
|
const record = store.makeRecord("header", {
|
|
703
757
|
...header,
|
|
@@ -718,8 +772,8 @@ var SessionStore = class _SessionStore {
|
|
|
718
772
|
return store;
|
|
719
773
|
}
|
|
720
774
|
/** 打开并验证已有会话;尾部残片先保存到隔离文件,中部损坏与未知版本拒绝恢复。 */
|
|
721
|
-
static async open(filePath) {
|
|
722
|
-
const store = new _SessionStore(filePath);
|
|
775
|
+
static async open(filePath, options = {}) {
|
|
776
|
+
const store = new _SessionStore(filePath, SESSION_SCHEMA_VERSION_V2, { ...options });
|
|
723
777
|
store.records = store.readFile(true);
|
|
724
778
|
store.refreshStamp();
|
|
725
779
|
return store;
|
|
@@ -813,6 +867,24 @@ var SessionStore = class _SessionStore {
|
|
|
813
867
|
}
|
|
814
868
|
return false;
|
|
815
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
|
+
}
|
|
816
888
|
/**
|
|
817
889
|
* 获取当前会话中所有已知分支的元数据与状态列表。
|
|
818
890
|
*/
|
|
@@ -1104,14 +1176,15 @@ ${summaryText}` }]
|
|
|
1104
1176
|
}
|
|
1105
1177
|
/** 校验每个完整记录,只有最后非空行的 JSON 残片可隔离,语义损坏绝不作为残片吞掉。 */
|
|
1106
1178
|
readFile(recover) {
|
|
1107
|
-
const iterator = readJsonlLines(this.filePath);
|
|
1179
|
+
const iterator = readJsonlLines(this.filePath, this.readOptions);
|
|
1108
1180
|
let lastTerminated = true;
|
|
1109
1181
|
let blankSeen = false;
|
|
1110
1182
|
const records = [];
|
|
1111
1183
|
const identities = /* @__PURE__ */ new Set();
|
|
1112
1184
|
const messages = /* @__PURE__ */ new Set();
|
|
1113
|
-
|
|
1114
|
-
|
|
1185
|
+
const branchLeaves = /* @__PURE__ */ new Map();
|
|
1186
|
+
let activeBranchId = MAIN_BRANCH_ID;
|
|
1187
|
+
let schemaVersion = this.schemaVersion;
|
|
1115
1188
|
let detectedSchemaVersion = null;
|
|
1116
1189
|
let i = -1;
|
|
1117
1190
|
for (const line of iterator) {
|
|
@@ -1126,7 +1199,7 @@ ${summaryText}` }]
|
|
|
1126
1199
|
try {
|
|
1127
1200
|
record = JSON.parse(line.text);
|
|
1128
1201
|
} catch {
|
|
1129
|
-
if (!recover ||
|
|
1202
|
+
if (!recover || schemaVersion === SESSION_SCHEMA_VERSION_V1 || !records.length || Array.from(iterator).some((next) => next.text.trim()))
|
|
1130
1203
|
throw new Error(`Session corrupt at line ${i + 1}`);
|
|
1131
1204
|
fs2.writeFileSync(
|
|
1132
1205
|
`${this.filePath}.corrupt-${createMessageId()}`,
|
|
@@ -1150,7 +1223,7 @@ ${summaryText}` }]
|
|
|
1150
1223
|
throw new Error("Unsupported session schema version");
|
|
1151
1224
|
}
|
|
1152
1225
|
detectedSchemaVersion = record.schemaVersion;
|
|
1153
|
-
|
|
1226
|
+
schemaVersion = detectedSchemaVersion;
|
|
1154
1227
|
} else if (record.schemaVersion !== detectedSchemaVersion) {
|
|
1155
1228
|
throw new Error(`Session corrupt schema version mismatch at line ${i + 1}`);
|
|
1156
1229
|
}
|
|
@@ -1167,11 +1240,11 @@ ${summaryText}` }]
|
|
|
1167
1240
|
} else if (record.type === "header") {
|
|
1168
1241
|
throw new Error("Session corrupt: repeated header");
|
|
1169
1242
|
}
|
|
1170
|
-
if (
|
|
1243
|
+
if (schemaVersion === SESSION_SCHEMA_VERSION_V1) {
|
|
1171
1244
|
if (record.parentId !== (records.at(-1)?.recordId ?? null)) {
|
|
1172
1245
|
throw new Error(`Session corrupt parent at line ${i + 1}`);
|
|
1173
1246
|
}
|
|
1174
|
-
|
|
1247
|
+
branchLeaves.set(MAIN_BRANCH_ID, record.recordId);
|
|
1175
1248
|
} else {
|
|
1176
1249
|
if (typeof record.seq !== "number" || record.seq !== records.length) {
|
|
1177
1250
|
throw new Error(`Session corrupt seq at line ${i + 1}`);
|
|
@@ -1185,37 +1258,37 @@ ${summaryText}` }]
|
|
|
1185
1258
|
throw new Error(`Session corrupt control record parent at line ${i + 1}`);
|
|
1186
1259
|
}
|
|
1187
1260
|
if (record.type === "header") {
|
|
1188
|
-
|
|
1189
|
-
|
|
1261
|
+
branchLeaves.set(MAIN_BRANCH_ID, record.recordId);
|
|
1262
|
+
activeBranchId = MAIN_BRANCH_ID;
|
|
1190
1263
|
} else if (record.type === "branch_created") {
|
|
1191
1264
|
const bId = record.payload.branchId;
|
|
1192
1265
|
const baseId = record.payload.baseRecordId;
|
|
1193
1266
|
if (typeof bId !== "string" || !bId) {
|
|
1194
1267
|
throw new Error(`Session corrupt branch_created branchId at line ${i + 1}`);
|
|
1195
1268
|
}
|
|
1196
|
-
if (
|
|
1269
|
+
if (branchLeaves.has(bId)) {
|
|
1197
1270
|
throw new Error(`Duplicate branch ID: ${bId}`);
|
|
1198
1271
|
}
|
|
1199
1272
|
if (typeof baseId !== "string" || !identities.has(baseId)) {
|
|
1200
1273
|
throw new Error(`Session corrupt branch_created baseRecordId at line ${i + 1}`);
|
|
1201
1274
|
}
|
|
1202
|
-
|
|
1275
|
+
branchLeaves.set(bId, baseId);
|
|
1203
1276
|
} else if (record.type === "branch_selected") {
|
|
1204
1277
|
const bId = record.payload.branchId;
|
|
1205
|
-
if (typeof bId !== "string" || !
|
|
1278
|
+
if (typeof bId !== "string" || !branchLeaves.has(bId)) {
|
|
1206
1279
|
throw new Error(`Selected branch not found: ${bId}`);
|
|
1207
1280
|
}
|
|
1208
|
-
|
|
1281
|
+
activeBranchId = bId;
|
|
1209
1282
|
}
|
|
1210
1283
|
} else {
|
|
1211
1284
|
const bId = record.branchId;
|
|
1212
|
-
if (typeof bId !== "string" || !
|
|
1285
|
+
if (typeof bId !== "string" || !branchLeaves.has(bId)) {
|
|
1213
1286
|
throw new Error(`Session corrupt branchId at line ${i + 1}`);
|
|
1214
1287
|
}
|
|
1215
1288
|
if (typeof record.parentId !== "string" || !identities.has(record.parentId)) {
|
|
1216
1289
|
throw new Error(`Session corrupt parent at line ${i + 1}`);
|
|
1217
1290
|
}
|
|
1218
|
-
|
|
1291
|
+
branchLeaves.set(bId, record.recordId);
|
|
1219
1292
|
}
|
|
1220
1293
|
}
|
|
1221
1294
|
identities.add(record.recordId);
|
|
@@ -1234,7 +1307,7 @@ ${summaryText}` }]
|
|
|
1234
1307
|
if (!records.length) {
|
|
1235
1308
|
throw new Error("Session corrupt header");
|
|
1236
1309
|
}
|
|
1237
|
-
if (recover && !lastTerminated && records.length === i + 1 &&
|
|
1310
|
+
if (recover && !lastTerminated && records.length === i + 1 && schemaVersion !== SESSION_SCHEMA_VERSION_V1) {
|
|
1238
1311
|
const fd = fs2.openSync(this.filePath, "a");
|
|
1239
1312
|
try {
|
|
1240
1313
|
fs2.writeFileSync(fd, "\n");
|
|
@@ -1243,6 +1316,9 @@ ${summaryText}` }]
|
|
|
1243
1316
|
fs2.closeSync(fd);
|
|
1244
1317
|
}
|
|
1245
1318
|
}
|
|
1319
|
+
this.branchLeaves = branchLeaves;
|
|
1320
|
+
this.activeBranchId = activeBranchId;
|
|
1321
|
+
this.schemaVersion = schemaVersion;
|
|
1246
1322
|
return records;
|
|
1247
1323
|
}
|
|
1248
1324
|
};
|
|
@@ -1274,19 +1350,23 @@ var SessionLock = class _SessionLock {
|
|
|
1274
1350
|
if (error.code !== "EEXIST") throw error;
|
|
1275
1351
|
const stale = _SessionLock.readOwner(lockPath);
|
|
1276
1352
|
if (!stale || !_SessionLock.isDead(stale.pid))
|
|
1277
|
-
throw
|
|
1353
|
+
throw createSessionError(
|
|
1354
|
+
"SESSION_LOCKED",
|
|
1278
1355
|
`Session locked by a live or unknown owner${stale ? ` (pid=${stale.pid})` : ""}`
|
|
1279
1356
|
);
|
|
1280
1357
|
try {
|
|
1281
1358
|
fs3.mkdirSync(recovery);
|
|
1282
1359
|
} catch {
|
|
1283
|
-
throw
|
|
1360
|
+
throw createSessionError(
|
|
1361
|
+
"SESSION_RECOVERY_IN_PROGRESS",
|
|
1362
|
+
"Session locked: recovery in progress"
|
|
1363
|
+
);
|
|
1284
1364
|
}
|
|
1285
1365
|
try {
|
|
1286
1366
|
fs3.writeFileSync(path2.join(recovery, "owner.json"), JSON.stringify(owner), { flag: "wx" });
|
|
1287
1367
|
const current = _SessionLock.readOwner(lockPath);
|
|
1288
1368
|
if (current?.nonce !== stale.nonce || current.pid !== stale.pid || !_SessionLock.isDead(current.pid)) {
|
|
1289
|
-
throw
|
|
1369
|
+
throw createSessionError("SESSION_LOCK_OWNER_CHANGED", "Session locked: owner changed");
|
|
1290
1370
|
}
|
|
1291
1371
|
const quarantine = `${lockPath}.stale-${randomUUID2()}`;
|
|
1292
1372
|
fs3.renameSync(lockPath, quarantine);
|
|
@@ -1306,7 +1386,8 @@ var SessionLock = class _SessionLock {
|
|
|
1306
1386
|
}
|
|
1307
1387
|
if (installError.code === "EEXIST") {
|
|
1308
1388
|
const winner = _SessionLock.readOwner(lockPath);
|
|
1309
|
-
throw
|
|
1389
|
+
throw createSessionError(
|
|
1390
|
+
"SESSION_LOCKED",
|
|
1310
1391
|
`Session locked by a live or unknown owner${winner ? ` (pid=${winner.pid})` : ""}`
|
|
1311
1392
|
);
|
|
1312
1393
|
}
|
|
@@ -1340,7 +1421,7 @@ var SessionLock = class _SessionLock {
|
|
|
1340
1421
|
if (this.released) return;
|
|
1341
1422
|
const current = _SessionLock.readOwner(this.lockPath);
|
|
1342
1423
|
if (current?.nonce !== this.owner.nonce || current.pid !== this.owner.pid)
|
|
1343
|
-
throw
|
|
1424
|
+
throw createSessionError("SESSION_LOCK_OWNER_CHANGED", "Session lock owner changed");
|
|
1344
1425
|
fs3.unlinkSync(path2.join(this.lockPath, "owner.json"));
|
|
1345
1426
|
fs3.rmdirSync(this.lockPath);
|
|
1346
1427
|
this.released = true;
|
|
@@ -1371,7 +1452,11 @@ var SessionLock = class _SessionLock {
|
|
|
1371
1452
|
/** 无残留或残留进程已死亡时放行;确认存活的回收进行中则明确失败。 */
|
|
1372
1453
|
static ensureNoLiveRecovery(recovery) {
|
|
1373
1454
|
_SessionLock.clearAbandonedRecovery(recovery);
|
|
1374
|
-
if (fs3.existsSync(recovery))
|
|
1455
|
+
if (fs3.existsSync(recovery))
|
|
1456
|
+
throw createSessionError(
|
|
1457
|
+
"SESSION_RECOVERY_IN_PROGRESS",
|
|
1458
|
+
"Session locked: recovery in progress"
|
|
1459
|
+
);
|
|
1375
1460
|
}
|
|
1376
1461
|
/** 仅 ESRCH 表示确定死亡;PID 复用或 EPERM 均保持原锁。 */
|
|
1377
1462
|
static isDead(pid) {
|
|
@@ -1521,18 +1606,21 @@ function resolveWorkspaceContext(input) {
|
|
|
1521
1606
|
function normalizedProjectRoot(root) {
|
|
1522
1607
|
return process.platform === "win32" ? root.replaceAll("\\", "/").toLowerCase() : root;
|
|
1523
1608
|
}
|
|
1524
|
-
function resolveSessionProject(cwd) {
|
|
1609
|
+
function resolveSessionProject(cwd, mode = "git") {
|
|
1525
1610
|
const realCwd = toNativePhysicalPath(cwd);
|
|
1526
1611
|
let root = realCwd;
|
|
1527
1612
|
let gitBranch;
|
|
1528
1613
|
let gitWorktree = false;
|
|
1529
1614
|
try {
|
|
1530
|
-
|
|
1615
|
+
const gitRoot = toNativePhysicalPath(
|
|
1531
1616
|
execFileSync("git", ["-C", realCwd, "rev-parse", "--show-toplevel"], {
|
|
1532
1617
|
encoding: "utf8",
|
|
1533
1618
|
stdio: ["ignore", "pipe", "ignore"]
|
|
1534
1619
|
}).trim()
|
|
1535
1620
|
);
|
|
1621
|
+
if (mode === "git") root = gitRoot;
|
|
1622
|
+
if (normalizedProjectRoot(gitRoot) !== normalizedProjectRoot(root))
|
|
1623
|
+
return { projectRoot: root, cwd: realCwd, projectKey: computeProjectKey(root) };
|
|
1536
1624
|
gitWorktree = true;
|
|
1537
1625
|
gitBranch = execFileSync("git", ["-C", realCwd, "symbolic-ref", "--short", "-q", "HEAD"], {
|
|
1538
1626
|
encoding: "utf8",
|
|
@@ -1540,11 +1628,10 @@ function resolveSessionProject(cwd) {
|
|
|
1540
1628
|
}).trim() || void 0;
|
|
1541
1629
|
} catch {
|
|
1542
1630
|
}
|
|
1543
|
-
const normalized = normalizedProjectRoot(root);
|
|
1544
1631
|
return {
|
|
1545
1632
|
projectRoot: root,
|
|
1546
1633
|
cwd: realCwd,
|
|
1547
|
-
projectKey:
|
|
1634
|
+
projectKey: computeProjectKey(root),
|
|
1548
1635
|
gitBranch,
|
|
1549
1636
|
gitWorktree
|
|
1550
1637
|
};
|
|
@@ -1562,7 +1649,10 @@ var SessionManager = class {
|
|
|
1562
1649
|
directory;
|
|
1563
1650
|
handles = /* @__PURE__ */ new Map();
|
|
1564
1651
|
diagnostic;
|
|
1652
|
+
readOptions;
|
|
1565
1653
|
constructor(options = {}) {
|
|
1654
|
+
this.readOptions = { maxReadBytes: options.maxReadBytes };
|
|
1655
|
+
assertHistoryReadBudget(0, this.readOptions);
|
|
1566
1656
|
const rootCandidate = options.workspaceRoot ?? options.projectRoot ?? options.cwd;
|
|
1567
1657
|
if (!rootCandidate) {
|
|
1568
1658
|
throw new KapibalaError("Workspace root is required for SessionManager", {
|
|
@@ -1570,7 +1660,7 @@ var SessionManager = class {
|
|
|
1570
1660
|
safeMessage: "\u5FC5\u987B\u663E\u5F0F\u6307\u5B9A\u5DE5\u4F5C\u533A\u6839\u8DEF\u5F84\u6216\u9879\u76EE\u76EE\u5F55"
|
|
1571
1661
|
});
|
|
1572
1662
|
}
|
|
1573
|
-
this.project = resolveSessionProject(rootCandidate);
|
|
1663
|
+
this.project = resolveSessionProject(rootCandidate, options.projectRootMode);
|
|
1574
1664
|
this.directory = path4.join(
|
|
1575
1665
|
options.homeDirectory ?? os.homedir(),
|
|
1576
1666
|
".kapibala",
|
|
@@ -1586,12 +1676,16 @@ var SessionManager = class {
|
|
|
1586
1676
|
const file = path4.join(this.directory, `${Date.now()}_${conversationId}.jsonl`);
|
|
1587
1677
|
const lock = await SessionLock.acquire(this.lockPath(conversationId));
|
|
1588
1678
|
try {
|
|
1589
|
-
const store = await SessionStore.create(
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
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
|
+
);
|
|
1595
1689
|
return await this.attach(store, lock);
|
|
1596
1690
|
} catch (error) {
|
|
1597
1691
|
await lock.release();
|
|
@@ -1600,89 +1694,146 @@ var SessionManager = class {
|
|
|
1600
1694
|
}
|
|
1601
1695
|
/**
|
|
1602
1696
|
* 按正文活动排序分页;有效 sidecar 只读元数据,不读取完整 JSONL。默认排除已归档会话。
|
|
1603
|
-
* pageSize 由调用方显式给出时必须在 1–100
|
|
1697
|
+
* pageSize 由调用方显式给出时必须在 1–100 之间;filter 在分页前筛选元数据,调用方须自行处理敏感信息。
|
|
1698
|
+
* 置顶项整体前置、组内仍按活动时间排序;排序只作用于本方法的返回值,不影响 continueRecent。
|
|
1604
1699
|
*/
|
|
1605
1700
|
async list(page = 1, options) {
|
|
1606
1701
|
const pageSize = options?.pageSize ?? 20;
|
|
1607
1702
|
if (!Number.isSafeInteger(page) || page < 1 || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100)
|
|
1608
|
-
throw
|
|
1703
|
+
throw createSessionError("HISTORY_PAGE_INVALID", "History page must be a positive integer");
|
|
1609
1704
|
const all = await this.allMetadata();
|
|
1610
|
-
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
|
+
];
|
|
1611
1711
|
return {
|
|
1612
|
-
items:
|
|
1613
|
-
total:
|
|
1712
|
+
items: ordered.slice((page - 1) * pageSize, page * pageSize),
|
|
1713
|
+
total: ordered.length,
|
|
1614
1714
|
page,
|
|
1615
|
-
pages: Math.max(1, Math.ceil(
|
|
1715
|
+
pages: Math.max(1, Math.ceil(ordered.length / pageSize))
|
|
1616
1716
|
};
|
|
1617
1717
|
}
|
|
1618
1718
|
/** 解析完整 ID 或唯一前缀对应的会话文件与 ID。 */
|
|
1619
1719
|
resolveSession(id) {
|
|
1620
|
-
if (!id || !/^[a-zA-Z0-9-]+$/.test(id))
|
|
1720
|
+
if (!id || !/^[a-zA-Z0-9-]+$/.test(id))
|
|
1721
|
+
throw createSessionError("SESSION_NOT_FOUND", "Session not found");
|
|
1621
1722
|
const all = fs5.readdirSync(this.directory).filter((name) => /^\d+_[a-zA-Z0-9-]+\.jsonl$/.test(name)).map((fileName) => ({
|
|
1622
1723
|
fileName,
|
|
1623
1724
|
conversationId: fileName.slice(fileName.indexOf("_") + 1, -6)
|
|
1624
1725
|
}));
|
|
1625
1726
|
const exact = all.find((m) => m.conversationId === id || m.fileName === id);
|
|
1626
1727
|
const matches = exact ? [exact] : all.filter((m) => m.conversationId.startsWith(id));
|
|
1627
|
-
if (!matches.length) throw
|
|
1628
|
-
if (matches.length !== 1)
|
|
1728
|
+
if (!matches.length) throw createSessionError("SESSION_NOT_FOUND", `Session not found: ${id}`);
|
|
1729
|
+
if (matches.length !== 1)
|
|
1730
|
+
throw createSessionError("SESSION_ID_AMBIGUOUS", `Ambiguous session prefix: ${id}`);
|
|
1629
1731
|
return matches[0];
|
|
1630
1732
|
}
|
|
1631
1733
|
/**
|
|
1632
|
-
*
|
|
1734
|
+
* 归档、恢复、重命名与置顶都向正文追加一条全局控制记录,共用同一套前置约束。
|
|
1633
1735
|
*
|
|
1634
1736
|
* 约束:
|
|
1635
1737
|
* - 必须取得会话独占锁;
|
|
1636
|
-
* -
|
|
1637
|
-
* -
|
|
1738
|
+
* - 若会话当前正处于活动打开状态,拒绝并抛出 SESSION_IN_USE(调用方先关闭句柄);
|
|
1739
|
+
* - v1 只读历史不能变更,明确以 LEGACY_SESSION_READ_ONLY 失败而不是静默忽略;
|
|
1740
|
+
* - 写入后按正文重建 sidecar,状态只以正文记录为准。
|
|
1638
1741
|
*/
|
|
1639
|
-
async
|
|
1742
|
+
async appendControlRecord(id, subject, append) {
|
|
1640
1743
|
const { fileName, conversationId } = this.resolveSession(id);
|
|
1641
1744
|
if (this.handles.has(conversationId)) {
|
|
1642
|
-
throw new KapibalaError("Session is currently active or in use
|
|
1745
|
+
throw new KapibalaError("Session is currently active or in use", {
|
|
1643
1746
|
code: "SESSION_IN_USE",
|
|
1644
|
-
safeMessage:
|
|
1747
|
+
safeMessage: `\u4F1A\u8BDD\u6B63\u5728\u4F7F\u7528\u4E2D\uFF0C\u65E0\u6CD5${subject}`
|
|
1645
1748
|
});
|
|
1646
1749
|
}
|
|
1647
1750
|
const targetFile = path4.join(this.directory, fileName);
|
|
1648
1751
|
const lock = await SessionLock.acquire(this.lockPath(conversationId));
|
|
1649
1752
|
try {
|
|
1650
|
-
const store = await SessionStore.open(targetFile);
|
|
1753
|
+
const store = await SessionStore.open(targetFile, this.readOptions);
|
|
1651
1754
|
if (store.isReadOnly()) {
|
|
1652
|
-
throw new KapibalaError("Cannot
|
|
1755
|
+
throw new KapibalaError("Cannot modify legacy v1 session state", {
|
|
1653
1756
|
code: "LEGACY_SESSION_READ_ONLY",
|
|
1654
|
-
safeMessage:
|
|
1757
|
+
safeMessage: `\u65E0\u6CD5\u53D8\u66F4\u53EA\u8BFB\u5386\u53F2\u4F1A\u8BDD\u7684${subject}\u72B6\u6001`
|
|
1655
1758
|
});
|
|
1656
1759
|
}
|
|
1657
|
-
await store
|
|
1658
|
-
|
|
1659
|
-
const meta = this.buildMetadata(targetFile, records);
|
|
1660
|
-
meta.archived = true;
|
|
1661
|
-
this.writeMetadata(targetFile, meta);
|
|
1760
|
+
await append(store);
|
|
1761
|
+
this.writeMetadata(targetFile, this.buildMetadata(targetFile, await store.readRecords()));
|
|
1662
1762
|
} finally {
|
|
1663
1763
|
await lock.release();
|
|
1664
1764
|
}
|
|
1665
1765
|
}
|
|
1666
1766
|
/**
|
|
1667
|
-
*
|
|
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 },正文保持不动。
|
|
1668
1774
|
*/
|
|
1669
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) {
|
|
1670
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
|
+
}
|
|
1671
1821
|
const targetFile = path4.join(this.directory, fileName);
|
|
1672
1822
|
const lock = await SessionLock.acquire(this.lockPath(conversationId));
|
|
1673
1823
|
try {
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1824
|
+
const headerLines = this.readDiscoveryRecords(targetFile);
|
|
1825
|
+
let header;
|
|
1826
|
+
try {
|
|
1827
|
+
header = headerLines.next().value.payload;
|
|
1828
|
+
} finally {
|
|
1829
|
+
headerLines.return(void 0);
|
|
1680
1830
|
}
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
const
|
|
1684
|
-
|
|
1685
|
-
|
|
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);
|
|
1686
1837
|
} finally {
|
|
1687
1838
|
await lock.release();
|
|
1688
1839
|
}
|
|
@@ -1694,6 +1845,11 @@ var SessionManager = class {
|
|
|
1694
1845
|
const file = path4.join(this.directory, fileName);
|
|
1695
1846
|
try {
|
|
1696
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
|
+
}
|
|
1697
1853
|
if (!meta) {
|
|
1698
1854
|
const records = this.readDiscoveryRecords(file);
|
|
1699
1855
|
meta = this.buildMetadata(file, records);
|
|
@@ -1704,7 +1860,17 @@ var SessionManager = class {
|
|
|
1704
1860
|
}
|
|
1705
1861
|
}
|
|
1706
1862
|
all.push(meta);
|
|
1707
|
-
} 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
|
+
}
|
|
1708
1874
|
this.report(`Cannot discover session: ${fileName}`);
|
|
1709
1875
|
}
|
|
1710
1876
|
}
|
|
@@ -1754,7 +1920,10 @@ var SessionManager = class {
|
|
|
1754
1920
|
}
|
|
1755
1921
|
if (header.conversationId !== meta.conversationId || normalizedProjectRoot(header.projectRoot) !== normalizedProjectRoot(this.project.projectRoot))
|
|
1756
1922
|
throw new Error("Foreign session identity");
|
|
1757
|
-
const store = await SessionStore.open(
|
|
1923
|
+
const store = await SessionStore.open(
|
|
1924
|
+
path4.join(this.directory, meta.fileName),
|
|
1925
|
+
this.readOptions
|
|
1926
|
+
);
|
|
1758
1927
|
if ((await store.loadState()).header.conversationId !== meta.conversationId)
|
|
1759
1928
|
throw new Error("Session identity mismatch");
|
|
1760
1929
|
await store.load();
|
|
@@ -1766,7 +1935,9 @@ var SessionManager = class {
|
|
|
1766
1935
|
}
|
|
1767
1936
|
/** 继续最近有正文的会话;没有历史才新建,显式打开失败绝不回退为空会话。 */
|
|
1768
1937
|
async continueRecent(options) {
|
|
1769
|
-
const latest = (await this.allMetadata()).find(
|
|
1938
|
+
const latest = (await this.allMetadata()).find(
|
|
1939
|
+
(m) => m.messageCount > 0 || m.unavailableReason
|
|
1940
|
+
);
|
|
1770
1941
|
if (latest)
|
|
1771
1942
|
return this.open(latest.conversationId, {
|
|
1772
1943
|
autoUpgradeLegacy: options?.autoUpgradeLegacy ?? true
|
|
@@ -1774,11 +1945,21 @@ var SessionManager = class {
|
|
|
1774
1945
|
this.report("No previous session with content; created a new session");
|
|
1775
1946
|
return this.create();
|
|
1776
1947
|
}
|
|
1777
|
-
/**
|
|
1948
|
+
/**
|
|
1949
|
+
* 重命名已打开或待打开的会话;标题作为独立耐久记录,标题或浏览都不改变正文活动排序。
|
|
1950
|
+
*
|
|
1951
|
+
* 未持有句柄时临时打开并在追加后释放:只改标题不该把会话一直挂在写锁上,
|
|
1952
|
+
* 否则同进程后续的归档、删除与置顶都会被自己占住的锁挡掉。
|
|
1953
|
+
*/
|
|
1778
1954
|
async rename(id, title) {
|
|
1779
1955
|
if (!title.trim()) throw new Error("Session title cannot be empty");
|
|
1956
|
+
const alreadyOpen = this.handles.has(this.resolveSession(id).conversationId);
|
|
1780
1957
|
const handle = await this.open(id, { autoUpgradeLegacy: true });
|
|
1781
|
-
|
|
1958
|
+
try {
|
|
1959
|
+
await handle.store.appendRecord("title", { title: sanitizeSessionTitle(title) });
|
|
1960
|
+
} finally {
|
|
1961
|
+
if (!alreadyOpen) await handle.release();
|
|
1962
|
+
}
|
|
1782
1963
|
}
|
|
1783
1964
|
/** 等候所有存储队列后释放句柄;宿主必须先结束 AgentSession 的工具/摘要清理。 */
|
|
1784
1965
|
async close() {
|
|
@@ -1798,7 +1979,7 @@ var SessionManager = class {
|
|
|
1798
1979
|
for (const source of sources) {
|
|
1799
1980
|
if (!fs5.existsSync(source)) continue;
|
|
1800
1981
|
const before = fs5.statSync(source);
|
|
1801
|
-
const bytes =
|
|
1982
|
+
const bytes = readHistoryBytes(source, this.readOptions);
|
|
1802
1983
|
const identity = createHash4("sha256").update(fs5.realpathSync.native(source)).update("\0").update(bytes).digest("hex");
|
|
1803
1984
|
const importLock = await SessionLock.acquire(
|
|
1804
1985
|
path4.join(this.directory, `import-${identity}.lock`)
|
|
@@ -1824,19 +2005,23 @@ var SessionManager = class {
|
|
|
1824
2005
|
const id = createMessageId();
|
|
1825
2006
|
const target = path4.join(this.directory, `${Date.now()}_${id}.jsonl`);
|
|
1826
2007
|
temp = `${target}.tmp-${createMessageId()}`;
|
|
1827
|
-
const store = await SessionStore.create(
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
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
|
+
);
|
|
1834
2019
|
await store.appendRecord("legacy_import", { source, identity });
|
|
1835
2020
|
await store.appendRecord("title", { title: "\u65E7\u5386\u53F2\u5BFC\u5165" });
|
|
1836
2021
|
for (const message of identifyHistory(raw, identity)) await store.append(message);
|
|
1837
2022
|
await store.load();
|
|
1838
2023
|
const after = fs5.statSync(source);
|
|
1839
|
-
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)) {
|
|
1840
2025
|
this.report("Legacy history is changing; import postponed");
|
|
1841
2026
|
continue;
|
|
1842
2027
|
}
|
|
@@ -1884,17 +2069,22 @@ var SessionManager = class {
|
|
|
1884
2069
|
}));
|
|
1885
2070
|
const exact = all.find((m) => m.conversationId === idOrFileName);
|
|
1886
2071
|
const matches = exact ? [exact] : all.filter((m) => m.conversationId.startsWith(idOrFileName));
|
|
1887
|
-
if (!matches.length)
|
|
1888
|
-
|
|
2072
|
+
if (!matches.length)
|
|
2073
|
+
throw createSessionError("SESSION_NOT_FOUND", `Session not found: ${idOrFileName}`);
|
|
2074
|
+
if (matches.length !== 1)
|
|
2075
|
+
throw createSessionError(
|
|
2076
|
+
"SESSION_ID_AMBIGUOUS",
|
|
2077
|
+
`Ambiguous session prefix: ${idOrFileName}`
|
|
2078
|
+
);
|
|
1889
2079
|
sourceFileName = matches[0].fileName;
|
|
1890
2080
|
}
|
|
1891
2081
|
const sourcePath = path4.join(this.directory, sourceFileName);
|
|
1892
2082
|
if (!fs5.existsSync(sourcePath)) {
|
|
1893
|
-
throw
|
|
2083
|
+
throw createSessionError("SESSION_NOT_FOUND", `Session not found: ${idOrFileName}`);
|
|
1894
2084
|
}
|
|
1895
2085
|
const nativeSource = toNativePhysicalPath(sourcePath);
|
|
1896
2086
|
const beforeStat = fs5.statSync(nativeSource);
|
|
1897
|
-
const bytes =
|
|
2087
|
+
const bytes = readHistoryBytes(nativeSource, this.readOptions);
|
|
1898
2088
|
const upgradeIdentity = createHash4("sha256").update(nativeSource).update("\0").update(bytes).digest("hex");
|
|
1899
2089
|
const existingMeta = (await this.allMetadata()).find(
|
|
1900
2090
|
(m) => m.upgradeIdentity === upgradeIdentity
|
|
@@ -1913,7 +2103,7 @@ var SessionManager = class {
|
|
|
1913
2103
|
if (doubleCheck) {
|
|
1914
2104
|
return this.open(doubleCheck.conversationId);
|
|
1915
2105
|
}
|
|
1916
|
-
const sourceStore = await SessionStore.open(nativeSource);
|
|
2106
|
+
const sourceStore = await SessionStore.open(nativeSource, this.readOptions);
|
|
1917
2107
|
if (sourceStore.getSchemaVersion() !== SESSION_SCHEMA_VERSION_V1) {
|
|
1918
2108
|
const state = await sourceStore.loadState();
|
|
1919
2109
|
return this.open(state.header.conversationId);
|
|
@@ -1980,7 +2170,7 @@ var SessionManager = class {
|
|
|
1980
2170
|
fs5.closeSync(fd);
|
|
1981
2171
|
}
|
|
1982
2172
|
const afterStat = fs5.statSync(nativeSource);
|
|
1983
|
-
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)) {
|
|
1984
2174
|
throw new Error("Source legacy session changed during upgrade; retry aborted");
|
|
1985
2175
|
}
|
|
1986
2176
|
fs5.renameSync(tempPath, targetPath);
|
|
@@ -2143,7 +2333,7 @@ var SessionManager = class {
|
|
|
2143
2333
|
store.setAppendObserver(
|
|
2144
2334
|
(record) => {
|
|
2145
2335
|
const previous = JSON.parse(
|
|
2146
|
-
|
|
2336
|
+
readHistoryBytes(this.metadataPath(store.filePath), this.readOptions).toString("utf8")
|
|
2147
2337
|
);
|
|
2148
2338
|
const currentPrev = record.prevRecordId ?? record.parentId;
|
|
2149
2339
|
if (previous.conversationId !== header.conversationId || currentPrev !== null && currentPrev !== (previous.logRevision ?? previous.revision))
|
|
@@ -2188,10 +2378,28 @@ var SessionManager = class {
|
|
|
2188
2378
|
this.handles.set(header.conversationId, handle);
|
|
2189
2379
|
return handle;
|
|
2190
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
|
+
}
|
|
2191
2397
|
/** 缓存通过文件名、身份、统计与正文 stat 校验;失败交由正文重建。 */
|
|
2192
2398
|
readMetadata(file) {
|
|
2193
2399
|
try {
|
|
2194
|
-
const meta = JSON.parse(
|
|
2400
|
+
const meta = JSON.parse(
|
|
2401
|
+
readHistoryBytes(this.metadataPath(file), this.readOptions).toString("utf8")
|
|
2402
|
+
);
|
|
2195
2403
|
const stat = fs5.statSync(file);
|
|
2196
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)
|
|
2197
2405
|
return meta;
|
|
@@ -2201,7 +2409,7 @@ var SessionManager = class {
|
|
|
2201
2409
|
}
|
|
2202
2410
|
/** 浏览仅解析完整记录;严格打开由 SessionStore 验证,浏览不能改写残片。 */
|
|
2203
2411
|
*readDiscoveryRecords(file) {
|
|
2204
|
-
const iterator = readJsonlLines(file);
|
|
2412
|
+
const iterator = readJsonlLines(file, this.readOptions);
|
|
2205
2413
|
let first = true;
|
|
2206
2414
|
let blankSeen = false;
|
|
2207
2415
|
for (const line of iterator) {
|
|
@@ -2256,7 +2464,8 @@ var SessionManager = class {
|
|
|
2256
2464
|
contentRevision: first.value.recordId,
|
|
2257
2465
|
activeBranchId: MAIN_BRANCH_ID,
|
|
2258
2466
|
activeLeafId: first.value.recordId,
|
|
2259
|
-
archived: false
|
|
2467
|
+
archived: false,
|
|
2468
|
+
pinned: false
|
|
2260
2469
|
};
|
|
2261
2470
|
const branchLeaves = /* @__PURE__ */ new Map();
|
|
2262
2471
|
branchLeaves.set(MAIN_BRANCH_ID, first.value.recordId);
|
|
@@ -2309,6 +2518,8 @@ var SessionManager = class {
|
|
|
2309
2518
|
if (bId) meta.activeBranchId = bId;
|
|
2310
2519
|
} else if (record.type === "archive_state") {
|
|
2311
2520
|
meta.archived = Boolean(record.payload.archived);
|
|
2521
|
+
} else if (record.type === "pin_state") {
|
|
2522
|
+
meta.pinned = Boolean(record.payload.pinned);
|
|
2312
2523
|
}
|
|
2313
2524
|
if (branchLeaves && !isGlobalControlRecordType(record.type)) {
|
|
2314
2525
|
const bId = record.branchId ?? meta.activeBranchId ?? MAIN_BRANCH_ID;
|
|
@@ -2815,6 +3026,7 @@ var CrossProjectSessionCatalog = class {
|
|
|
2815
3026
|
let title = "\u65B0\u4F1A\u8BDD";
|
|
2816
3027
|
let activeBranchId = MAIN_BRANCH_ID;
|
|
2817
3028
|
let archived = false;
|
|
3029
|
+
let pinned = false;
|
|
2818
3030
|
let lastRecordId = headerRecord.recordId;
|
|
2819
3031
|
for (const line of iterator) {
|
|
2820
3032
|
if (!line.text.trim()) continue;
|
|
@@ -2835,6 +3047,8 @@ var CrossProjectSessionCatalog = class {
|
|
|
2835
3047
|
title = String(rec.payload.title);
|
|
2836
3048
|
} else if (rec.type === "archive_state") {
|
|
2837
3049
|
archived = Boolean(rec.payload.archived);
|
|
3050
|
+
} else if (rec.type === "pin_state") {
|
|
3051
|
+
pinned = Boolean(rec.payload.pinned);
|
|
2838
3052
|
} else if (rec.type === "branch_selected") {
|
|
2839
3053
|
activeBranchId = String(rec.payload.branchId);
|
|
2840
3054
|
}
|
|
@@ -2853,6 +3067,7 @@ var CrossProjectSessionCatalog = class {
|
|
|
2853
3067
|
title,
|
|
2854
3068
|
activeBranchId,
|
|
2855
3069
|
archived,
|
|
3070
|
+
pinned,
|
|
2856
3071
|
size: stat.size,
|
|
2857
3072
|
mtimeMs: stat.mtimeMs,
|
|
2858
3073
|
revision: lastRecordId,
|
|
@@ -3259,7 +3474,6 @@ var WorkspaceLeaseCoordinator = class {
|
|
|
3259
3474
|
}
|
|
3260
3475
|
}
|
|
3261
3476
|
};
|
|
3262
|
-
var sharedWorkspaceLeaseCoordinator = new WorkspaceLeaseCoordinator();
|
|
3263
3477
|
|
|
3264
3478
|
// src/context/manager.ts
|
|
3265
3479
|
import { createHash as createHash7 } from "crypto";
|
|
@@ -5991,7 +6205,8 @@ function stopProcessTree(child, nativeGroup) {
|
|
|
5991
6205
|
try {
|
|
5992
6206
|
process.kill(-child.pid, "SIGTERM");
|
|
5993
6207
|
return true;
|
|
5994
|
-
} catch {
|
|
6208
|
+
} catch (error) {
|
|
6209
|
+
if (error.code === "ESRCH") return true;
|
|
5995
6210
|
try {
|
|
5996
6211
|
child.kill("SIGTERM");
|
|
5997
6212
|
} catch {
|
|
@@ -6012,7 +6227,8 @@ function forceProcessTree(child, nativeGroup) {
|
|
|
6012
6227
|
try {
|
|
6013
6228
|
process.kill(-child.pid, "SIGKILL");
|
|
6014
6229
|
return true;
|
|
6015
|
-
} catch {
|
|
6230
|
+
} catch (error) {
|
|
6231
|
+
if (error.code === "ESRCH") return true;
|
|
6016
6232
|
try {
|
|
6017
6233
|
child.kill("SIGKILL");
|
|
6018
6234
|
} catch {
|
|
@@ -6033,13 +6249,31 @@ async function confirmProcessTreeStopped(child, terminationSucceeded, nativeGrou
|
|
|
6033
6249
|
if (process.platform === "win32") return true;
|
|
6034
6250
|
const deadline = Date.now() + 1e3;
|
|
6035
6251
|
while (Date.now() < deadline) {
|
|
6252
|
+
let exists;
|
|
6036
6253
|
try {
|
|
6037
6254
|
process.kill(-child.pid, 0);
|
|
6255
|
+
exists = true;
|
|
6038
6256
|
} catch (error) {
|
|
6039
|
-
|
|
6040
|
-
return
|
|
6257
|
+
const code = error.code;
|
|
6258
|
+
if (code === "ESRCH") return true;
|
|
6259
|
+
if (code !== "EPERM") return false;
|
|
6260
|
+
exists = true;
|
|
6261
|
+
}
|
|
6262
|
+
if (groupHasLiveProcess(child.pid)) {
|
|
6263
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
6264
|
+
continue;
|
|
6041
6265
|
}
|
|
6042
|
-
|
|
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;
|
|
6043
6277
|
}
|
|
6044
6278
|
return false;
|
|
6045
6279
|
}
|
|
@@ -7407,90 +7641,419 @@ var HookRegistry = class {
|
|
|
7407
7641
|
};
|
|
7408
7642
|
|
|
7409
7643
|
// src/runtime/executor/index.ts
|
|
7410
|
-
import { createHash as createHash12 } from "crypto";
|
|
7411
7644
|
import path17 from "path";
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
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
|
-
await action();
|
|
7475
|
-
this.rollbackActions.shift();
|
|
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)) {
|
|
7684
|
+
try {
|
|
7685
|
+
await audit("tool.decided", { decision: "deny", decisionSource: "repeat_block" });
|
|
7686
|
+
await audit("tool.finished", { status: "blocked", errorCode: "REPEAT_BLOCKED" });
|
|
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
|
+
};
|
|
7700
|
+
}
|
|
7701
|
+
return {
|
|
7702
|
+
result: failure(
|
|
7703
|
+
"Identical failed tool call cannot be repeated in this run",
|
|
7704
|
+
"REPEAT_BLOCKED"
|
|
7705
|
+
)
|
|
7706
|
+
};
|
|
7476
7707
|
}
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
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 } : {}
|
|
7720
|
+
});
|
|
7721
|
+
let permitted = decision.decision === "allow";
|
|
7722
|
+
let source = decision.source;
|
|
7723
|
+
let approvalChoice;
|
|
7724
|
+
if (decision.decision === "ask") {
|
|
7725
|
+
let choice = "deny_once";
|
|
7487
7726
|
try {
|
|
7488
|
-
await
|
|
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 });
|
|
7489
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
|
+
};
|
|
7490
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
|
+
};
|
|
7491
7791
|
}
|
|
7492
|
-
|
|
7493
|
-
|
|
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 完成资源回收及结果审计。 */
|
|
7494
8057
|
cancel() {
|
|
7495
8058
|
this.cancellation.abort();
|
|
7496
8059
|
}
|
|
@@ -7725,191 +8288,28 @@ var ToolExecutor = class {
|
|
|
7725
8288
|
}
|
|
7726
8289
|
return failure("Invalid tool execution identity", "INVALID_TARGET");
|
|
7727
8290
|
}
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
"AUDIT_UNAVAILABLE",
|
|
7751
|
-
"after_user_action",
|
|
7752
|
-
{
|
|
7753
|
-
version: 1,
|
|
7754
|
-
dispatchOutcome: "not_sent",
|
|
7755
|
-
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7756
|
-
}
|
|
7757
|
-
);
|
|
7758
|
-
}
|
|
7759
|
-
return failure(
|
|
7760
|
-
"Identical failed tool call cannot be repeated in this run",
|
|
7761
|
-
"REPEAT_BLOCKED"
|
|
7762
|
-
);
|
|
7763
|
-
}
|
|
7764
|
-
const hardDenied = this.hardDenial?.(tool, finalInput);
|
|
7765
|
-
const cached = this.approvalCache.get(request);
|
|
7766
|
-
const decision = this.policy.decide({
|
|
7767
|
-
hardDenied: hardDenied !== void 0,
|
|
7768
|
-
mode: typeof this.mode === "function" ? this.mode() : this.mode,
|
|
7769
|
-
toolName: call.name,
|
|
7770
|
-
capabilities: request.capabilities,
|
|
7771
|
-
dangerous: tool.metadata?.dangerous,
|
|
7772
|
-
cachedApproval: cached === "allow",
|
|
7773
|
-
cachedDenial: cached === "deny",
|
|
7774
|
-
rules: this.permissionRules,
|
|
7775
|
-
...shell ? { shell } : {}
|
|
7776
|
-
});
|
|
7777
|
-
let permitted = decision.decision === "allow";
|
|
7778
|
-
let source = decision.source;
|
|
7779
|
-
let approvalChoice;
|
|
7780
|
-
if (decision.decision === "ask") {
|
|
7781
|
-
let choice = "deny_once";
|
|
7782
|
-
try {
|
|
7783
|
-
choice = await this.approvalChannel?.requestApproval(request, this.signal) ?? "deny_once";
|
|
7784
|
-
} catch {
|
|
7785
|
-
choice = "cancel";
|
|
7786
|
-
}
|
|
7787
|
-
approvalChoice = choice;
|
|
7788
|
-
if (choice === "allow_session" && request.sessionAllowed)
|
|
7789
|
-
this.approvalCache.set(request, "allow");
|
|
7790
|
-
if (choice === "deny_session") this.approvalCache.set(request, "deny");
|
|
7791
|
-
permitted = choice === "allow_once" || choice === "allow_session" && request.sessionAllowed;
|
|
7792
|
-
source = this.approvalChannel ? "human" : "non_interactive";
|
|
7793
|
-
}
|
|
7794
|
-
auditFields = shell ? {
|
|
7795
|
-
shell: shell.interpreter,
|
|
7796
|
-
cwdDigest: createHash12("sha256").update(shell.cwd).digest("hex"),
|
|
7797
|
-
executableDigest: shell.executableDigest,
|
|
7798
|
-
executionFingerprint: shell.executionFingerprint,
|
|
7799
|
-
...commandAuditSummary(shell.command)
|
|
7800
|
-
} : executionIdentity ? {
|
|
7801
|
-
serverIdentity: executionIdentity.serverIdentity,
|
|
7802
|
-
connectionGeneration: executionIdentity.connectionGeneration,
|
|
7803
|
-
catalogEpoch: executionIdentity.catalogEpoch,
|
|
7804
|
-
toolDigest: executionIdentity.toolDigest
|
|
7805
|
-
} : {};
|
|
7806
|
-
try {
|
|
7807
|
-
await audit("tool.decided", {
|
|
7808
|
-
decision: permitted ? "allow" : "deny",
|
|
7809
|
-
decisionSource: source,
|
|
7810
|
-
...approvalChoice ? { approvalChoice } : {},
|
|
7811
|
-
...hardDenied?.fields ?? {},
|
|
7812
|
-
...auditFields
|
|
7813
|
-
});
|
|
7814
|
-
} catch {
|
|
7815
|
-
return failure(
|
|
7816
|
-
"Audit unavailable; tool was not started",
|
|
7817
|
-
"AUDIT_UNAVAILABLE",
|
|
7818
|
-
"after_user_action",
|
|
7819
|
-
{
|
|
7820
|
-
version: 1,
|
|
7821
|
-
dispatchOutcome: "not_sent",
|
|
7822
|
-
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7823
|
-
}
|
|
7824
|
-
);
|
|
7825
|
-
}
|
|
7826
|
-
if (!permitted || this.signal?.aborted) {
|
|
7827
|
-
const code = this.signal?.aborted ? "ABORTED" : hardDenied?.code ?? "PERMISSION_DENIED";
|
|
7828
|
-
try {
|
|
7829
|
-
await audit("tool.finished", { status: "denied", errorCode: code });
|
|
7830
|
-
} catch {
|
|
7831
|
-
return failure("Audit result unavailable", "AUDIT_UNAVAILABLE", "after_user_action", {
|
|
7832
|
-
version: 1,
|
|
7833
|
-
dispatchOutcome: "not_sent",
|
|
7834
|
-
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7835
|
-
});
|
|
7836
|
-
}
|
|
7837
|
-
return failure(
|
|
7838
|
-
this.signal?.aborted ? "Operation cancelled" : hardDenied?.message ?? "Tool permission denied",
|
|
7839
|
-
code,
|
|
7840
|
-
this.signal?.aborted ? "after_user_action" : hardDenied?.retryPolicy ?? "after_user_action"
|
|
7841
|
-
);
|
|
7842
|
-
}
|
|
7843
|
-
if (shell) {
|
|
7844
|
-
try {
|
|
7845
|
-
const next = tool.approvalScope?.(finalInput, this.rootDir, this.executionCwd);
|
|
7846
|
-
if (next?.executionFingerprint !== shell.executionFingerprint) {
|
|
7847
|
-
if (attempt === 0 && next) {
|
|
7848
|
-
try {
|
|
7849
|
-
await audit("tool.target_changed", { status: "redeciding" });
|
|
7850
|
-
} catch {
|
|
7851
|
-
return failure(
|
|
7852
|
-
"Audit unavailable; tool was not started",
|
|
7853
|
-
"AUDIT_UNAVAILABLE",
|
|
7854
|
-
"after_user_action",
|
|
7855
|
-
{
|
|
7856
|
-
version: 1,
|
|
7857
|
-
dispatchOutcome: "not_sent",
|
|
7858
|
-
hostError: { phase: "audit", code: "AUDIT_UNAVAILABLE" }
|
|
7859
|
-
}
|
|
7860
|
-
);
|
|
7861
|
-
}
|
|
7862
|
-
shell = next;
|
|
7863
|
-
continue;
|
|
7864
|
-
}
|
|
7865
|
-
await audit("tool.finished", { status: "target_changed", errorCode: "TARGET_CHANGED" });
|
|
7866
|
-
return failure(
|
|
7867
|
-
"Execution target changed after approval",
|
|
7868
|
-
"TARGET_CHANGED",
|
|
7869
|
-
"after_user_action"
|
|
7870
|
-
);
|
|
7871
|
-
}
|
|
7872
|
-
} catch {
|
|
7873
|
-
return failure(
|
|
7874
|
-
"Execution target changed after approval",
|
|
7875
|
-
"TARGET_CHANGED",
|
|
7876
|
-
"after_user_action"
|
|
7877
|
-
);
|
|
7878
|
-
}
|
|
7879
|
-
}
|
|
7880
|
-
if (executionIdentity) {
|
|
7881
|
-
try {
|
|
7882
|
-
const nextIdentity = tool.executionIdentity?.(finalInput, this.rootDir);
|
|
7883
|
-
if (!nextIdentity || nextIdentity.serverIdentity !== executionIdentity.serverIdentity || nextIdentity.connectionGeneration !== executionIdentity.connectionGeneration || nextIdentity.catalogEpoch !== executionIdentity.catalogEpoch || nextIdentity.toolDigest !== executionIdentity.toolDigest) {
|
|
7884
|
-
await audit("tool.finished", {
|
|
7885
|
-
status: "identity_changed",
|
|
7886
|
-
errorCode: "MCP_TOOL_CHANGED"
|
|
7887
|
-
});
|
|
7888
|
-
return failure(
|
|
7889
|
-
"Tool execution identity changed after approval",
|
|
7890
|
-
"MCP_TOOL_CHANGED",
|
|
7891
|
-
"after_user_action",
|
|
7892
|
-
{
|
|
7893
|
-
version: 1,
|
|
7894
|
-
dispatchOutcome: "not_sent",
|
|
7895
|
-
hostError: { phase: "validation", code: "MCP_TOOL_CHANGED" }
|
|
7896
|
-
}
|
|
7897
|
-
);
|
|
7898
|
-
}
|
|
7899
|
-
} catch {
|
|
7900
|
-
return failure(
|
|
7901
|
-
"Tool execution identity check failed after approval",
|
|
7902
|
-
"MCP_TOOL_CHANGED",
|
|
7903
|
-
"after_user_action",
|
|
7904
|
-
{
|
|
7905
|
-
version: 1,
|
|
7906
|
-
dispatchOutcome: "not_sent"
|
|
7907
|
-
}
|
|
7908
|
-
);
|
|
7909
|
-
}
|
|
7910
|
-
}
|
|
7911
|
-
break;
|
|
7912
|
-
}
|
|
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 ?? {};
|
|
7913
8313
|
try {
|
|
7914
8314
|
await audit("tool.started", { status: "started", ...auditFields });
|
|
7915
8315
|
} catch {
|
|
@@ -7938,6 +8338,7 @@ var ToolExecutor = class {
|
|
|
7938
8338
|
this.signal?.addEventListener("abort", abortFromSession, { once: true });
|
|
7939
8339
|
}
|
|
7940
8340
|
const toolCtx = {
|
|
8341
|
+
leaseCoordinator: this.leaseCoordinator,
|
|
7941
8342
|
workspaceRoot: this.workspaceRoot,
|
|
7942
8343
|
executionCwd: this.executionCwd,
|
|
7943
8344
|
sandbox: this.sandbox,
|
|
@@ -7968,9 +8369,10 @@ var ToolExecutor = class {
|
|
|
7968
8369
|
{ signal: executionController.signal }
|
|
7969
8370
|
);
|
|
7970
8371
|
}
|
|
7971
|
-
const rawResult = tool.metadata?.managesTimeout ? await tool.execute(finalInput, toolCtx) : await
|
|
8372
|
+
const rawResult = tool.metadata?.managesTimeout ? await tool.execute(finalInput, toolCtx) : await withToolTimeout(
|
|
7972
8373
|
tool.execute(finalInput, toolCtx),
|
|
7973
8374
|
call.name,
|
|
8375
|
+
this.toolTimeoutMs,
|
|
7974
8376
|
executionController
|
|
7975
8377
|
);
|
|
7976
8378
|
if (typeof tool.toExecutionResult === "function") {
|
|
@@ -8031,105 +8433,18 @@ var ToolExecutor = class {
|
|
|
8031
8433
|
dispatchOutcome: outcome
|
|
8032
8434
|
};
|
|
8033
8435
|
}
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
retryPolicy: resultBlock.retryPolicy,
|
|
8046
|
-
message: outputStr
|
|
8047
|
-
});
|
|
8048
|
-
}
|
|
8049
|
-
try {
|
|
8050
|
-
for (const hook of this.hooks.get("tool:after")) {
|
|
8051
|
-
await hook(
|
|
8052
|
-
hookCtx,
|
|
8053
|
-
{ id: call.id, name: call.name, input: finalInput },
|
|
8054
|
-
{ output: outputStr, isError }
|
|
8055
|
-
);
|
|
8056
|
-
}
|
|
8057
|
-
} catch {
|
|
8058
|
-
resultBlock.content = JSON.stringify({
|
|
8059
|
-
code: "HOOK_ERROR",
|
|
8060
|
-
retryPolicy: "after_user_action",
|
|
8061
|
-
message: "Tool finished but result processing failed"
|
|
8062
|
-
});
|
|
8063
|
-
resultBlock.isError = true;
|
|
8064
|
-
resultBlock.errorCode = "HOOK_ERROR";
|
|
8065
|
-
resultBlock.retryPolicy = "after_user_action";
|
|
8066
|
-
resultBlock.execution = {
|
|
8067
|
-
version: 1,
|
|
8068
|
-
dispatchOutcome: resultBlock.execution?.dispatchOutcome ?? "failed",
|
|
8069
|
-
hostError: {
|
|
8070
|
-
phase: "hook",
|
|
8071
|
-
code: "HOOK_ERROR"
|
|
8072
|
-
}
|
|
8073
|
-
};
|
|
8074
|
-
}
|
|
8075
|
-
resultBlock.durationMs = Math.max(0, Date.now() - startedAt);
|
|
8076
|
-
if (resultBlock.isError && resultBlock.retryPolicy === "never") {
|
|
8077
|
-
this.neverFailures.add(
|
|
8078
|
-
createHash12("sha256").update(stableJsonStringify([call.name, finalInput])).digest("hex")
|
|
8079
|
-
);
|
|
8080
|
-
}
|
|
8081
|
-
try {
|
|
8082
|
-
await audit("tool.finished", {
|
|
8083
|
-
status: resultBlock.isError ? "failed" : "completed",
|
|
8084
|
-
...resultBlock.errorCode ? { errorCode: resultBlock.errorCode } : {},
|
|
8085
|
-
durationMs: resultBlock.durationMs
|
|
8086
|
-
});
|
|
8087
|
-
} catch {
|
|
8088
|
-
resultBlock.isError = true;
|
|
8089
|
-
resultBlock.errorCode = "OUTCOME_UNKNOWN";
|
|
8090
|
-
resultBlock.retryPolicy = "after_user_action";
|
|
8091
|
-
resultBlock.content = JSON.stringify({
|
|
8092
|
-
code: "OUTCOME_UNKNOWN",
|
|
8093
|
-
retryPolicy: "after_user_action",
|
|
8094
|
-
message: "Tool outcome is unknown because the audit result could not be written"
|
|
8095
|
-
});
|
|
8096
|
-
resultBlock.execution = {
|
|
8097
|
-
version: 1,
|
|
8098
|
-
dispatchOutcome: resultBlock.execution?.dispatchOutcome ?? "succeeded",
|
|
8099
|
-
hostError: {
|
|
8100
|
-
phase: "audit",
|
|
8101
|
-
code: "AUDIT_UNAVAILABLE"
|
|
8102
|
-
}
|
|
8103
|
-
};
|
|
8104
|
-
return resultBlock;
|
|
8105
|
-
}
|
|
8106
|
-
await this.onToolResult?.(tool, finalInput, resultBlock);
|
|
8107
|
-
return resultBlock;
|
|
8108
|
-
}
|
|
8109
|
-
/**
|
|
8110
|
-
* 给工具执行加超时护栏:Promise.race 在超时后reject,循环拿到 isError 结果继续运转,
|
|
8111
|
-
* 不会因某个异步工具(如未来接入的网络/exec 工具)挂起而永久卡死。
|
|
8112
|
-
*
|
|
8113
|
-
* 局限:同步 CPU 密集型实现会阻塞事件循环,timer 同样无法触发;
|
|
8114
|
-
* 因此内置工具仍需避免执行用户可控的同步高复杂度计算。
|
|
8115
|
-
*/
|
|
8116
|
-
withTimeout(promise, toolName, executionController) {
|
|
8117
|
-
let timer;
|
|
8118
|
-
const timeout = new Promise((_, reject) => {
|
|
8119
|
-
timer = setTimeout(() => {
|
|
8120
|
-
const error = new KapibalaError(
|
|
8121
|
-
`Tool '${toolName}' timed out after ${this.toolTimeoutMs}ms`,
|
|
8122
|
-
{
|
|
8123
|
-
code: "OUTCOME_UNKNOWN",
|
|
8124
|
-
retryPolicy: "after_user_action",
|
|
8125
|
-
safeMessage: "Tool timed out; completion of side effects is unknown"
|
|
8126
|
-
}
|
|
8127
|
-
);
|
|
8128
|
-
reject(error);
|
|
8129
|
-
executionController.abort(error);
|
|
8130
|
-
}, 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 }
|
|
8131
8447
|
});
|
|
8132
|
-
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
8133
8448
|
}
|
|
8134
8449
|
/**
|
|
8135
8450
|
* 构建当前会话在派生时刻的父权限策略快照。
|
|
@@ -8728,7 +9043,7 @@ var OpenAICompatibleProvider = class {
|
|
|
8728
9043
|
};
|
|
8729
9044
|
|
|
8730
9045
|
// src/models/anthropic/index.ts
|
|
8731
|
-
import { createHash as
|
|
9046
|
+
import { createHash as createHash14 } from "crypto";
|
|
8732
9047
|
function invalidResponse2() {
|
|
8733
9048
|
throw new ModelError("Invalid Anthropic response state", {
|
|
8734
9049
|
code: "MODEL_INVALID_RESPONSE",
|
|
@@ -8743,7 +9058,7 @@ function wireToolId(id) {
|
|
|
8743
9058
|
stage: "request",
|
|
8744
9059
|
retryable: false
|
|
8745
9060
|
});
|
|
8746
|
-
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)}`;
|
|
8747
9062
|
}
|
|
8748
9063
|
var AnthropicProvider = class {
|
|
8749
9064
|
name = "anthropic";
|
|
@@ -9109,7 +9424,7 @@ var AnthropicProvider = class {
|
|
|
9109
9424
|
};
|
|
9110
9425
|
|
|
9111
9426
|
// src/models/openai-responses/index.ts
|
|
9112
|
-
import { createHash as
|
|
9427
|
+
import { createHash as createHash15 } from "crypto";
|
|
9113
9428
|
function invalid(message = "Invalid Responses output Item") {
|
|
9114
9429
|
throw new ModelError(message, {
|
|
9115
9430
|
code: "MODEL_INVALID_RESPONSE",
|
|
@@ -9130,7 +9445,7 @@ function wireCallId(id) {
|
|
|
9130
9445
|
stage: "request",
|
|
9131
9446
|
retryable: false
|
|
9132
9447
|
});
|
|
9133
|
-
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)}`;
|
|
9134
9449
|
}
|
|
9135
9450
|
function validateItem(item, final) {
|
|
9136
9451
|
if (!item || typeof item !== "object" || !nonempty(item.id)) invalid();
|
|
@@ -9827,13 +10142,99 @@ Follow these key behavioral rules:
|
|
|
9827
10142
|
}
|
|
9828
10143
|
return lines.join("\n");
|
|
9829
10144
|
}
|
|
9830
|
-
getL3Environment() {
|
|
9831
|
-
const platform = os6.platform();
|
|
9832
|
-
return `# Environment Context:
|
|
9833
|
-
- Current Working Directory (Workspace Root): ${this.rootDir}
|
|
9834
|
-
- 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);
|
|
9835
10236
|
}
|
|
9836
|
-
}
|
|
10237
|
+
}
|
|
9837
10238
|
|
|
9838
10239
|
// src/runtime/loop/index.ts
|
|
9839
10240
|
var AgentLoop = class {
|
|
@@ -9917,26 +10318,7 @@ var AgentLoop = class {
|
|
|
9917
10318
|
request = modified;
|
|
9918
10319
|
}
|
|
9919
10320
|
}
|
|
9920
|
-
|
|
9921
|
-
for (const t of request.tools) {
|
|
9922
|
-
const original = toolSnapshot.get(t.name);
|
|
9923
|
-
if (!original) {
|
|
9924
|
-
throw new ModelError(`Hook injected tool '${t.name}' not present in snapshot`, {
|
|
9925
|
-
stage: "request",
|
|
9926
|
-
retryable: false
|
|
9927
|
-
});
|
|
9928
|
-
}
|
|
9929
|
-
if (t.description !== original.description || JSON.stringify(t.parameters) !== JSON.stringify(original.parameters)) {
|
|
9930
|
-
throw new ModelError(
|
|
9931
|
-
`Hook mutated tool definition for '${t.name}' (description or parameters drifted from snapshot)`,
|
|
9932
|
-
{
|
|
9933
|
-
stage: "request",
|
|
9934
|
-
retryable: false
|
|
9935
|
-
}
|
|
9936
|
-
);
|
|
9937
|
-
}
|
|
9938
|
-
}
|
|
9939
|
-
}
|
|
10321
|
+
assertHookToolDefinitionsIntact(request, toolSnapshot);
|
|
9940
10322
|
if (this.prepareRequest) request = yield* this.prepareRequest(request, history);
|
|
9941
10323
|
yield {
|
|
9942
10324
|
type: "step_log",
|
|
@@ -10087,22 +10469,7 @@ var AgentLoop = class {
|
|
|
10087
10469
|
stage: "stream",
|
|
10088
10470
|
retryable: false
|
|
10089
10471
|
});
|
|
10090
|
-
|
|
10091
|
-
history.flatMap(
|
|
10092
|
-
(message) => message.content.filter((block) => block.type === "tool_use").map((block) => block.id)
|
|
10093
|
-
)
|
|
10094
|
-
);
|
|
10095
|
-
for (const call of toolCalls) {
|
|
10096
|
-
if (!call.name?.trim() || !call.input || typeof call.input !== "object" || Array.isArray(call.input))
|
|
10097
|
-
throw new ModelError("Invalid tool call arguments or name; no tools executed", {
|
|
10098
|
-
code: "MODEL_INVALID_RESPONSE",
|
|
10099
|
-
stage: "stream",
|
|
10100
|
-
retryable: false
|
|
10101
|
-
});
|
|
10102
|
-
if (!call.id?.trim() || knownCalls.has(call.id))
|
|
10103
|
-
throw new ModelError("Duplicate tool call ID; no tools executed");
|
|
10104
|
-
knownCalls.add(call.id);
|
|
10105
|
-
}
|
|
10472
|
+
assertToolCallsExecutable(history, toolCalls);
|
|
10106
10473
|
break;
|
|
10107
10474
|
} catch (error) {
|
|
10108
10475
|
if (!(error instanceof ContextOverflowError) || outputSeen || accumulatedText || accumulatedThinking || toolCalls.length || overflowRetried || !this.prepareRequest || this.signal?.aborted)
|
|
@@ -10215,47 +10582,9 @@ var AgentLoop = class {
|
|
|
10215
10582
|
timestamp: Date.now()
|
|
10216
10583
|
};
|
|
10217
10584
|
validateProtocolContent(assistantMessage.content);
|
|
10218
|
-
const
|
|
10219
|
-
const identity = stableSerialize({ ...assistantMessage, content: void 0 });
|
|
10220
|
-
const originalTools = stableSerialize(
|
|
10221
|
-
assistantMessage.content.filter(
|
|
10222
|
-
(block) => block.type === "tool_use" || block.type === "tool_result"
|
|
10223
|
-
)
|
|
10224
|
-
);
|
|
10225
|
-
const privateOutput = assistantMessage.content.some(
|
|
10226
|
-
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.origin || block.signature)
|
|
10227
|
-
);
|
|
10585
|
+
const integrity = snapshotAssistantIntegrity(assistantMessage);
|
|
10228
10586
|
await this.hooks.emit("model:after", hookCtx, { message: assistantMessage });
|
|
10229
|
-
|
|
10230
|
-
throw new ModelError("Hook changed assistant message identity", {
|
|
10231
|
-
stage: "request",
|
|
10232
|
-
retryable: false
|
|
10233
|
-
});
|
|
10234
|
-
if (originalTools !== stableSerialize(
|
|
10235
|
-
assistantMessage.content.filter(
|
|
10236
|
-
(block) => block.type === "tool_use" || block.type === "tool_result"
|
|
10237
|
-
)
|
|
10238
|
-
))
|
|
10239
|
-
throw new ModelError("Hook changed validated tool model output", {
|
|
10240
|
-
stage: "request",
|
|
10241
|
-
retryable: false
|
|
10242
|
-
});
|
|
10243
|
-
if (originalContent !== stableSerialize(assistantMessage.content)) {
|
|
10244
|
-
if (privateOutput || assistantMessage.content.some(
|
|
10245
|
-
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.origin || block.signature)
|
|
10246
|
-
))
|
|
10247
|
-
throw new ModelError("Hook changed signed or private model output", {
|
|
10248
|
-
stage: "request",
|
|
10249
|
-
retryable: false
|
|
10250
|
-
});
|
|
10251
|
-
if (toolCalls.length)
|
|
10252
|
-
throw new ModelError("Hook changed validated tool model output", {
|
|
10253
|
-
stage: "request",
|
|
10254
|
-
retryable: false
|
|
10255
|
-
});
|
|
10256
|
-
for (const block of assistantMessage.content)
|
|
10257
|
-
if (block.type === "text" || block.type === "tool_use") block.protocolMeta = void 0;
|
|
10258
|
-
}
|
|
10587
|
+
assertAssistantMessageUntampered(assistantMessage, integrity);
|
|
10259
10588
|
validateProtocolContent(assistantMessage.content);
|
|
10260
10589
|
if (toolCalls.length === 0) {
|
|
10261
10590
|
history.push(assistantMessage);
|
|
@@ -10660,6 +10989,7 @@ var SessionInbox = class _SessionInbox {
|
|
|
10660
10989
|
|
|
10661
10990
|
// src/runtime/agent/runtime.ts
|
|
10662
10991
|
var AgentRuntime = class {
|
|
10992
|
+
runnerContexts = /* @__PURE__ */ new Map();
|
|
10663
10993
|
tasks = /* @__PURE__ */ new Map();
|
|
10664
10994
|
controllers = /* @__PURE__ */ new Map();
|
|
10665
10995
|
timers = /* @__PURE__ */ new Map();
|
|
@@ -10743,6 +11073,13 @@ var AgentRuntime = class {
|
|
|
10743
11073
|
* - 登记为 preparing 状态并返回 accepted 回执,使父工具事务能立刻闭合。
|
|
10744
11074
|
*/
|
|
10745
11075
|
async prepareSpawn(context, params) {
|
|
11076
|
+
const coordinator = context.leaseCoordinator;
|
|
11077
|
+
if (!coordinator || typeof coordinator !== "object" || typeof coordinator.isLocked !== "function" || typeof coordinator.getActiveLease !== "function" || typeof coordinator.tryAcquire !== "function" || typeof coordinator.acquire !== "function") {
|
|
11078
|
+
throw new KapibalaError("Spawn requires the parent workspace lease coordinator", {
|
|
11079
|
+
code: "CONFIG_INVALID",
|
|
11080
|
+
safeMessage: "\u5FC5\u987B\u663E\u5F0F\u4F20\u5165\u7236\u4F1A\u8BDD\u5199\u79DF\u7EA6\u534F\u8C03\u5668"
|
|
11081
|
+
});
|
|
11082
|
+
}
|
|
10746
11083
|
if (this.isClosed) {
|
|
10747
11084
|
throw new KapibalaError("AgentRuntime is closed", {
|
|
10748
11085
|
code: "CONFIG_INVALID",
|
|
@@ -10885,6 +11222,7 @@ var AgentRuntime = class {
|
|
|
10885
11222
|
timeoutMs
|
|
10886
11223
|
})
|
|
10887
11224
|
};
|
|
11225
|
+
this.runnerContexts.set(agentId, Object.freeze({ leaseCoordinator: context.leaseCoordinator }));
|
|
10888
11226
|
this.tasks.set(agentId, taskRecord);
|
|
10889
11227
|
this.totalActiveCount++;
|
|
10890
11228
|
this.treeActiveCounts.set(rootTaskId, treeActive + 1);
|
|
@@ -10937,6 +11275,7 @@ var AgentRuntime = class {
|
|
|
10937
11275
|
const task = this.tasks.get(agentId);
|
|
10938
11276
|
if (!task) return;
|
|
10939
11277
|
if (task.status === "preparing") {
|
|
11278
|
+
this.runnerContexts.delete(agentId);
|
|
10940
11279
|
task.status = "cancelled";
|
|
10941
11280
|
task.error = "Parent transaction aborted before commit";
|
|
10942
11281
|
task.finishedAt = Date.now();
|
|
@@ -10962,7 +11301,10 @@ var AgentRuntime = class {
|
|
|
10962
11301
|
this.timers.set(task.agentId, timer);
|
|
10963
11302
|
let runError;
|
|
10964
11303
|
try {
|
|
10965
|
-
const
|
|
11304
|
+
const context = this.runnerContexts.get(task.agentId);
|
|
11305
|
+
if (!context)
|
|
11306
|
+
throw new KapibalaError("Runner context is unavailable", { code: "CONFIG_INVALID" });
|
|
11307
|
+
const result = await this.runner(task, controller.signal, context);
|
|
10966
11308
|
if (task.status === "cancelling") {
|
|
10967
11309
|
task.status = "cancelled";
|
|
10968
11310
|
return;
|
|
@@ -11011,14 +11353,17 @@ var AgentRuntime = class {
|
|
|
11011
11353
|
task.status = "failed";
|
|
11012
11354
|
}
|
|
11013
11355
|
task.error = runError.message;
|
|
11014
|
-
await this.cascadeCancelChildren(task.agentId, "Parent task failed");
|
|
11015
11356
|
} finally {
|
|
11357
|
+
this.runnerContexts.delete(task.agentId);
|
|
11016
11358
|
this.clearTaskTimer(task.agentId);
|
|
11017
11359
|
this.controllers.delete(task.agentId);
|
|
11018
11360
|
if (!isTerminalTaskStatus(task.status)) {
|
|
11019
11361
|
task.status = "failed";
|
|
11020
11362
|
task.error = runError?.message ?? "Unknown execution failure";
|
|
11021
11363
|
}
|
|
11364
|
+
if (task.status === "failed") {
|
|
11365
|
+
await this.cascadeCancelChildren(task.agentId, task.error ?? "Parent task failed");
|
|
11366
|
+
}
|
|
11022
11367
|
task.finishedAt = Date.now();
|
|
11023
11368
|
this.releaseTaskSlotOnce(task);
|
|
11024
11369
|
this.outbox.enqueueFromTask(task);
|
|
@@ -11088,6 +11433,7 @@ var AgentRuntime = class {
|
|
|
11088
11433
|
controller?.abort(new Error(reason));
|
|
11089
11434
|
await this.cascadeCancelChildren(agentId, reason);
|
|
11090
11435
|
const inFlight = this.runningPromises.get(agentId);
|
|
11436
|
+
if (!inFlight) this.runnerContexts.delete(agentId);
|
|
11091
11437
|
if (inFlight) {
|
|
11092
11438
|
const GRACE_PERIOD_MS = 5e3;
|
|
11093
11439
|
let timer;
|
|
@@ -11196,6 +11542,7 @@ var AgentRuntime = class {
|
|
|
11196
11542
|
}
|
|
11197
11543
|
this.timers.clear();
|
|
11198
11544
|
this.controllers.clear();
|
|
11545
|
+
this.runnerContexts.clear();
|
|
11199
11546
|
this.stateListeners.clear();
|
|
11200
11547
|
}
|
|
11201
11548
|
/**
|
|
@@ -11336,9 +11683,15 @@ function createSpawnAgentTool(runtime) {
|
|
|
11336
11683
|
required: ["description", "prompt", "role"]
|
|
11337
11684
|
},
|
|
11338
11685
|
async execute(input, ctx) {
|
|
11686
|
+
if (!ctx.leaseCoordinator)
|
|
11687
|
+
throw new KapibalaError("Missing parent lease coordinator", {
|
|
11688
|
+
code: "CONFIG_INVALID",
|
|
11689
|
+
safeMessage: "\u7F3A\u5C11\u7236\u4F1A\u8BDD\u5199\u79DF\u7EA6\u534F\u8C03\u5668"
|
|
11690
|
+
});
|
|
11339
11691
|
const workspaceRoot = ctx.workspaceRoot ?? ctx.rootDir;
|
|
11340
11692
|
const executionCwd = ctx.executionCwd ?? workspaceRoot;
|
|
11341
11693
|
const spawnContext = {
|
|
11694
|
+
leaseCoordinator: ctx.leaseCoordinator,
|
|
11342
11695
|
rootTaskId: ctx.rootTaskId,
|
|
11343
11696
|
sessionId: ctx.sessionId ?? "session-default",
|
|
11344
11697
|
branchId: ctx.branchId ?? "main",
|
|
@@ -11617,6 +11970,274 @@ function cancellableGenerator(create, external) {
|
|
|
11617
11970
|
};
|
|
11618
11971
|
}
|
|
11619
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
|
+
|
|
11620
12241
|
// src/context/session/index.ts
|
|
11621
12242
|
var AgentSession = class {
|
|
11622
12243
|
skillRegistry;
|
|
@@ -11701,41 +12322,27 @@ var AgentSession = class {
|
|
|
11701
12322
|
this.cwd = resolvedWorkspace.executionCwd;
|
|
11702
12323
|
this.projectKey = resolvedWorkspace.projectKey;
|
|
11703
12324
|
this.sandbox = config.sandbox ?? new PathSandbox({ workspaceRoot: this.workspaceRoot });
|
|
11704
|
-
this.leaseCoordinator = config.leaseCoordinator ??
|
|
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;
|
|
11705
12339
|
this.tools = new ToolRegistry();
|
|
11706
12340
|
if (config.skills) {
|
|
11707
12341
|
this.skillRegistry = new SkillRegistry(config.skills);
|
|
11708
12342
|
this.skillRuntime = new SkillRuntime(
|
|
11709
12343
|
this.skillRegistry,
|
|
11710
12344
|
this.tools,
|
|
11711
|
-
|
|
11712
|
-
try {
|
|
11713
|
-
await this.eventLogger.recordAudit({
|
|
11714
|
-
level: "info",
|
|
11715
|
-
event: "skill.resource_read",
|
|
11716
|
-
sessionId: this.sessionId,
|
|
11717
|
-
runId: source.runId,
|
|
11718
|
-
fields: {
|
|
11719
|
-
skillName: source.name,
|
|
11720
|
-
contentDigest: source.contentDigest,
|
|
11721
|
-
invocationId: source.invocationId,
|
|
11722
|
-
pathDigest: skillDigest(input.path),
|
|
11723
|
-
resourceDigest: skillDigest(page.text),
|
|
11724
|
-
outputBytes: Buffer.byteLength(page.text),
|
|
11725
|
-
outputTruncated: page.truncated,
|
|
11726
|
-
offset: input.offset,
|
|
11727
|
-
startLine: input.startLine,
|
|
11728
|
-
endLine: input.endLine
|
|
11729
|
-
}
|
|
11730
|
-
});
|
|
11731
|
-
} catch {
|
|
11732
|
-
throw new KapibalaError("Skill resource audit unavailable", {
|
|
11733
|
-
code: "AUDIT_UNAVAILABLE",
|
|
11734
|
-
retryPolicy: "after_user_action",
|
|
11735
|
-
safeMessage: "\u6280\u80FD\u8D44\u6E90\u8BFB\u53D6\u5BA1\u8BA1\u4E0D\u53EF\u7528\uFF0C\u672C\u8F6E\u505C\u6B62"
|
|
11736
|
-
});
|
|
11737
|
-
}
|
|
11738
|
-
}
|
|
12345
|
+
createSkillResourceAuditor(this.eventLogger, this.sessionId)
|
|
11739
12346
|
);
|
|
11740
12347
|
}
|
|
11741
12348
|
this.hooks = new HookRegistry();
|
|
@@ -11748,19 +12355,6 @@ var AgentSession = class {
|
|
|
11748
12355
|
this.mode = config.mode ?? "Approval";
|
|
11749
12356
|
this.permissionRules = config.permissionRules ?? [];
|
|
11750
12357
|
this.approvalChannel = config.approvalChannel;
|
|
11751
|
-
this.userInstructionsPath = config.userInstructionsPath;
|
|
11752
|
-
const sinks = config.eventLogger ? void 0 : createDefaultLogSinks(config.loggingDirectory);
|
|
11753
|
-
const eventLogger = config.eventLogger ?? new StructuredLogger({
|
|
11754
|
-
operationSink: sinks.operationSink,
|
|
11755
|
-
auditSink: sinks.auditSink,
|
|
11756
|
-
debug: config.logger !== void 0,
|
|
11757
|
-
onDiagnostic: config.onDiagnostic ?? config.logger
|
|
11758
|
-
});
|
|
11759
|
-
this.eventLogger = {
|
|
11760
|
-
record: (input) => eventLogger.record({ ...input, conversationId: this.persistentId }),
|
|
11761
|
-
recordAudit: (input) => eventLogger.recordAudit({ ...input, conversationId: this.persistentId })
|
|
11762
|
-
};
|
|
11763
|
-
this.auditRecoveryDirectory = config.auditRecoveryDirectory ?? sinks?.auditSink.directory;
|
|
11764
12358
|
this.rootTaskId = config.rootTaskId;
|
|
11765
12359
|
this.depth = config.depth ?? 0;
|
|
11766
12360
|
this.agentId = config.agentId;
|
|
@@ -12365,71 +12959,21 @@ var AgentSession = class {
|
|
|
12365
12959
|
runId,
|
|
12366
12960
|
modelId: profile.id
|
|
12367
12961
|
}) : this.context.project(history);
|
|
12368
|
-
|
|
12369
|
-
const original = sourceView.find((source) => source.id === message.id);
|
|
12370
|
-
const privateBag = /* @__PURE__ */ new Map();
|
|
12371
|
-
const blockBag = /* @__PURE__ */ new Map();
|
|
12372
|
-
for (const block of original?.content ?? []) {
|
|
12373
|
-
const serialized = stableSerialize(block);
|
|
12374
|
-
if (block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && block.origin)
|
|
12375
|
-
privateBag.set(serialized, (privateBag.get(serialized) ?? 0) + 1);
|
|
12376
|
-
blockBag.set(serialized, (blockBag.get(serialized) ?? 0) + 1);
|
|
12377
|
-
}
|
|
12378
|
-
const take = (bag, serialized) => {
|
|
12379
|
-
const count = bag.get(serialized) ?? 0;
|
|
12380
|
-
if (!count) return false;
|
|
12381
|
-
if (count === 1) bag.delete(serialized);
|
|
12382
|
-
else bag.set(serialized, count - 1);
|
|
12383
|
-
return true;
|
|
12384
|
-
};
|
|
12385
|
-
for (const block of message.content) {
|
|
12386
|
-
if (block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && block.origin) {
|
|
12387
|
-
if (!original || !take(privateBag, stableSerialize(block)))
|
|
12388
|
-
throw new ModelError("Hook changed signed or private request provenance", {
|
|
12389
|
-
stage: "request",
|
|
12390
|
-
retryable: false
|
|
12391
|
-
});
|
|
12392
|
-
}
|
|
12393
|
-
if (block.type !== "text" && block.type !== "tool_use") continue;
|
|
12394
|
-
if (block.protocolMeta && !take(blockBag, stableSerialize(block)))
|
|
12395
|
-
block.protocolMeta = void 0;
|
|
12396
|
-
}
|
|
12397
|
-
}
|
|
12398
|
-
const frozen = Boolean(
|
|
12399
|
-
previousRequest && history.some(
|
|
12400
|
-
(message) => message.interactionId === runId && message.content.some(
|
|
12401
|
-
(block) => block.type === "provider_state" || block.type === "redacted_thinking" || block.type === "thinking" && Boolean(block.signature && block.origin?.runId === runId)
|
|
12402
|
-
)
|
|
12403
|
-
)
|
|
12404
|
-
);
|
|
12962
|
+
verifyPrivateBlockIntegrity(projected, sourceView);
|
|
12405
12963
|
const candidate = {
|
|
12406
12964
|
...request,
|
|
12407
12965
|
messages: projected,
|
|
12408
12966
|
context: { runId, modelId: profile.id },
|
|
12409
12967
|
signal: options?.signal
|
|
12410
12968
|
};
|
|
12411
|
-
|
|
12412
|
-
|
|
12413
|
-
|
|
12414
|
-
|
|
12415
|
-
|
|
12416
|
-
|
|
12417
|
-
|
|
12418
|
-
|
|
12419
|
-
for (const original of generated) {
|
|
12420
|
-
const matches = candidate.messages.filter((message) => message.id === original.id);
|
|
12421
|
-
if (matches.length !== 1 || stableSerialize(matches[0]) !== stableSerialize(original))
|
|
12422
|
-
throw new ModelError("Signed continuation prefix or tool chain changed", {
|
|
12423
|
-
stage: "request",
|
|
12424
|
-
retryable: false
|
|
12425
|
-
});
|
|
12426
|
-
}
|
|
12427
|
-
}
|
|
12428
|
-
if (frozen && previousRequest && (stableSerialize(candidate.messages.slice(0, previousRequest.messages.length)) !== stableSerialize(previousRequest.messages) || candidate.systemPrompt !== previousRequest.systemPrompt || stableSerialize(candidate.tools) !== stableSerialize(previousRequest.tools)))
|
|
12429
|
-
throw new ModelError("Signed continuation prefix changed", {
|
|
12430
|
-
stage: "request",
|
|
12431
|
-
retryable: false
|
|
12432
|
-
});
|
|
12969
|
+
const frozen = assertSignedContinuationStable({
|
|
12970
|
+
candidate,
|
|
12971
|
+
previousRequest,
|
|
12972
|
+
history,
|
|
12973
|
+
runId,
|
|
12974
|
+
nativeBinding,
|
|
12975
|
+
projectHistory: (projectedHistory) => this.context.project(projectedHistory)
|
|
12976
|
+
});
|
|
12433
12977
|
const context = this.context;
|
|
12434
12978
|
const skills = this.skillRuntime;
|
|
12435
12979
|
const preparation = context.prepare(history, candidate, profile, runId, reason, !frozen);
|
|
@@ -12478,6 +13022,7 @@ var AgentSession = class {
|
|
|
12478
13022
|
let persistedHistoryLength = this.history.length;
|
|
12479
13023
|
let loopCompleted = false;
|
|
12480
13024
|
let loopFailed = false;
|
|
13025
|
+
let consumerInterrupted = false;
|
|
12481
13026
|
try {
|
|
12482
13027
|
for await (const event of loop.run(this.history)) {
|
|
12483
13028
|
await this.logContextEvent(event, runId);
|
|
@@ -12552,7 +13097,12 @@ var AgentSession = class {
|
|
|
12552
13097
|
if (event.type === "message_stop" || event.type === "tool_messages") {
|
|
12553
13098
|
persistedHistoryLength = this.history.length;
|
|
12554
13099
|
}
|
|
12555
|
-
|
|
13100
|
+
try {
|
|
13101
|
+
yield event;
|
|
13102
|
+
} catch (error) {
|
|
13103
|
+
consumerInterrupted = true;
|
|
13104
|
+
throw error;
|
|
13105
|
+
}
|
|
12556
13106
|
}
|
|
12557
13107
|
loopCompleted = true;
|
|
12558
13108
|
} catch (error) {
|
|
@@ -12569,7 +13119,7 @@ var AgentSession = class {
|
|
|
12569
13119
|
}
|
|
12570
13120
|
throw error;
|
|
12571
13121
|
} finally {
|
|
12572
|
-
if (!loopCompleted && !loopFailed) {
|
|
13122
|
+
if (!loopCompleted && (!loopFailed || consumerInterrupted)) {
|
|
12573
13123
|
await executor.rollback();
|
|
12574
13124
|
if (this.store) {
|
|
12575
13125
|
for (const message of this.history.slice(persistedHistoryLength)) {
|
|
@@ -12701,102 +13251,44 @@ var AgentSession = class {
|
|
|
12701
13251
|
}
|
|
12702
13252
|
/** 完整消息提交后完成技能审计;审计失败保留真实历史,停止本轮且不发布成功事件。 */
|
|
12703
13253
|
async commitSkills(runId) {
|
|
12704
|
-
|
|
12705
|
-
|
|
12706
|
-
|
|
12707
|
-
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
sessionId: this.sessionId,
|
|
12712
|
-
runId,
|
|
12713
|
-
fields: {
|
|
12714
|
-
skillName: source.name,
|
|
12715
|
-
skillScope: source.scope,
|
|
12716
|
-
contentDigest: source.contentDigest,
|
|
12717
|
-
renderedDigest: source.renderedDigest,
|
|
12718
|
-
pathDigest: skillDigest(source.filePath),
|
|
12719
|
-
caller: source.caller,
|
|
12720
|
-
invocationId: source.invocationId,
|
|
12721
|
-
messageId
|
|
12722
|
-
}
|
|
12723
|
-
});
|
|
12724
|
-
} catch {
|
|
12725
|
-
throw new KapibalaError("Skill commit audit unavailable", {
|
|
12726
|
-
code: "AUDIT_UNAVAILABLE",
|
|
12727
|
-
retryPolicy: "after_user_action",
|
|
12728
|
-
safeMessage: "\u6280\u80FD\u63D0\u4EA4\u5BA1\u8BA1\u4E0D\u53EF\u7528\uFF0C\u672C\u8F6E\u505C\u6B62"
|
|
12729
|
-
});
|
|
12730
|
-
}
|
|
12731
|
-
},
|
|
12732
|
-
this.store ? "disk" : "memory"
|
|
12733
|
-
);
|
|
13254
|
+
return commitSkillEvents({
|
|
13255
|
+
skillRuntime: this.skillRuntime,
|
|
13256
|
+
eventLogger: this.eventLogger,
|
|
13257
|
+
sessionId: this.sessionId,
|
|
13258
|
+
runId,
|
|
13259
|
+
persistence: this.store ? "disk" : "memory"
|
|
13260
|
+
});
|
|
12734
13261
|
}
|
|
12735
13262
|
/** 显式加载仍按同一读取策略、精确缓存与宿主审批裁决;拒绝/取消/审计失败时不追加用户任务。 */
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
13263
|
+
authorizeSkill(name, runId, signal) {
|
|
13264
|
+
return authorizeExplicitSkill({
|
|
13265
|
+
name,
|
|
13266
|
+
runId,
|
|
13267
|
+
signal,
|
|
12741
13268
|
rootDir: this.rootDir,
|
|
12742
|
-
sessionAllowed: true
|
|
12743
|
-
};
|
|
12744
|
-
const cached = this.approvalCache.get(request);
|
|
12745
|
-
const decision = new PermissionPolicy().decide({
|
|
12746
|
-
toolName: request.toolName,
|
|
12747
|
-
capabilities: request.capabilities,
|
|
12748
13269
|
mode: this.mode,
|
|
12749
|
-
|
|
12750
|
-
|
|
12751
|
-
|
|
12752
|
-
|
|
12753
|
-
|
|
12754
|
-
let approvalChoice;
|
|
12755
|
-
if (decision.decision === "ask") {
|
|
12756
|
-
try {
|
|
12757
|
-
approvalChoice = await this.approvalChannel?.requestApproval(request, signal) ?? "deny_once";
|
|
12758
|
-
} catch {
|
|
12759
|
-
approvalChoice = "cancel";
|
|
12760
|
-
}
|
|
12761
|
-
permitted = approvalChoice === "allow_once" || approvalChoice === "allow_session";
|
|
12762
|
-
if (approvalChoice === "allow_session") this.approvalCache.set(request, "allow");
|
|
12763
|
-
if (approvalChoice === "deny_session") this.approvalCache.set(request, "deny");
|
|
12764
|
-
}
|
|
12765
|
-
await this.eventLogger.recordAudit({
|
|
12766
|
-
level: "info",
|
|
12767
|
-
event: "skill.decided",
|
|
12768
|
-
sessionId: this.sessionId,
|
|
12769
|
-
runId,
|
|
12770
|
-
fields: {
|
|
12771
|
-
skillName: name,
|
|
12772
|
-
caller: "user",
|
|
12773
|
-
decision: permitted && !signal?.aborted ? "allow" : "deny",
|
|
12774
|
-
decisionSource: decision.decision === "ask" ? this.approvalChannel ? "human" : "non_interactive" : decision.source,
|
|
12775
|
-
approvalChoice
|
|
12776
|
-
}
|
|
13270
|
+
permissionRules: this.permissionRules,
|
|
13271
|
+
approvalChannel: this.approvalChannel,
|
|
13272
|
+
approvalCache: this.approvalCache,
|
|
13273
|
+
eventLogger: this.eventLogger,
|
|
13274
|
+
sessionId: this.sessionId
|
|
12777
13275
|
});
|
|
12778
|
-
if (!permitted) throw skillFailure("SKILL_INVOCATION_FORBIDDEN", "\u5BBF\u4E3B\u6743\u9650\u7981\u6B62\u663E\u5F0F\u6280\u80FD\u8BFB\u53D6");
|
|
12779
|
-
signal?.throwIfAborted();
|
|
12780
13276
|
}
|
|
12781
13277
|
/**
|
|
12782
13278
|
* 计算最近一次内部模型请求的上下文占用;无 usage 时显式标注预算估算。
|
|
12783
13279
|
* run 累计 promptTokens 会重复计算每一步都重发的历史,不能用于窗口占用率。
|
|
12784
13280
|
*/
|
|
12785
13281
|
createContextUsage(usedTokens, estimatedUsage = false) {
|
|
12786
|
-
|
|
12787
|
-
|
|
12788
|
-
);
|
|
12789
|
-
return {
|
|
13282
|
+
return computeContextUsage(
|
|
13283
|
+
this.lastPrimaryProfile ?? this.getActiveProfile(),
|
|
12790
13284
|
usedTokens,
|
|
12791
|
-
|
|
12792
|
-
|
|
12793
|
-
estimatedLimit: contextWindow.estimated,
|
|
12794
|
-
...estimatedUsage ? { estimatedUsage: true } : {}
|
|
12795
|
-
};
|
|
13285
|
+
estimatedUsage
|
|
13286
|
+
);
|
|
12796
13287
|
}
|
|
12797
13288
|
async persistAssistant(event) {
|
|
12798
|
-
|
|
12799
|
-
|
|
13289
|
+
await persistAssistantMessage(event, this.store, () => {
|
|
13290
|
+
this.usageKnown = false;
|
|
13291
|
+
});
|
|
12800
13292
|
}
|
|
12801
13293
|
/** 创建上下文协调器并注入当前路由;摘要不经过主任务 Loop,插件结果不默认为可剪裁。 */
|
|
12802
13294
|
createContextManager() {
|
|
@@ -12815,17 +13307,8 @@ var AgentSession = class {
|
|
|
12815
13307
|
}
|
|
12816
13308
|
/** 摘要实际消耗按 attempt 身份独立累加;缺失消耗保留未知,不计入主任务指标。 */
|
|
12817
13309
|
accountSummaryUsage(attemptId, usage) {
|
|
12818
|
-
|
|
12819
|
-
this.
|
|
12820
|
-
if (!usage || ![usage.promptTokens, usage.completionTokens, usage.totalTokens].every(
|
|
12821
|
-
(n) => Number.isSafeInteger(n) && n >= 0
|
|
12822
|
-
)) {
|
|
12823
|
-
this.summaryUsageKnown = false;
|
|
12824
|
-
return;
|
|
12825
|
-
}
|
|
12826
|
-
this.summaryUsage.promptTokens += usage.promptTokens;
|
|
12827
|
-
this.summaryUsage.completionTokens += usage.completionTokens;
|
|
12828
|
-
this.summaryUsage.totalTokens += usage.totalTokens;
|
|
13310
|
+
const outcome = recordSummaryUsage(attemptId, usage, this.summaryAttempts, this.summaryUsage);
|
|
13311
|
+
if (outcome === "unknown") this.summaryUsageKnown = false;
|
|
12829
13312
|
}
|
|
12830
13313
|
/**
|
|
12831
13314
|
* 空闲时手动摘要旧完整前缀,不创建用户任务、不执行工具,不突破当前与最近成功交互。
|
|
@@ -12898,61 +13381,21 @@ var AgentSession = class {
|
|
|
12898
13381
|
}
|
|
12899
13382
|
/** 以模型 attempt 身份幂等累加真实消耗;非法或缺失 usage 保持未知。 */
|
|
12900
13383
|
accountUsage(attemptId, usage) {
|
|
12901
|
-
|
|
12902
|
-
if (
|
|
12903
|
-
(n) => Number.isSafeInteger(n) && n >= 0
|
|
12904
|
-
)) {
|
|
12905
|
-
this.usageKnown = false;
|
|
12906
|
-
return;
|
|
12907
|
-
}
|
|
12908
|
-
this.accountedAttempts.add(attemptId);
|
|
12909
|
-
this.usageStats.promptTokens += usage.promptTokens;
|
|
12910
|
-
this.usageStats.completionTokens += usage.completionTokens;
|
|
12911
|
-
this.usageStats.totalTokens += usage.totalTokens;
|
|
13384
|
+
const outcome = recordUsage(attemptId, usage, this.accountedAttempts, this.usageStats);
|
|
13385
|
+
if (outcome === "unknown") this.usageKnown = false;
|
|
12912
13386
|
}
|
|
12913
13387
|
async persistToolMessages(messages) {
|
|
12914
|
-
|
|
12915
|
-
for (const message of messages) await this.store.append(message);
|
|
13388
|
+
await persistToolMessages(messages, this.store);
|
|
12916
13389
|
}
|
|
12917
13390
|
/** 中止时为缓冲 assistant 的每个 tool_use 生成 OUTCOME_UNKNOWN 结果并入历史,保持事务闭合。 */
|
|
12918
13391
|
closeInterruptedToolCalls(assistantEvent, error) {
|
|
12919
|
-
const message =
|
|
12920
|
-
id: createMessageId(),
|
|
12921
|
-
role: "tool",
|
|
12922
|
-
content: assistantEvent.message.content.filter((block) => block.type === "tool_use").map((toolUse) => ({
|
|
12923
|
-
type: "tool_result",
|
|
12924
|
-
toolUseId: toolUse.id,
|
|
12925
|
-
content: JSON.stringify({
|
|
12926
|
-
code: "OUTCOME_UNKNOWN",
|
|
12927
|
-
retryPolicy: "after_user_action",
|
|
12928
|
-
message: "Tool execution was interrupted; outcome is unknown"
|
|
12929
|
-
}),
|
|
12930
|
-
isError: true,
|
|
12931
|
-
errorCode: "OUTCOME_UNKNOWN",
|
|
12932
|
-
retryPolicy: "after_user_action"
|
|
12933
|
-
})),
|
|
12934
|
-
timestamp: Date.now()
|
|
12935
|
-
};
|
|
13392
|
+
const message = buildInterruptedToolMessage(assistantEvent);
|
|
12936
13393
|
this.history.push(message);
|
|
12937
13394
|
return [message];
|
|
12938
13395
|
}
|
|
12939
13396
|
/** 收集历史中紧跟该 assistant 的连续 tool 消息;仅当覆盖全部 tool_use 时视为已闭合。 */
|
|
12940
13397
|
findClosedToolMessages(assistantEvent) {
|
|
12941
|
-
|
|
12942
|
-
if (assistantIndex < 0) return [];
|
|
12943
|
-
const expected = new Set(
|
|
12944
|
-
assistantEvent.message.content.filter((block) => block.type === "tool_use").map((toolUse) => toolUse.id)
|
|
12945
|
-
);
|
|
12946
|
-
const messages = [];
|
|
12947
|
-
for (let index2 = assistantIndex + 1; index2 < this.history.length; index2++) {
|
|
12948
|
-
const message = this.history[index2];
|
|
12949
|
-
if (message.role !== "tool") break;
|
|
12950
|
-
messages.push(message);
|
|
12951
|
-
for (const block of message.content) {
|
|
12952
|
-
if (block.type === "tool_result") expected.delete(block.toolUseId);
|
|
12953
|
-
}
|
|
12954
|
-
}
|
|
12955
|
-
return expected.size === 0 ? messages : [];
|
|
13398
|
+
return findClosedToolMessages(this.history, assistantEvent);
|
|
12956
13399
|
}
|
|
12957
13400
|
};
|
|
12958
13401
|
export {
|
|
@@ -13029,6 +13472,7 @@ export {
|
|
|
13029
13472
|
createLogId,
|
|
13030
13473
|
createMessageId,
|
|
13031
13474
|
createRunCommandTool,
|
|
13475
|
+
createSessionError,
|
|
13032
13476
|
createSpawnAgentTool,
|
|
13033
13477
|
createWaitAgentTool,
|
|
13034
13478
|
defineTool,
|
|
@@ -13084,7 +13528,6 @@ export {
|
|
|
13084
13528
|
sanitizeModelErrorMessage,
|
|
13085
13529
|
sanitizeSessionTitle,
|
|
13086
13530
|
scanSkillRoot,
|
|
13087
|
-
sharedWorkspaceLeaseCoordinator,
|
|
13088
13531
|
skillDigest,
|
|
13089
13532
|
skillFailure,
|
|
13090
13533
|
skillPathKey,
|