kibi-runtime 2.13.2 → 2.14.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.
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // ../cli/src/engine-daemon.ts
6
- import { writeFileSync as writeFileSync3 } from "node:fs";
6
+ import { writeFileSync as writeFileSync4 } from "node:fs";
7
7
  import path9 from "node:path";
8
8
  import { pathToFileURL } from "node:url";
9
9
 
@@ -24,7 +24,7 @@ import {
24
24
  rmSync as rmSync2,
25
25
  statSync as statSync4,
26
26
  unlinkSync,
27
- writeFileSync as writeFileSync2
27
+ writeFileSync as writeFileSync3
28
28
  } from "node:fs";
29
29
  import * as net from "node:net";
30
30
  import * as os from "node:os";
@@ -102,7 +102,7 @@ function manifestVersion(manifestPath, expectedName) {
102
102
  }
103
103
  function cliVersion() {
104
104
  if (true)
105
- return "2.17.0";
105
+ return "2.19.0";
106
106
  return manifestVersion(path.join(moduleDirectory, "..", "package.json"), "kibi-cli") ?? "unknown";
107
107
  }
108
108
  function coreVersion() {
@@ -1576,7 +1576,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1576
1576
  child.kill(signal);
1577
1577
  } catch {}
1578
1578
  }
1579
- async terminateProcessTree(child) {
1579
+ async terminateProcessTree(child, graceMs = 0) {
1580
1580
  let closed = false;
1581
1581
  const exited = new Promise((resolve) => {
1582
1582
  const markExited = () => {
@@ -1586,6 +1586,19 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1586
1586
  child.once("close", markExited);
1587
1587
  child.once("error", markExited);
1588
1588
  });
1589
+ if (graceMs > 0 && child.exitCode === null && child.signalCode === null) {
1590
+ let graceTimer;
1591
+ await Promise.race([
1592
+ exited,
1593
+ new Promise((resolve) => {
1594
+ graceTimer = setTimeout(resolve, graceMs);
1595
+ })
1596
+ ]);
1597
+ if (graceTimer)
1598
+ clearTimeout(graceTimer);
1599
+ if (closed)
1600
+ return;
1601
+ }
1589
1602
  if (child.exitCode === null)
1590
1603
  this.signalProcessTree(child, "SIGTERM");
1591
1604
  await Promise.race([
@@ -1707,7 +1720,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1707
1720
  }
1708
1721
  return new PrologProcessTerminatedError(this.terminationReason ?? "process exited");
1709
1722
  }
1710
- async terminate() {
1723
+ async terminate(options = {}) {
1711
1724
  if (this.interactiveStarted) {
1712
1725
  this.terminationReason ??= "terminated";
1713
1726
  }
@@ -1727,7 +1740,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1727
1740
  this.terminationPromise = (async () => {
1728
1741
  if (current) {
1729
1742
  current.stdin?.end();
1730
- await this.terminateProcessTree(current);
1743
+ await this.terminateProcessTree(current, options.graceMs ?? 0);
1731
1744
  }
1732
1745
  await Promise.all(oneShots.map((child) => this.terminateProcessTree(child)));
1733
1746
  })();
@@ -1741,7 +1754,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1741
1754
  }
1742
1755
 
1743
1756
  // ../cli/src/prolog/store-lock.ts
1744
- import { existsSync as existsSync4, rmSync } from "node:fs";
1757
+ import { existsSync as existsSync4, rmSync, writeFileSync } from "node:fs";
1745
1758
  import { readFileSync as readFileSync3 } from "node:fs";
1746
1759
  import { dirname, join } from "node:path";
1747
1760
  var cachedBootId;
@@ -1785,6 +1798,33 @@ function classifyStoreLockHolder(owner) {
1785
1798
  }
1786
1799
  return "alive";
1787
1800
  }
1801
+ function readStoreLockOwner(storePath) {
1802
+ const candidates = [
1803
+ join(storePath, ".kibi-lock-owner.json"),
1804
+ join(storePath, "rdf", ".kibi-lock-owner.json")
1805
+ ];
1806
+ for (const journalPath of candidates) {
1807
+ if (!existsSync4(journalPath))
1808
+ continue;
1809
+ let parsed;
1810
+ try {
1811
+ parsed = JSON.parse(readFileSync3(journalPath, "utf8"));
1812
+ } catch {
1813
+ continue;
1814
+ }
1815
+ if (parsed === null || typeof parsed !== "object")
1816
+ continue;
1817
+ const record = parsed;
1818
+ const owner = {
1819
+ ...typeof record.pid === "number" ? { pid: record.pid } : {},
1820
+ ...typeof record.workspaceRoot === "string" ? { workspaceRoot: record.workspaceRoot } : {},
1821
+ ...typeof record.bootId === "string" ? { bootId: record.bootId } : {},
1822
+ ...typeof record.startedAt === "string" ? { startedAt: record.startedAt } : {}
1823
+ };
1824
+ return { owner, journalPath };
1825
+ }
1826
+ return null;
1827
+ }
1788
1828
  function breakStoreLock(rdfDirectory) {
1789
1829
  const candidates = [
1790
1830
  join(rdfDirectory, "lock"),
@@ -1797,6 +1837,40 @@ function breakStoreLock(rdfDirectory) {
1797
1837
  }
1798
1838
  }
1799
1839
  }
1840
+ var STORE_LOCK_TAKEOVER_FILE = ".kibi-lock-takeover.json";
1841
+ var STORE_LOCK_TAKEOVER_VISIBLE_MS = 24 * 60 * 60 * 1000;
1842
+ function recordStoreLockTakeover(storePath, brokenPid, reason, at = new Date) {
1843
+ try {
1844
+ writeFileSync(join(storePath, STORE_LOCK_TAKEOVER_FILE), `${JSON.stringify({ brokenPid, reason, at: at.toISOString() })}
1845
+ `, { mode: 384 });
1846
+ } catch {}
1847
+ }
1848
+ function readRdfLockPid(lockPath) {
1849
+ try {
1850
+ const match = /pid\((\d+)\)/.exec(readFileSync3(lockPath, "utf8"));
1851
+ const pid = Number.parseInt(match?.[1] ?? "", 10);
1852
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
1853
+ } catch {
1854
+ return null;
1855
+ }
1856
+ }
1857
+ function noteDeadHolderBeforeAttach(kbPath) {
1858
+ const lockPath = join(kbPath, "rdf", "lock");
1859
+ if (!existsSync4(lockPath))
1860
+ return () => {
1861
+ return;
1862
+ };
1863
+ const lockPid = readRdfLockPid(lockPath);
1864
+ const journal = readStoreLockOwner(kbPath)?.owner ?? null;
1865
+ const owner = journal !== null && (lockPid === null || journal.pid === lockPid) ? journal : lockPid !== null ? { pid: lockPid } : null;
1866
+ const decision = decideStoreLockTakeover(owner);
1867
+ if (decision.action !== "break")
1868
+ return () => {
1869
+ return;
1870
+ };
1871
+ const brokenPid = owner?.pid ?? null;
1872
+ return () => recordStoreLockTakeover(kbPath, brokenPid, decision.reason);
1873
+ }
1800
1874
  function decideStoreLockTakeover(owner) {
1801
1875
  const state = classifyStoreLockHolder(owner);
1802
1876
  switch (state) {
@@ -1833,6 +1907,7 @@ Store lock holder: ${holderSummary}. Close that session, or run 'kibi engine sto
1833
1907
  };
1834
1908
  }
1835
1909
  const lockDirectory = storeLocked.lockDirectory !== "" ? storeLocked.lockDirectory : join(kbPath, "rdf");
1910
+ recordStoreLockTakeover(kbPath, storeLocked.owner?.pid ?? null, decision.reason);
1836
1911
  breakStoreLock(lockDirectory);
1837
1912
  const retried = await prolog.query(`kb_attach('${escapeAtom(kbPath)}')`);
1838
1913
  if (retried.success) {
@@ -1856,7 +1931,7 @@ import {
1856
1931
  readFileSync as readFileSync4,
1857
1932
  readdirSync,
1858
1933
  statSync as statSync3,
1859
- writeFileSync
1934
+ writeFileSync as writeFileSync2
1860
1935
  } from "node:fs";
1861
1936
  import path5 from "node:path";
1862
1937
  var BRANCH_STORE_MANIFEST_VERSION = 1;
@@ -1918,7 +1993,7 @@ function ensureBranchStoreManifest(workspaceRoot, branch) {
1918
1993
  if (entries.length > 0) {
1919
1994
  throw new Error(`Branch store at ${storePath} has compiled data but no valid identity manifest; refusing to adopt it.`);
1920
1995
  }
1921
- writeFileSync(manifestPath, `${JSON.stringify(expectedBranchStoreManifest(branch), null, 2)}
1996
+ writeFileSync2(manifestPath, `${JSON.stringify(expectedBranchStoreManifest(branch), null, 2)}
1922
1997
  `, { mode: 384 });
1923
1998
  }
1924
1999
  return storePath;
@@ -2005,6 +2080,8 @@ var READ_LIMITED_METHODS = new Set([
2005
2080
  ]);
2006
2081
  var ENGINE_PROTOCOL_VERSION = 1;
2007
2082
  var ENGINE_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
2083
+ var ENGINE_IDLE_CHECK_INTERVAL_MS = 30 * 1000;
2084
+ var ENGINE_PROLOG_EXIT_GRACE_MS = 1000;
2008
2085
  function enginePackageVersions() {
2009
2086
  return kibiPackageVersions();
2010
2087
  }
@@ -2174,7 +2251,7 @@ function runtimeDirectory() {
2174
2251
  try {
2175
2252
  mkdirSync2(candidate, { recursive: true, mode: 448 });
2176
2253
  const probe = path8.join(candidate, `.kibi-write-${process.pid}`);
2177
- writeFileSync2(probe, "", { mode: 384 });
2254
+ writeFileSync3(probe, "", { mode: 384 });
2178
2255
  unlinkSync(probe);
2179
2256
  return candidate;
2180
2257
  } catch {}
@@ -2214,7 +2291,7 @@ function runtimeDirectory() {
2214
2291
  if (Buffer.byteLength(path8.join(canonical, socketName), "utf8") > maximumBytes)
2215
2292
  continue;
2216
2293
  const probe = path8.join(canonical, `.kibi-write-${randomUUID()}`);
2217
- writeFileSync2(probe, "", { mode: 384, flag: "wx" });
2294
+ writeFileSync3(probe, "", { mode: 384, flag: "wx" });
2218
2295
  unlinkSync(probe);
2219
2296
  return canonical;
2220
2297
  } catch {}
@@ -2278,26 +2355,13 @@ function recoverStaleJournalLock(branchPath) {
2278
2355
  const lockPath = path8.join(branchPath, "rdf", "lock");
2279
2356
  if (!existsSync6(lockPath))
2280
2357
  return;
2281
- let contents = "";
2282
- try {
2283
- contents = readFileSync5(lockPath, "utf8");
2284
- } catch {
2285
- return;
2286
- }
2287
- const match = /pid\((\d+)\)/.exec(contents);
2288
- if (!match)
2289
- return;
2290
- const pid = Number.parseInt(match[1] ?? "", 10);
2291
- if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid)
2358
+ const pid = readRdfLockPid(lockPath);
2359
+ if (pid === null || pid === process.pid || isProcessAlive(pid))
2292
2360
  return;
2361
+ recordStoreLockTakeover(branchPath, pid, decideStoreLockTakeover({ pid }).reason);
2293
2362
  try {
2294
- process.kill(pid, 0);
2295
- return;
2296
- } catch {
2297
- try {
2298
- rmSync2(lockPath, { force: true });
2299
- } catch {}
2300
- }
2363
+ rmSync2(lockPath, { force: true });
2364
+ } catch {}
2301
2365
  }
2302
2366
  function connectSocket(socketPath, timeoutMs) {
2303
2367
  return new Promise((resolve, reject) => {
@@ -2340,7 +2404,7 @@ async function ensureJournaledBranchStoreAsync(branchPath) {
2340
2404
  recoverStaleJournalLock(branchPath);
2341
2405
  archiveLegacyFiles(branchPath);
2342
2406
  if (!existsSync6(sentinelPath)) {
2343
- writeFileSync2(sentinelPath, `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
2407
+ writeFileSync3(sentinelPath, `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
2344
2408
  `, { mode: 384 });
2345
2409
  }
2346
2410
  return;
@@ -2359,7 +2423,7 @@ async function ensureJournaledBranchStoreAsync(branchPath) {
2359
2423
  recoverStaleJournalLock(branchPath);
2360
2424
  archiveLegacyFiles(branchPath);
2361
2425
  if (!existsSync6(sentinelPath)) {
2362
- writeFileSync2(sentinelPath, `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
2426
+ writeFileSync3(sentinelPath, `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
2363
2427
  `, { mode: 384 });
2364
2428
  }
2365
2429
  return;
@@ -2369,13 +2433,13 @@ async function ensureJournaledBranchStoreAsync(branchPath) {
2369
2433
  const legacyAudit = path8.join(branchPath, "audit.log");
2370
2434
  if (!existsSync6(legacyRdf) && !existsSync6(legacyAudit)) {
2371
2435
  mkdirSync2(path8.join(branchPath, "rdf"), { recursive: true });
2372
- writeFileSync2(markerPath, `{"format":"kibi.rdf-journal.v1","schemaVersion":1}
2436
+ writeFileSync3(markerPath, `{"format":"kibi.rdf-journal.v1","schemaVersion":1}
2373
2437
  `, { mode: 384 });
2374
- writeFileSync2(path8.join(branchPath, "CURRENT"), `generation-1:0
2438
+ writeFileSync3(path8.join(branchPath, "CURRENT"), `generation-1:0
2375
2439
  `, {
2376
2440
  mode: 384
2377
2441
  });
2378
- writeFileSync2(path8.join(branchPath, "kb.rdf"), `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
2442
+ writeFileSync3(path8.join(branchPath, "kb.rdf"), `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
2379
2443
  `, { mode: 384 });
2380
2444
  return;
2381
2445
  }
@@ -2424,7 +2488,7 @@ async function ensureJournaledBranchStoreAsync(branchPath) {
2424
2488
  renameSync(source, target);
2425
2489
  }
2426
2490
  archiveLegacyFiles(branchPath);
2427
- writeFileSync2(path8.join(branchPath, "kb.rdf"), `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
2491
+ writeFileSync3(path8.join(branchPath, "kb.rdf"), `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
2428
2492
  `, { mode: 384 });
2429
2493
  fsyncJournaledBranchStore(branchPath);
2430
2494
  rmSync2(staging, { recursive: true, force: true });
@@ -2517,8 +2581,11 @@ class EngineSession {
2517
2581
  }
2518
2582
  async boot() {
2519
2583
  await this.prolog.start();
2584
+ const recordTakeover = noteDeadHolderBeforeAttach(this.branchPath);
2520
2585
  const attached = await this.prolog.query(`kb_attach('${quoteProlog(this.branchPath)}')`);
2521
- if (!attached.success) {
2586
+ if (attached.success)
2587
+ recordTakeover();
2588
+ else {
2522
2589
  const recovered = await retryAttachAfterBreakingStaleLock(this.prolog, this.branchPath, attached);
2523
2590
  if (!recovered.success)
2524
2591
  throw new Error(recovered.error ?? "Failed to attach branch KB");
@@ -2617,11 +2684,12 @@ async function runEngineDaemon(requestedOptions) {
2617
2684
  rmSync2(options.socketPath, { force: true });
2618
2685
  } catch {}
2619
2686
  }
2620
- writeFileSync2(enginePidPath(options.workspaceRoot, options.branch), `${process.pid}
2687
+ writeFileSync3(enginePidPath(options.workspaceRoot, options.branch), `${process.pid}
2621
2688
  `, { mode: 384 });
2622
2689
  const server = net.createServer();
2623
2690
  let activeClients = 0;
2624
- let idleTimer = null;
2691
+ let lastActivityAt = Date.now();
2692
+ let idleWatch = null;
2625
2693
  let queue = Promise.resolve();
2626
2694
  let idleCompactionQueued = false;
2627
2695
  const queryCache = new Map;
@@ -2698,12 +2766,15 @@ async function runEngineDaemon(requestedOptions) {
2698
2766
  idleCompactionQueued = false;
2699
2767
  });
2700
2768
  }
2701
- if (idleTimer)
2702
- clearTimeout(idleTimer);
2703
- idleTimer = setTimeout(() => {
2704
- if (activeClients === 0)
2769
+ lastActivityAt = Date.now();
2770
+ if (idleWatch)
2771
+ return;
2772
+ const idleMs = engineIdleTimeoutMs();
2773
+ idleWatch = setInterval(() => {
2774
+ if (!shuttingDown && activeClients === 0 && Date.now() - lastActivityAt >= idleMs)
2705
2775
  shutdown();
2706
- }, engineIdleTimeoutMs());
2776
+ }, Math.min(idleMs, ENGINE_IDLE_CHECK_INTERVAL_MS));
2777
+ idleWatch.unref();
2707
2778
  };
2708
2779
  const handle = async (request, cancelledRequests) => {
2709
2780
  if (request.protocolVersion !== ENGINE_PROTOCOL_VERSION) {
@@ -2958,20 +3029,22 @@ async function runEngineDaemon(requestedOptions) {
2958
3029
  shuttingDown = true;
2959
3030
  process.off("SIGTERM", requestSignalShutdown);
2960
3031
  process.off("SIGINT", requestSignalShutdown);
2961
- if (idleTimer)
2962
- clearTimeout(idleTimer);
3032
+ if (idleWatch)
3033
+ clearInterval(idleWatch);
2963
3034
  clearInterval(workspaceWatchdog);
2964
3035
  server.close();
2965
3036
  await queue.catch(() => {
2966
3037
  return;
2967
3038
  });
2968
3039
  const saved = session.needsRecycle() ? null : await prolog.query("kb_save").catch(() => null);
3040
+ let detached = false;
2969
3041
  if (saved?.success) {
2970
3042
  try {
2971
3043
  fsyncJournaledBranchStore(branchPath);
2972
3044
  } catch {}
3045
+ detached = await prolog.query("kb_detach").then((result) => result.success).catch(() => false);
2973
3046
  }
2974
- await prolog.terminate().catch(() => {
3047
+ await prolog.terminate({ graceMs: detached ? 0 : ENGINE_PROLOG_EXIT_GRACE_MS }).catch(() => {
2975
3048
  return;
2976
3049
  });
2977
3050
  for (const client of clients)
@@ -3008,6 +3081,7 @@ async function runEngineDaemon(requestedOptions) {
3008
3081
  const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
3009
3082
  buffer = parseFrames(Buffer.concat([buffer, bytes]), (value) => {
3010
3083
  const request = value;
3084
+ lastActivityAt = Date.now();
3011
3085
  if (shuttingDown) {
3012
3086
  writeSocketFrame(socket, {
3013
3087
  id: request.id,
@@ -3114,7 +3188,7 @@ async function runEngineDaemonCli(args = process.argv.slice(2)) {
3114
3188
  const socketPath = socketIndex >= 0 ? args[socketIndex + 1] : undefined;
3115
3189
  if (socketPath) {
3116
3190
  try {
3117
- writeFileSync3(`${socketPath}.error`, `${message}
3191
+ writeFileSync4(`${socketPath}.error`, `${message}
3118
3192
  `, { mode: 384 });
3119
3193
  } catch {}
3120
3194
  }