kibi-runtime 2.13.3 → 2.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,10 +19,10 @@ import {
19
19
  openSync as openSync2,
20
20
  readFileSync as readFileSync5,
21
21
  readdirSync as readdirSync2,
22
- realpathSync as realpathSync3,
22
+ realpathSync as realpathSync4,
23
23
  renameSync,
24
24
  rmSync as rmSync2,
25
- statSync as statSync4,
25
+ statSync as statSync5,
26
26
  unlinkSync,
27
27
  writeFileSync as writeFileSync3
28
28
  } from "node:fs";
@@ -102,7 +102,7 @@ function manifestVersion(manifestPath, expectedName) {
102
102
  }
103
103
  function cliVersion() {
104
104
  if (true)
105
- return "2.18.0";
105
+ return "2.20.0";
106
106
  return manifestVersion(path.join(moduleDirectory, "..", "package.json"), "kibi-cli") ?? "unknown";
107
107
  }
108
108
  function coreVersion() {
@@ -1754,9 +1754,15 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1754
1754
  }
1755
1755
 
1756
1756
  // ../cli/src/prolog/store-lock.ts
1757
- import { existsSync as existsSync4, rmSync, writeFileSync } from "node:fs";
1757
+ import {
1758
+ existsSync as existsSync4,
1759
+ realpathSync as realpathSync2,
1760
+ rmSync,
1761
+ statSync as statSync3,
1762
+ writeFileSync
1763
+ } from "node:fs";
1758
1764
  import { readFileSync as readFileSync3 } from "node:fs";
1759
- import { dirname, join } from "node:path";
1765
+ import { basename, dirname, join } from "node:path";
1760
1766
  var cachedBootId;
1761
1767
  function currentBootId() {
1762
1768
  if (cachedBootId !== undefined)
@@ -1798,6 +1804,33 @@ function classifyStoreLockHolder(owner) {
1798
1804
  }
1799
1805
  return "alive";
1800
1806
  }
1807
+ function readStoreLockOwner(storePath) {
1808
+ const candidates = [
1809
+ join(storePath, ".kibi-lock-owner.json"),
1810
+ join(storePath, "rdf", ".kibi-lock-owner.json")
1811
+ ];
1812
+ for (const journalPath of candidates) {
1813
+ if (!existsSync4(journalPath))
1814
+ continue;
1815
+ let parsed;
1816
+ try {
1817
+ parsed = JSON.parse(readFileSync3(journalPath, "utf8"));
1818
+ } catch {
1819
+ continue;
1820
+ }
1821
+ if (parsed === null || typeof parsed !== "object")
1822
+ continue;
1823
+ const record = parsed;
1824
+ const owner = {
1825
+ ...typeof record.pid === "number" ? { pid: record.pid } : {},
1826
+ ...typeof record.workspaceRoot === "string" ? { workspaceRoot: record.workspaceRoot } : {},
1827
+ ...typeof record.bootId === "string" ? { bootId: record.bootId } : {},
1828
+ ...typeof record.startedAt === "string" ? { startedAt: record.startedAt } : {}
1829
+ };
1830
+ return { owner, journalPath };
1831
+ }
1832
+ return null;
1833
+ }
1801
1834
  function breakStoreLock(rdfDirectory) {
1802
1835
  const candidates = [
1803
1836
  join(rdfDirectory, "lock"),
@@ -1818,14 +1851,97 @@ function recordStoreLockTakeover(storePath, brokenPid, reason, at = new Date) {
1818
1851
  `, { mode: 384 });
1819
1852
  } catch {}
1820
1853
  }
1821
- function decideStoreLockTakeover(owner) {
1854
+ function readRdfLockPid(lockPath) {
1855
+ try {
1856
+ const match = /pid\((\d+)\)/.exec(readFileSync3(lockPath, "utf8"));
1857
+ const pid = Number.parseInt(match?.[1] ?? "", 10);
1858
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
1859
+ } catch {
1860
+ return null;
1861
+ }
1862
+ }
1863
+ function noteDeadHolderBeforeAttach(kbPath) {
1864
+ const lockPath = join(kbPath, "rdf", "lock");
1865
+ if (!existsSync4(lockPath))
1866
+ return () => {
1867
+ return;
1868
+ };
1869
+ const lockPid = readRdfLockPid(lockPath);
1870
+ const journal = readStoreLockOwner(kbPath)?.owner ?? null;
1871
+ const owner = journal !== null && (lockPid === null || journal.pid === lockPid) ? journal : lockPid !== null ? { pid: lockPid } : null;
1872
+ const decision = decideStoreLockTakeover(owner, { storePath: kbPath });
1873
+ if (decision.action !== "break")
1874
+ return () => {
1875
+ return;
1876
+ };
1877
+ const brokenPid = owner?.pid ?? null;
1878
+ return () => recordStoreLockTakeover(kbPath, brokenPid, decision.reason);
1879
+ }
1880
+ var FOREIGN_STORE_LOCK_REASON_PREFIX = "journal belongs to another workspace";
1881
+ function existingRealPath(pathname) {
1882
+ try {
1883
+ return realpathSync2(pathname);
1884
+ } catch {
1885
+ return null;
1886
+ }
1887
+ }
1888
+ function sameDirectory(left, right) {
1889
+ try {
1890
+ const a = statSync3(left);
1891
+ const b = statSync3(right);
1892
+ return a.dev === b.dev && a.ino === b.ino;
1893
+ } catch {
1894
+ return false;
1895
+ }
1896
+ }
1897
+ function owningWorkspaceRoot(storePath) {
1898
+ let current = storePath;
1899
+ for (;; ) {
1900
+ const parent = dirname(current);
1901
+ if (parent === current)
1902
+ return null;
1903
+ if (basename(parent) === "branches" && basename(dirname(parent)) === ".kb")
1904
+ return dirname(dirname(parent));
1905
+ current = parent;
1906
+ }
1907
+ }
1908
+ function foreignStoreLockJournalRoot(owner, storePath) {
1909
+ const recorded = owner?.workspaceRoot;
1910
+ if (recorded === undefined || recorded === "")
1911
+ return null;
1912
+ const store = existingRealPath(storePath);
1913
+ if (store === null)
1914
+ return null;
1915
+ const ownWorkspace = owningWorkspaceRoot(store);
1916
+ if (ownWorkspace === null)
1917
+ return null;
1918
+ const lockPid = readRdfLockPid(join(storePath, "rdf", "lock"));
1919
+ if (lockPid !== null && lockPid !== owner?.pid)
1920
+ return null;
1921
+ const recordedPath = existingRealPath(recorded);
1922
+ if (recordedPath === null || recordedPath === store)
1923
+ return null;
1924
+ if (sameDirectory(recordedPath, store))
1925
+ return null;
1926
+ const recordedWorkspace = owningWorkspaceRoot(recordedPath) ?? recordedPath;
1927
+ return recordedWorkspace === ownWorkspace ? null : recorded;
1928
+ }
1929
+ function decideStoreLockTakeover(owner, options = {}) {
1822
1930
  const state = classifyStoreLockHolder(owner);
1931
+ if (state === "dead") {
1932
+ return {
1933
+ action: "break",
1934
+ reason: `recorded holder pid ${owner?.pid ?? "?"} is no longer running`
1935
+ };
1936
+ }
1937
+ const foreignRoot = options.storePath === undefined ? null : foreignStoreLockJournalRoot(owner, options.storePath);
1938
+ if (foreignRoot !== null) {
1939
+ return {
1940
+ action: "break",
1941
+ reason: `${FOREIGN_STORE_LOCK_REASON_PREFIX} ${foreignRoot}`
1942
+ };
1943
+ }
1823
1944
  switch (state) {
1824
- case "dead":
1825
- return {
1826
- action: "break",
1827
- reason: `recorded holder pid ${owner?.pid ?? "?"} is no longer running`
1828
- };
1829
1945
  case "unknown":
1830
1946
  return {
1831
1947
  action: "surface",
@@ -1844,7 +1960,9 @@ async function retryAttachAfterBreakingStaleLock(prolog, kbPath, failed) {
1844
1960
  const storeLocked = record?.storeLocked;
1845
1961
  if (storeLocked === undefined)
1846
1962
  return failed;
1847
- const decision = decideStoreLockTakeover(storeLocked.owner);
1963
+ const decision = decideStoreLockTakeover(storeLocked.owner, {
1964
+ storePath: kbPath
1965
+ });
1848
1966
  if (decision.action !== "break") {
1849
1967
  const holderSummary = storeLocked.owner === null ? decision.reason : `pid ${storeLocked.owner.pid ?? "?"} (${decision.reason})`;
1850
1968
  return {
@@ -1877,7 +1995,7 @@ import {
1877
1995
  mkdirSync,
1878
1996
  readFileSync as readFileSync4,
1879
1997
  readdirSync,
1880
- statSync as statSync3,
1998
+ statSync as statSync4,
1881
1999
  writeFileSync as writeFileSync2
1882
2000
  } from "node:fs";
1883
2001
  import path5 from "node:path";
@@ -1989,14 +2107,14 @@ function isValidBranchName(name) {
1989
2107
  }
1990
2108
 
1991
2109
  // ../cli/src/utils/canonical-path.ts
1992
- import { lstatSync as lstatSync2, realpathSync as realpathSync2 } from "node:fs";
2110
+ import { lstatSync as lstatSync2, realpathSync as realpathSync3 } from "node:fs";
1993
2111
  import path7 from "node:path";
1994
2112
  function canonicalFilesystemPath(candidate) {
1995
2113
  let existing = path7.resolve(candidate);
1996
2114
  const missing = [];
1997
2115
  for (;; ) {
1998
2116
  try {
1999
- return path7.resolve(realpathSync2.native(existing), ...missing);
2117
+ return path7.resolve(realpathSync3.native(existing), ...missing);
2000
2118
  } catch (error) {
2001
2119
  if (error.code !== "ENOENT")
2002
2120
  throw error;
@@ -2054,10 +2172,10 @@ function readJournalGeneration(branchPath) {
2054
2172
  function readEngineAttachmentIdentity(branchPath) {
2055
2173
  if (!existsSync6(branchPath))
2056
2174
  return null;
2057
- const stats = statSync4(branchPath);
2175
+ const stats = statSync5(branchPath);
2058
2176
  let resolved = path8.resolve(branchPath);
2059
2177
  try {
2060
- resolved = realpathSync3(branchPath);
2178
+ resolved = realpathSync4(branchPath);
2061
2179
  } catch {}
2062
2180
  return {
2063
2181
  path: resolved,
@@ -2302,26 +2420,27 @@ function recoverStaleJournalLock(branchPath) {
2302
2420
  const lockPath = path8.join(branchPath, "rdf", "lock");
2303
2421
  if (!existsSync6(lockPath))
2304
2422
  return;
2305
- let contents = "";
2306
- try {
2307
- contents = readFileSync5(lockPath, "utf8");
2308
- } catch {
2423
+ const pid = readRdfLockPid(lockPath);
2424
+ if (pid === null || pid === process.pid)
2309
2425
  return;
2426
+ let reason;
2427
+ if (!isProcessAlive(pid)) {
2428
+ reason = decideStoreLockTakeover({ pid }).reason;
2429
+ } else {
2430
+ const journal = readStoreLockOwner(branchPath)?.owner ?? null;
2431
+ if (journal === null || journal.pid !== pid)
2432
+ return;
2433
+ const decision = decideStoreLockTakeover(journal, {
2434
+ storePath: branchPath
2435
+ });
2436
+ if (decision.action !== "break")
2437
+ return;
2438
+ reason = decision.reason;
2310
2439
  }
2311
- const match = /pid\((\d+)\)/.exec(contents);
2312
- if (!match)
2313
- return;
2314
- const pid = Number.parseInt(match[1] ?? "", 10);
2315
- if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid)
2316
- return;
2440
+ recordStoreLockTakeover(branchPath, pid, reason);
2317
2441
  try {
2318
- process.kill(pid, 0);
2319
- return;
2320
- } catch {
2321
- try {
2322
- rmSync2(lockPath, { force: true });
2323
- } catch {}
2324
- }
2442
+ rmSync2(lockPath, { force: true });
2443
+ } catch {}
2325
2444
  }
2326
2445
  function connectSocket(socketPath, timeoutMs) {
2327
2446
  return new Promise((resolve, reject) => {
@@ -2541,8 +2660,11 @@ class EngineSession {
2541
2660
  }
2542
2661
  async boot() {
2543
2662
  await this.prolog.start();
2663
+ const recordTakeover = noteDeadHolderBeforeAttach(this.branchPath);
2544
2664
  const attached = await this.prolog.query(`kb_attach('${quoteProlog(this.branchPath)}')`);
2545
- if (!attached.success) {
2665
+ if (attached.success)
2666
+ recordTakeover();
2667
+ else {
2546
2668
  const recovered = await retryAttachAfterBreakingStaleLock(this.prolog, this.branchPath, attached);
2547
2669
  if (!recovered.success)
2548
2670
  throw new Error(recovered.error ?? "Failed to attach branch KB");