@kiturone/kapibala 0.0.8 → 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) {
@@ -1777,30 +1801,36 @@ var SessionManager = class {
1777
1801
  throw error;
1778
1802
  }
1779
1803
  }
1780
- /** 每页 20 项按正文活动排序;有效 sidecar 只读元数据,不读取完整 JSONL。默认排除已归档会话。 */
1804
+ /**
1805
+ * 按正文活动排序分页;有效 sidecar 只读元数据,不读取完整 JSONL。默认排除已归档会话。
1806
+ * pageSize 由调用方显式给出时必须在 1–100 之间;非法页参数直接失败,不静默修正。
1807
+ */
1781
1808
  async list(page = 1, options) {
1782
- if (!Number.isSafeInteger(page) || page < 1)
1783
- throw new Error("History page must be a positive integer");
1809
+ const pageSize = options?.pageSize ?? 20;
1810
+ if (!Number.isSafeInteger(page) || page < 1 || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100)
1811
+ throw createSessionError("HISTORY_PAGE_INVALID", "History page must be a positive integer");
1784
1812
  const all = await this.allMetadata();
1785
1813
  const filtered = options?.includeArchived ? all : all.filter((m) => !m.archived);
1786
1814
  return {
1787
- items: filtered.slice((page - 1) * 20, page * 20),
1815
+ items: filtered.slice((page - 1) * pageSize, page * pageSize),
1788
1816
  total: filtered.length,
1789
1817
  page,
1790
- pages: Math.max(1, Math.ceil(filtered.length / 20))
1818
+ pages: Math.max(1, Math.ceil(filtered.length / pageSize))
1791
1819
  };
1792
1820
  }
1793
1821
  /** 解析完整 ID 或唯一前缀对应的会话文件与 ID。 */
1794
1822
  resolveSession(id) {
1795
- 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");
1796
1825
  const all = import_node_fs5.default.readdirSync(this.directory).filter((name) => /^\d+_[a-zA-Z0-9-]+\.jsonl$/.test(name)).map((fileName) => ({
1797
1826
  fileName,
1798
1827
  conversationId: fileName.slice(fileName.indexOf("_") + 1, -6)
1799
1828
  }));
1800
1829
  const exact = all.find((m) => m.conversationId === id || m.fileName === id);
1801
1830
  const matches = exact ? [exact] : all.filter((m) => m.conversationId.startsWith(id));
1802
- if (!matches.length) throw new Error(`Session not found: ${id}`);
1803
- 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}`);
1804
1834
  return matches[0];
1805
1835
  }
1806
1836
  /**
@@ -2059,13 +2089,18 @@ var SessionManager = class {
2059
2089
  }));
2060
2090
  const exact = all.find((m) => m.conversationId === idOrFileName);
2061
2091
  const matches = exact ? [exact] : all.filter((m) => m.conversationId.startsWith(idOrFileName));
2062
- if (!matches.length) throw new Error(`Session not found: ${idOrFileName}`);
2063
- 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
+ );
2064
2099
  sourceFileName = matches[0].fileName;
2065
2100
  }
2066
2101
  const sourcePath = import_node_path4.default.join(this.directory, sourceFileName);
2067
2102
  if (!import_node_fs5.default.existsSync(sourcePath)) {
2068
- throw new Error(`Session not found: ${idOrFileName}`);
2103
+ throw createSessionError("SESSION_NOT_FOUND", `Session not found: ${idOrFileName}`);
2069
2104
  }
2070
2105
  const nativeSource = toNativePhysicalPath(sourcePath);
2071
2106
  const beforeStat = import_node_fs5.default.statSync(nativeSource);
@@ -3434,7 +3469,6 @@ var WorkspaceLeaseCoordinator = class {
3434
3469
  }
3435
3470
  }
3436
3471
  };
3437
- var sharedWorkspaceLeaseCoordinator = new WorkspaceLeaseCoordinator();
3438
3472
 
3439
3473
  // src/context/manager.ts
3440
3474
  var import_node_crypto10 = require("crypto");
@@ -8113,6 +8147,7 @@ var ToolExecutor = class {
8113
8147
  this.signal?.addEventListener("abort", abortFromSession, { once: true });
8114
8148
  }
8115
8149
  const toolCtx = {
8150
+ leaseCoordinator: this.leaseCoordinator,
8116
8151
  workspaceRoot: this.workspaceRoot,
8117
8152
  executionCwd: this.executionCwd,
8118
8153
  sandbox: this.sandbox,
@@ -10835,6 +10870,7 @@ var SessionInbox = class _SessionInbox {
10835
10870
 
10836
10871
  // src/runtime/agent/runtime.ts
10837
10872
  var AgentRuntime = class {
10873
+ runnerContexts = /* @__PURE__ */ new Map();
10838
10874
  tasks = /* @__PURE__ */ new Map();
10839
10875
  controllers = /* @__PURE__ */ new Map();
10840
10876
  timers = /* @__PURE__ */ new Map();
@@ -10918,6 +10954,13 @@ var AgentRuntime = class {
10918
10954
  * - 登记为 preparing 状态并返回 accepted 回执,使父工具事务能立刻闭合。
10919
10955
  */
10920
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
+ }
10921
10964
  if (this.isClosed) {
10922
10965
  throw new KapibalaError("AgentRuntime is closed", {
10923
10966
  code: "CONFIG_INVALID",
@@ -11060,6 +11103,7 @@ var AgentRuntime = class {
11060
11103
  timeoutMs
11061
11104
  })
11062
11105
  };
11106
+ this.runnerContexts.set(agentId, Object.freeze({ leaseCoordinator: context.leaseCoordinator }));
11063
11107
  this.tasks.set(agentId, taskRecord);
11064
11108
  this.totalActiveCount++;
11065
11109
  this.treeActiveCounts.set(rootTaskId, treeActive + 1);
@@ -11112,6 +11156,7 @@ var AgentRuntime = class {
11112
11156
  const task = this.tasks.get(agentId);
11113
11157
  if (!task) return;
11114
11158
  if (task.status === "preparing") {
11159
+ this.runnerContexts.delete(agentId);
11115
11160
  task.status = "cancelled";
11116
11161
  task.error = "Parent transaction aborted before commit";
11117
11162
  task.finishedAt = Date.now();
@@ -11137,7 +11182,10 @@ var AgentRuntime = class {
11137
11182
  this.timers.set(task.agentId, timer);
11138
11183
  let runError;
11139
11184
  try {
11140
- 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);
11141
11189
  if (task.status === "cancelling") {
11142
11190
  task.status = "cancelled";
11143
11191
  return;
@@ -11188,6 +11236,7 @@ var AgentRuntime = class {
11188
11236
  task.error = runError.message;
11189
11237
  await this.cascadeCancelChildren(task.agentId, "Parent task failed");
11190
11238
  } finally {
11239
+ this.runnerContexts.delete(task.agentId);
11191
11240
  this.clearTaskTimer(task.agentId);
11192
11241
  this.controllers.delete(task.agentId);
11193
11242
  if (!isTerminalTaskStatus(task.status)) {
@@ -11263,6 +11312,7 @@ var AgentRuntime = class {
11263
11312
  controller?.abort(new Error(reason));
11264
11313
  await this.cascadeCancelChildren(agentId, reason);
11265
11314
  const inFlight = this.runningPromises.get(agentId);
11315
+ if (!inFlight) this.runnerContexts.delete(agentId);
11266
11316
  if (inFlight) {
11267
11317
  const GRACE_PERIOD_MS = 5e3;
11268
11318
  let timer;
@@ -11371,6 +11421,7 @@ var AgentRuntime = class {
11371
11421
  }
11372
11422
  this.timers.clear();
11373
11423
  this.controllers.clear();
11424
+ this.runnerContexts.clear();
11374
11425
  this.stateListeners.clear();
11375
11426
  }
11376
11427
  /**
@@ -11511,9 +11562,15 @@ function createSpawnAgentTool(runtime) {
11511
11562
  required: ["description", "prompt", "role"]
11512
11563
  },
11513
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
+ });
11514
11570
  const workspaceRoot = ctx.workspaceRoot ?? ctx.rootDir;
11515
11571
  const executionCwd = ctx.executionCwd ?? workspaceRoot;
11516
11572
  const spawnContext = {
11573
+ leaseCoordinator: ctx.leaseCoordinator,
11517
11574
  rootTaskId: ctx.rootTaskId,
11518
11575
  sessionId: ctx.sessionId ?? "session-default",
11519
11576
  branchId: ctx.branchId ?? "main",
@@ -11876,7 +11933,7 @@ var AgentSession = class {
11876
11933
  this.cwd = resolvedWorkspace.executionCwd;
11877
11934
  this.projectKey = resolvedWorkspace.projectKey;
11878
11935
  this.sandbox = config.sandbox ?? new PathSandbox({ workspaceRoot: this.workspaceRoot });
11879
- this.leaseCoordinator = config.leaseCoordinator ?? sharedWorkspaceLeaseCoordinator;
11936
+ this.leaseCoordinator = config.leaseCoordinator ?? new WorkspaceLeaseCoordinator();
11880
11937
  this.tools = new ToolRegistry();
11881
11938
  if (config.skills) {
11882
11939
  this.skillRegistry = new SkillRegistry(config.skills);
@@ -13205,6 +13262,7 @@ var AgentSession = class {
13205
13262
  createLogId,
13206
13263
  createMessageId,
13207
13264
  createRunCommandTool,
13265
+ createSessionError,
13208
13266
  createSpawnAgentTool,
13209
13267
  createWaitAgentTool,
13210
13268
  defineTool,
@@ -13260,7 +13318,6 @@ var AgentSession = class {
13260
13318
  sanitizeModelErrorMessage,
13261
13319
  sanitizeSessionTitle,
13262
13320
  scanSkillRoot,
13263
- sharedWorkspaceLeaseCoordinator,
13264
13321
  skillDigest,
13265
13322
  skillFailure,
13266
13323
  skillPathKey,