@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 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 new Error(
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 new Error("Session locked: recovery in progress");
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 new Error("Session locked: owner changed");
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 new Error(
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 new Error("Session lock owner changed");
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)) throw new Error("Session locked: recovery in progress");
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 new Error("History page must be a positive integer");
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)) throw new Error("Session not found");
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 new Error(`Session not found: ${id}`);
1807
- if (matches.length !== 1) throw new Error(`Ambiguous session prefix: ${id}`);
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) throw new Error(`Session not found: ${idOrFileName}`);
2067
- if (matches.length !== 1) throw new Error(`Ambiguous session prefix: ${idOrFileName}`);
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 new Error(`Session not found: ${idOrFileName}`);
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 result = await this.runner(task, controller.signal);
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 ?? sharedWorkspaceLeaseCoordinator;
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,