@kiturone/kapibala 0.0.9 → 0.0.10
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 +71 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -11
- package/dist/index.d.ts +34 -11
- package/dist/index.js +70 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -103,6 +103,7 @@ __export(index_exports, {
|
|
|
103
103
|
createLogId: () => createLogId,
|
|
104
104
|
createMessageId: () => createMessageId,
|
|
105
105
|
createRunCommandTool: () => createRunCommandTool,
|
|
106
|
+
createSessionError: () => createSessionError,
|
|
106
107
|
createSpawnAgentTool: () => createSpawnAgentTool,
|
|
107
108
|
createWaitAgentTool: () => createWaitAgentTool,
|
|
108
109
|
defineTool: () => defineTool,
|
|
@@ -158,7 +159,6 @@ __export(index_exports, {
|
|
|
158
159
|
sanitizeModelErrorMessage: () => sanitizeModelErrorMessage,
|
|
159
160
|
sanitizeSessionTitle: () => sanitizeSessionTitle,
|
|
160
161
|
scanSkillRoot: () => scanSkillRoot,
|
|
161
|
-
sharedWorkspaceLeaseCoordinator: () => sharedWorkspaceLeaseCoordinator,
|
|
162
162
|
skillDigest: () => skillDigest,
|
|
163
163
|
skillFailure: () => skillFailure,
|
|
164
164
|
skillPathKey: () => skillPathKey,
|
|
@@ -308,6 +308,21 @@ var KapibalaError = class extends Error {
|
|
|
308
308
|
this.dispatchOutcome = options?.dispatchOutcome;
|
|
309
309
|
}
|
|
310
310
|
};
|
|
311
|
+
var SESSION_SAFE_MESSAGES = {
|
|
312
|
+
SESSION_LOCKED: "\u4F1A\u8BDD\u5DF2\u88AB\u5176\u4ED6\u6301\u6709\u8005\u5360\u7528",
|
|
313
|
+
SESSION_RECOVERY_IN_PROGRESS: "\u4F1A\u8BDD\u6B63\u5728\u6062\u590D",
|
|
314
|
+
SESSION_LOCK_OWNER_CHANGED: "\u4F1A\u8BDD\u9501\u6301\u6709\u8005\u5DF2\u53D8\u5316",
|
|
315
|
+
SESSION_NOT_FOUND: "\u672A\u627E\u5230\u6307\u5B9A\u4F1A\u8BDD",
|
|
316
|
+
SESSION_ID_AMBIGUOUS: "\u4F1A\u8BDD\u524D\u7F00\u4E0D\u552F\u4E00",
|
|
317
|
+
HISTORY_PAGE_INVALID: "\u5386\u53F2\u9875\u53C2\u6570\u65E0\u6548"
|
|
318
|
+
};
|
|
319
|
+
function createSessionError(code, message) {
|
|
320
|
+
return new KapibalaError(message, {
|
|
321
|
+
code,
|
|
322
|
+
safeMessage: SESSION_SAFE_MESSAGES[code],
|
|
323
|
+
retryPolicy: "after_user_action"
|
|
324
|
+
});
|
|
325
|
+
}
|
|
311
326
|
var PreDispatchError = class extends KapibalaError {
|
|
312
327
|
constructor(message, options) {
|
|
313
328
|
super(message, {
|
|
@@ -1453,19 +1468,23 @@ var SessionLock = class _SessionLock {
|
|
|
1453
1468
|
if (error.code !== "EEXIST") throw error;
|
|
1454
1469
|
const stale = _SessionLock.readOwner(lockPath);
|
|
1455
1470
|
if (!stale || !_SessionLock.isDead(stale.pid))
|
|
1456
|
-
throw
|
|
1471
|
+
throw createSessionError(
|
|
1472
|
+
"SESSION_LOCKED",
|
|
1457
1473
|
`Session locked by a live or unknown owner${stale ? ` (pid=${stale.pid})` : ""}`
|
|
1458
1474
|
);
|
|
1459
1475
|
try {
|
|
1460
1476
|
import_node_fs3.default.mkdirSync(recovery);
|
|
1461
1477
|
} catch {
|
|
1462
|
-
throw
|
|
1478
|
+
throw createSessionError(
|
|
1479
|
+
"SESSION_RECOVERY_IN_PROGRESS",
|
|
1480
|
+
"Session locked: recovery in progress"
|
|
1481
|
+
);
|
|
1463
1482
|
}
|
|
1464
1483
|
try {
|
|
1465
1484
|
import_node_fs3.default.writeFileSync(import_node_path2.default.join(recovery, "owner.json"), JSON.stringify(owner), { flag: "wx" });
|
|
1466
1485
|
const current = _SessionLock.readOwner(lockPath);
|
|
1467
1486
|
if (current?.nonce !== stale.nonce || current.pid !== stale.pid || !_SessionLock.isDead(current.pid)) {
|
|
1468
|
-
throw
|
|
1487
|
+
throw createSessionError("SESSION_LOCK_OWNER_CHANGED", "Session locked: owner changed");
|
|
1469
1488
|
}
|
|
1470
1489
|
const quarantine = `${lockPath}.stale-${(0, import_node_crypto4.randomUUID)()}`;
|
|
1471
1490
|
import_node_fs3.default.renameSync(lockPath, quarantine);
|
|
@@ -1485,7 +1504,8 @@ var SessionLock = class _SessionLock {
|
|
|
1485
1504
|
}
|
|
1486
1505
|
if (installError.code === "EEXIST") {
|
|
1487
1506
|
const winner = _SessionLock.readOwner(lockPath);
|
|
1488
|
-
throw
|
|
1507
|
+
throw createSessionError(
|
|
1508
|
+
"SESSION_LOCKED",
|
|
1489
1509
|
`Session locked by a live or unknown owner${winner ? ` (pid=${winner.pid})` : ""}`
|
|
1490
1510
|
);
|
|
1491
1511
|
}
|
|
@@ -1519,7 +1539,7 @@ var SessionLock = class _SessionLock {
|
|
|
1519
1539
|
if (this.released) return;
|
|
1520
1540
|
const current = _SessionLock.readOwner(this.lockPath);
|
|
1521
1541
|
if (current?.nonce !== this.owner.nonce || current.pid !== this.owner.pid)
|
|
1522
|
-
throw
|
|
1542
|
+
throw createSessionError("SESSION_LOCK_OWNER_CHANGED", "Session lock owner changed");
|
|
1523
1543
|
import_node_fs3.default.unlinkSync(import_node_path2.default.join(this.lockPath, "owner.json"));
|
|
1524
1544
|
import_node_fs3.default.rmdirSync(this.lockPath);
|
|
1525
1545
|
this.released = true;
|
|
@@ -1550,7 +1570,11 @@ var SessionLock = class _SessionLock {
|
|
|
1550
1570
|
/** 无残留或残留进程已死亡时放行;确认存活的回收进行中则明确失败。 */
|
|
1551
1571
|
static ensureNoLiveRecovery(recovery) {
|
|
1552
1572
|
_SessionLock.clearAbandonedRecovery(recovery);
|
|
1553
|
-
if (import_node_fs3.default.existsSync(recovery))
|
|
1573
|
+
if (import_node_fs3.default.existsSync(recovery))
|
|
1574
|
+
throw createSessionError(
|
|
1575
|
+
"SESSION_RECOVERY_IN_PROGRESS",
|
|
1576
|
+
"Session locked: recovery in progress"
|
|
1577
|
+
);
|
|
1554
1578
|
}
|
|
1555
1579
|
/** 仅 ESRCH 表示确定死亡;PID 复用或 EPERM 均保持原锁。 */
|
|
1556
1580
|
static isDead(pid) {
|
|
@@ -1784,7 +1808,7 @@ var SessionManager = class {
|
|
|
1784
1808
|
async list(page = 1, options) {
|
|
1785
1809
|
const pageSize = options?.pageSize ?? 20;
|
|
1786
1810
|
if (!Number.isSafeInteger(page) || page < 1 || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100)
|
|
1787
|
-
throw
|
|
1811
|
+
throw createSessionError("HISTORY_PAGE_INVALID", "History page must be a positive integer");
|
|
1788
1812
|
const all = await this.allMetadata();
|
|
1789
1813
|
const filtered = options?.includeArchived ? all : all.filter((m) => !m.archived);
|
|
1790
1814
|
return {
|
|
@@ -1796,15 +1820,17 @@ var SessionManager = class {
|
|
|
1796
1820
|
}
|
|
1797
1821
|
/** 解析完整 ID 或唯一前缀对应的会话文件与 ID。 */
|
|
1798
1822
|
resolveSession(id) {
|
|
1799
|
-
if (!id || !/^[a-zA-Z0-9-]+$/.test(id))
|
|
1823
|
+
if (!id || !/^[a-zA-Z0-9-]+$/.test(id))
|
|
1824
|
+
throw createSessionError("SESSION_NOT_FOUND", "Session not found");
|
|
1800
1825
|
const all = import_node_fs5.default.readdirSync(this.directory).filter((name) => /^\d+_[a-zA-Z0-9-]+\.jsonl$/.test(name)).map((fileName) => ({
|
|
1801
1826
|
fileName,
|
|
1802
1827
|
conversationId: fileName.slice(fileName.indexOf("_") + 1, -6)
|
|
1803
1828
|
}));
|
|
1804
1829
|
const exact = all.find((m) => m.conversationId === id || m.fileName === id);
|
|
1805
1830
|
const matches = exact ? [exact] : all.filter((m) => m.conversationId.startsWith(id));
|
|
1806
|
-
if (!matches.length) throw
|
|
1807
|
-
if (matches.length !== 1)
|
|
1831
|
+
if (!matches.length) throw createSessionError("SESSION_NOT_FOUND", `Session not found: ${id}`);
|
|
1832
|
+
if (matches.length !== 1)
|
|
1833
|
+
throw createSessionError("SESSION_ID_AMBIGUOUS", `Ambiguous session prefix: ${id}`);
|
|
1808
1834
|
return matches[0];
|
|
1809
1835
|
}
|
|
1810
1836
|
/**
|
|
@@ -2063,13 +2089,18 @@ var SessionManager = class {
|
|
|
2063
2089
|
}));
|
|
2064
2090
|
const exact = all.find((m) => m.conversationId === idOrFileName);
|
|
2065
2091
|
const matches = exact ? [exact] : all.filter((m) => m.conversationId.startsWith(idOrFileName));
|
|
2066
|
-
if (!matches.length)
|
|
2067
|
-
|
|
2092
|
+
if (!matches.length)
|
|
2093
|
+
throw createSessionError("SESSION_NOT_FOUND", `Session not found: ${idOrFileName}`);
|
|
2094
|
+
if (matches.length !== 1)
|
|
2095
|
+
throw createSessionError(
|
|
2096
|
+
"SESSION_ID_AMBIGUOUS",
|
|
2097
|
+
`Ambiguous session prefix: ${idOrFileName}`
|
|
2098
|
+
);
|
|
2068
2099
|
sourceFileName = matches[0].fileName;
|
|
2069
2100
|
}
|
|
2070
2101
|
const sourcePath = import_node_path4.default.join(this.directory, sourceFileName);
|
|
2071
2102
|
if (!import_node_fs5.default.existsSync(sourcePath)) {
|
|
2072
|
-
throw
|
|
2103
|
+
throw createSessionError("SESSION_NOT_FOUND", `Session not found: ${idOrFileName}`);
|
|
2073
2104
|
}
|
|
2074
2105
|
const nativeSource = toNativePhysicalPath(sourcePath);
|
|
2075
2106
|
const beforeStat = import_node_fs5.default.statSync(nativeSource);
|
|
@@ -3438,7 +3469,6 @@ var WorkspaceLeaseCoordinator = class {
|
|
|
3438
3469
|
}
|
|
3439
3470
|
}
|
|
3440
3471
|
};
|
|
3441
|
-
var sharedWorkspaceLeaseCoordinator = new WorkspaceLeaseCoordinator();
|
|
3442
3472
|
|
|
3443
3473
|
// src/context/manager.ts
|
|
3444
3474
|
var import_node_crypto10 = require("crypto");
|
|
@@ -8117,6 +8147,7 @@ var ToolExecutor = class {
|
|
|
8117
8147
|
this.signal?.addEventListener("abort", abortFromSession, { once: true });
|
|
8118
8148
|
}
|
|
8119
8149
|
const toolCtx = {
|
|
8150
|
+
leaseCoordinator: this.leaseCoordinator,
|
|
8120
8151
|
workspaceRoot: this.workspaceRoot,
|
|
8121
8152
|
executionCwd: this.executionCwd,
|
|
8122
8153
|
sandbox: this.sandbox,
|
|
@@ -10839,6 +10870,7 @@ var SessionInbox = class _SessionInbox {
|
|
|
10839
10870
|
|
|
10840
10871
|
// src/runtime/agent/runtime.ts
|
|
10841
10872
|
var AgentRuntime = class {
|
|
10873
|
+
runnerContexts = /* @__PURE__ */ new Map();
|
|
10842
10874
|
tasks = /* @__PURE__ */ new Map();
|
|
10843
10875
|
controllers = /* @__PURE__ */ new Map();
|
|
10844
10876
|
timers = /* @__PURE__ */ new Map();
|
|
@@ -10922,6 +10954,13 @@ var AgentRuntime = class {
|
|
|
10922
10954
|
* - 登记为 preparing 状态并返回 accepted 回执,使父工具事务能立刻闭合。
|
|
10923
10955
|
*/
|
|
10924
10956
|
async prepareSpawn(context, params) {
|
|
10957
|
+
const coordinator = context.leaseCoordinator;
|
|
10958
|
+
if (!coordinator || typeof coordinator !== "object" || typeof coordinator.isLocked !== "function" || typeof coordinator.getActiveLease !== "function" || typeof coordinator.tryAcquire !== "function" || typeof coordinator.acquire !== "function") {
|
|
10959
|
+
throw new KapibalaError("Spawn requires the parent workspace lease coordinator", {
|
|
10960
|
+
code: "CONFIG_INVALID",
|
|
10961
|
+
safeMessage: "\u5FC5\u987B\u663E\u5F0F\u4F20\u5165\u7236\u4F1A\u8BDD\u5199\u79DF\u7EA6\u534F\u8C03\u5668"
|
|
10962
|
+
});
|
|
10963
|
+
}
|
|
10925
10964
|
if (this.isClosed) {
|
|
10926
10965
|
throw new KapibalaError("AgentRuntime is closed", {
|
|
10927
10966
|
code: "CONFIG_INVALID",
|
|
@@ -11064,6 +11103,7 @@ var AgentRuntime = class {
|
|
|
11064
11103
|
timeoutMs
|
|
11065
11104
|
})
|
|
11066
11105
|
};
|
|
11106
|
+
this.runnerContexts.set(agentId, Object.freeze({ leaseCoordinator: context.leaseCoordinator }));
|
|
11067
11107
|
this.tasks.set(agentId, taskRecord);
|
|
11068
11108
|
this.totalActiveCount++;
|
|
11069
11109
|
this.treeActiveCounts.set(rootTaskId, treeActive + 1);
|
|
@@ -11116,6 +11156,7 @@ var AgentRuntime = class {
|
|
|
11116
11156
|
const task = this.tasks.get(agentId);
|
|
11117
11157
|
if (!task) return;
|
|
11118
11158
|
if (task.status === "preparing") {
|
|
11159
|
+
this.runnerContexts.delete(agentId);
|
|
11119
11160
|
task.status = "cancelled";
|
|
11120
11161
|
task.error = "Parent transaction aborted before commit";
|
|
11121
11162
|
task.finishedAt = Date.now();
|
|
@@ -11141,7 +11182,10 @@ var AgentRuntime = class {
|
|
|
11141
11182
|
this.timers.set(task.agentId, timer);
|
|
11142
11183
|
let runError;
|
|
11143
11184
|
try {
|
|
11144
|
-
const
|
|
11185
|
+
const context = this.runnerContexts.get(task.agentId);
|
|
11186
|
+
if (!context)
|
|
11187
|
+
throw new KapibalaError("Runner context is unavailable", { code: "CONFIG_INVALID" });
|
|
11188
|
+
const result = await this.runner(task, controller.signal, context);
|
|
11145
11189
|
if (task.status === "cancelling") {
|
|
11146
11190
|
task.status = "cancelled";
|
|
11147
11191
|
return;
|
|
@@ -11192,6 +11236,7 @@ var AgentRuntime = class {
|
|
|
11192
11236
|
task.error = runError.message;
|
|
11193
11237
|
await this.cascadeCancelChildren(task.agentId, "Parent task failed");
|
|
11194
11238
|
} finally {
|
|
11239
|
+
this.runnerContexts.delete(task.agentId);
|
|
11195
11240
|
this.clearTaskTimer(task.agentId);
|
|
11196
11241
|
this.controllers.delete(task.agentId);
|
|
11197
11242
|
if (!isTerminalTaskStatus(task.status)) {
|
|
@@ -11267,6 +11312,7 @@ var AgentRuntime = class {
|
|
|
11267
11312
|
controller?.abort(new Error(reason));
|
|
11268
11313
|
await this.cascadeCancelChildren(agentId, reason);
|
|
11269
11314
|
const inFlight = this.runningPromises.get(agentId);
|
|
11315
|
+
if (!inFlight) this.runnerContexts.delete(agentId);
|
|
11270
11316
|
if (inFlight) {
|
|
11271
11317
|
const GRACE_PERIOD_MS = 5e3;
|
|
11272
11318
|
let timer;
|
|
@@ -11375,6 +11421,7 @@ var AgentRuntime = class {
|
|
|
11375
11421
|
}
|
|
11376
11422
|
this.timers.clear();
|
|
11377
11423
|
this.controllers.clear();
|
|
11424
|
+
this.runnerContexts.clear();
|
|
11378
11425
|
this.stateListeners.clear();
|
|
11379
11426
|
}
|
|
11380
11427
|
/**
|
|
@@ -11515,9 +11562,15 @@ function createSpawnAgentTool(runtime) {
|
|
|
11515
11562
|
required: ["description", "prompt", "role"]
|
|
11516
11563
|
},
|
|
11517
11564
|
async execute(input, ctx) {
|
|
11565
|
+
if (!ctx.leaseCoordinator)
|
|
11566
|
+
throw new KapibalaError("Missing parent lease coordinator", {
|
|
11567
|
+
code: "CONFIG_INVALID",
|
|
11568
|
+
safeMessage: "\u7F3A\u5C11\u7236\u4F1A\u8BDD\u5199\u79DF\u7EA6\u534F\u8C03\u5668"
|
|
11569
|
+
});
|
|
11518
11570
|
const workspaceRoot = ctx.workspaceRoot ?? ctx.rootDir;
|
|
11519
11571
|
const executionCwd = ctx.executionCwd ?? workspaceRoot;
|
|
11520
11572
|
const spawnContext = {
|
|
11573
|
+
leaseCoordinator: ctx.leaseCoordinator,
|
|
11521
11574
|
rootTaskId: ctx.rootTaskId,
|
|
11522
11575
|
sessionId: ctx.sessionId ?? "session-default",
|
|
11523
11576
|
branchId: ctx.branchId ?? "main",
|
|
@@ -11880,7 +11933,7 @@ var AgentSession = class {
|
|
|
11880
11933
|
this.cwd = resolvedWorkspace.executionCwd;
|
|
11881
11934
|
this.projectKey = resolvedWorkspace.projectKey;
|
|
11882
11935
|
this.sandbox = config.sandbox ?? new PathSandbox({ workspaceRoot: this.workspaceRoot });
|
|
11883
|
-
this.leaseCoordinator = config.leaseCoordinator ??
|
|
11936
|
+
this.leaseCoordinator = config.leaseCoordinator ?? new WorkspaceLeaseCoordinator();
|
|
11884
11937
|
this.tools = new ToolRegistry();
|
|
11885
11938
|
if (config.skills) {
|
|
11886
11939
|
this.skillRegistry = new SkillRegistry(config.skills);
|
|
@@ -13209,6 +13262,7 @@ var AgentSession = class {
|
|
|
13209
13262
|
createLogId,
|
|
13210
13263
|
createMessageId,
|
|
13211
13264
|
createRunCommandTool,
|
|
13265
|
+
createSessionError,
|
|
13212
13266
|
createSpawnAgentTool,
|
|
13213
13267
|
createWaitAgentTool,
|
|
13214
13268
|
defineTool,
|
|
@@ -13264,7 +13318,6 @@ var AgentSession = class {
|
|
|
13264
13318
|
sanitizeModelErrorMessage,
|
|
13265
13319
|
sanitizeSessionTitle,
|
|
13266
13320
|
scanSkillRoot,
|
|
13267
|
-
sharedWorkspaceLeaseCoordinator,
|
|
13268
13321
|
skillDigest,
|
|
13269
13322
|
skillFailure,
|
|
13270
13323
|
skillPathKey,
|