kibi-runtime 2.0.1 → 2.0.2

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.
@@ -385,7 +385,7 @@ function unquoteAtom(value) {
385
385
 
386
386
  // ../cli/src/prolog.ts
387
387
  var importMetaDir = path.dirname(fileURLToPath(import.meta.url));
388
- var PROLOG_OUTPUT_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
388
+ var DEFAULT_PROLOG_OUTPUT_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
389
389
  var PROLOG_OUTPUT_OVERFLOW_ERROR = "Query exceeded bounded Prolog output capacity (ENOBUFS); narrow the operation or reduce stored entity size";
390
390
  var INTERACTIVE_QUERY_FRAME_END = "__KIBI_QUERY_FRAME_END__";
391
391
  var require2 = createRequire2(import.meta.url);
@@ -426,6 +426,16 @@ function resolveKbPlPath() {
426
426
  }
427
427
  throw new Error("Unable to resolve kb.pl. Expected kibi-core to be installed (node_modules) " + "or to be running inside the monorepo checkout.");
428
428
  }
429
+
430
+ class PrologProcessTerminatedError extends Error {
431
+ code = "prolog_process_terminated";
432
+ reason;
433
+ constructor(reason) {
434
+ super(`Kibi Prolog process is no longer running (${reason}); the interactive session and its attached KB were lost. The owning engine must restart it before serving more queries — run 'kibi engine stop' and retry if this persists.`);
435
+ this.name = "PrologProcessTerminatedError";
436
+ this.reason = reason;
437
+ }
438
+ }
429
439
  function registerProcessExitOnce(current, handler, on = (event, listener) => process.on(event, listener)) {
430
440
  if (current)
431
441
  return current;
@@ -454,19 +464,32 @@ class PrologProcess {
454
464
  useOneShotMode;
455
465
  attachedKbPath = null;
456
466
  onProcessExit = null;
467
+ maxOutputBytes;
468
+ interactiveStarted = false;
469
+ terminationReason = null;
457
470
  constructor(options = {}) {
458
471
  this.swiplPath = options.swiplPath || "swipl";
459
472
  this.timeout = options.timeout || 30000;
473
+ this.maxOutputBytes = options.maxOutputBytes !== undefined && Number.isInteger(options.maxOutputBytes) && options.maxOutputBytes > 0 ? options.maxOutputBytes : DEFAULT_PROLOG_OUTPUT_MAX_BUFFER_BYTES;
460
474
  this.useOneShotMode = options.oneShot ?? false;
461
475
  }
462
476
  attachProcessExitHandler(on = (event, listener) => process.on(event, listener)) {
463
477
  this.onProcessExit = bindProcessExitHandler(this.onProcessExit, () => this.terminate(), on);
464
478
  }
479
+ get oneShotMode() {
480
+ return this.useOneShotMode || !this.interactiveStarted;
481
+ }
482
+ needsRestart() {
483
+ return this.interactiveStarted && !this.isProcessUsable();
484
+ }
465
485
  async start() {
466
486
  if (!existsSync(this.swiplPath) && this.swiplPath !== "swipl") {
467
487
  throw new Error(`SWI-Prolog not found at ${this.swiplPath}. Please install SWI-Prolog or check your PATH.`);
468
488
  }
469
489
  const kbPath = resolveKbPlPath();
490
+ this.interactiveStarted = true;
491
+ this.terminationReason = null;
492
+ this.invalidateCache();
470
493
  this.process = spawn(this.swiplPath, [
471
494
  "-g",
472
495
  `use_module('${kbPath}'), use_module(library(semweb/rdf_db)), set_prolog_flag(answer_write_options, [max_depth(0), quoted(true)])`,
@@ -490,6 +513,12 @@ class PrologProcess {
490
513
  this.process.stderr.on("data", (chunk) => {
491
514
  this.appendErrorChunk(chunk);
492
515
  });
516
+ const child = this.process;
517
+ child.stdin?.on("error", (error) => {
518
+ if (this.process === child) {
519
+ this.terminationReason ??= `stdin ${error.code ?? "error"}`;
520
+ }
521
+ });
493
522
  this.process.stdin.write(`true.
494
523
  `);
495
524
  this.attachProcessExitHandler();
@@ -543,10 +572,10 @@ class PrologProcess {
543
572
  return this.query(batchGoal);
544
573
  }
545
574
  if (!this.isProcessUsable()) {
546
- if (this.process === null) {
575
+ if (this.process === null && !this.interactiveStarted) {
547
576
  return this.execOneShot(goal, this.attachedKbPath);
548
577
  }
549
- throw new Error("Prolog process not started");
578
+ throw this.lostProcessError();
550
579
  }
551
580
  const runInteractiveQuery = async () => {
552
581
  this.clearQueryBuffers();
@@ -576,6 +605,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
576
605
  console.error(`[prolog debug] runtime=${runtime ?? "unknown"} packages=${process.env.KIBI_PACKAGE_VERSIONS ?? KIBI_PACKAGE_VERSIONS}`);
577
606
  }
578
607
  settled = true;
608
+ this.terminationReason = `query timeout: ${goalLabel}`;
579
609
  this.terminate().finally(() => {
580
610
  reject(new Error(msg));
581
611
  });
@@ -587,6 +617,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
587
617
  if (this.outputOverflowed) {
588
618
  clearTimeout(timeoutId);
589
619
  settled = true;
620
+ this.terminationReason = `output overflow (ENOBUFS): ${goalLabel}`;
590
621
  this.terminate().finally(() => {
591
622
  resolve({
592
623
  success: false,
@@ -599,6 +630,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
599
630
  if (!this.isProcessUsable()) {
600
631
  clearTimeout(timeoutId);
601
632
  settled = true;
633
+ this.terminationReason ??= `process exited during query: ${goalLabel}`;
602
634
  resolve({
603
635
  success: false,
604
636
  bindings: {},
@@ -666,7 +698,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
666
698
  await previousQuery;
667
699
  try {
668
700
  if (!this.isProcessUsable()) {
669
- throw new Error("Prolog process not started");
701
+ throw this.lostProcessError();
670
702
  }
671
703
  return await runInteractiveQuery();
672
704
  } finally {
@@ -784,7 +816,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
784
816
  if (outputOverflowed)
785
817
  return;
786
818
  const bytes = target === "stdout" ? outputBytes : errorBytes;
787
- const remaining = PROLOG_OUTPUT_MAX_BUFFER_BYTES - bytes;
819
+ const remaining = this.maxOutputBytes - bytes;
788
820
  if (chunk.byteLength > remaining) {
789
821
  const clipped = chunk.subarray(0, Math.max(0, remaining)).toString();
790
822
  if (target === "stdout")
@@ -897,10 +929,10 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
897
929
  appendOutputChunk(chunk) {
898
930
  if (this.outputOverflowed)
899
931
  return;
900
- const remaining = PROLOG_OUTPUT_MAX_BUFFER_BYTES - this.outputBufferBytes;
932
+ const remaining = this.maxOutputBytes - this.outputBufferBytes;
901
933
  if (chunk.byteLength > remaining) {
902
934
  this.outputBuffer += chunk.subarray(0, remaining).toString();
903
- this.outputBufferBytes = PROLOG_OUTPUT_MAX_BUFFER_BYTES;
935
+ this.outputBufferBytes = this.maxOutputBytes;
904
936
  this.outputOverflowed = true;
905
937
  return;
906
938
  }
@@ -910,10 +942,10 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
910
942
  appendErrorChunk(chunk) {
911
943
  if (this.outputOverflowed)
912
944
  return;
913
- const remaining = PROLOG_OUTPUT_MAX_BUFFER_BYTES - this.errorBufferBytes;
945
+ const remaining = this.maxOutputBytes - this.errorBufferBytes;
914
946
  if (chunk.byteLength > remaining) {
915
947
  this.errorBuffer += chunk.subarray(0, remaining).toString();
916
- this.errorBufferBytes = PROLOG_OUTPUT_MAX_BUFFER_BYTES;
948
+ this.errorBufferBytes = this.maxOutputBytes;
917
949
  this.outputOverflowed = true;
918
950
  return;
919
951
  }
@@ -1059,7 +1091,16 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1059
1091
  getPid() {
1060
1092
  return this.process?.pid || 0;
1061
1093
  }
1094
+ lostProcessError() {
1095
+ if (!this.interactiveStarted) {
1096
+ return new Error("Prolog process not started");
1097
+ }
1098
+ return new PrologProcessTerminatedError(this.terminationReason ?? "process exited");
1099
+ }
1062
1100
  async terminate() {
1101
+ if (this.interactiveStarted) {
1102
+ this.terminationReason ??= "terminated";
1103
+ }
1063
1104
  if (this.terminationPromise) {
1064
1105
  await this.terminationPromise;
1065
1106
  return;
@@ -1448,11 +1489,12 @@ function frame(value) {
1448
1489
  header.writeUInt32BE(payload.byteLength, 0);
1449
1490
  return Buffer.concat([header, payload]);
1450
1491
  }
1492
+ function ignoreWriteFailure() {}
1451
1493
  function writeSocketFrame(socket, value) {
1452
1494
  if (socket.destroyed || !socket.writable)
1453
1495
  return;
1454
1496
  try {
1455
- socket.write(frame(value));
1497
+ socket.write(frame(value), ignoreWriteFailure);
1456
1498
  } catch {}
1457
1499
  }
1458
1500
  function parseFrames(buffer, onFrame) {
@@ -1756,32 +1798,88 @@ function recoverInterruptedGeneration(branchPath) {
1756
1798
  } catch {}
1757
1799
  }
1758
1800
  }
1801
+ function isEngineLifecycleRequest(request) {
1802
+ if (request.method === "stop" || request.method === "cancel")
1803
+ return true;
1804
+ const kind = request.command?.kind;
1805
+ return request.method === "command" && (kind === "stop" || kind === "cancel" || kind === "lifecycle");
1806
+ }
1807
+ var ENGINE_PRELOADED_MODULES = [
1808
+ ["status.pl", "status"],
1809
+ ["discovery.pl", "discovery"]
1810
+ ];
1811
+ var USE_MODULE_GOAL = /^use_module\('(?:[^']|'')*'\)$/u;
1812
+
1813
+ class EngineSession {
1814
+ prolog;
1815
+ branchPath;
1816
+ modules = [];
1817
+ constructor(prolog, branchPath) {
1818
+ this.prolog = prolog;
1819
+ this.branchPath = branchPath;
1820
+ const coreModuleDir = path4.dirname(resolveKbPlPath());
1821
+ for (const [fileName, label] of ENGINE_PRELOADED_MODULES) {
1822
+ const modulePath = path4.join(coreModuleDir, fileName);
1823
+ this.modules.push({
1824
+ goal: `use_module('${quoteProlog(modulePath.replaceAll("\\", "/"))}')`,
1825
+ label
1826
+ });
1827
+ }
1828
+ }
1829
+ async boot() {
1830
+ await this.prolog.start();
1831
+ const attached = await this.prolog.query(`kb_attach('${quoteProlog(this.branchPath)}')`);
1832
+ if (!attached.success) {
1833
+ const recovered = await retryAttachAfterBreakingStaleLock(this.prolog, this.branchPath, attached);
1834
+ if (!recovered.success)
1835
+ throw new Error(recovered.error ?? "Failed to attach branch KB");
1836
+ }
1837
+ for (const { goal, label } of this.modules) {
1838
+ const loaded = await this.prolog.query(goal);
1839
+ if (!loaded.success) {
1840
+ throw new Error(loaded.error ?? `Failed to load Kibi ${label} module`);
1841
+ }
1842
+ }
1843
+ }
1844
+ recordModuleLoad(goal, result) {
1845
+ const normalized = goal.trim().replace(/\.+\s*$/u, "");
1846
+ if (result.success && USE_MODULE_GOAL.test(normalized) && !this.modules.some((module) => module.goal === normalized)) {
1847
+ this.modules.push({ goal: normalized, label: "client-loaded" });
1848
+ }
1849
+ }
1850
+ needsRecycle() {
1851
+ return this.prolog.needsRestart();
1852
+ }
1853
+ async ensureLive() {
1854
+ if (!this.prolog.needsRestart())
1855
+ return false;
1856
+ console.error("[KIBI] engine Prolog session was lost (output overflow, timeout, or crash); restarting and reattaching the branch store");
1857
+ try {
1858
+ await this.boot();
1859
+ } catch (error) {
1860
+ await this.prolog.terminate().catch(() => {
1861
+ return;
1862
+ });
1863
+ const message = error instanceof Error ? error.message : String(error);
1864
+ throw new Error(`Kibi engine failed to recycle its Prolog session: ${message}`);
1865
+ }
1866
+ return true;
1867
+ }
1868
+ }
1759
1869
  async function runEngineDaemon(options) {
1760
1870
  if (!isValidBranchName(options.branch)) {
1761
1871
  throw new Error(`Invalid Kibi engine branch name: ${options.branch}`);
1762
1872
  }
1763
1873
  const branchPath = ensureBranchStoreManifest(options.workspaceRoot, options.branch);
1764
1874
  await ensureJournaledBranchStoreAsync(branchPath);
1765
- const prolog = new PrologProcess({ timeout: 120000, oneShot: false });
1766
- await prolog.start();
1767
- const attached = await prolog.query(`kb_attach('${quoteProlog(branchPath)}')`);
1768
- if (!attached.success) {
1769
- const recovered = await retryAttachAfterBreakingStaleLock(prolog, branchPath, attached);
1770
- if (!recovered.success)
1771
- throw new Error(recovered.error ?? "Failed to attach branch KB");
1772
- }
1773
- const attachedIdentity = readEngineAttachmentIdentity(branchPath);
1774
- const coreModuleDir = path4.dirname(resolveKbPlPath());
1775
- for (const [fileName, label] of [
1776
- ["status.pl", "status"],
1777
- ["discovery.pl", "discovery"]
1778
- ]) {
1779
- const modulePath = path4.join(coreModuleDir, fileName);
1780
- const loaded = await prolog.query(`use_module('${quoteProlog(modulePath.replaceAll("\\", "/"))}')`);
1781
- if (!loaded.success) {
1782
- throw new Error(loaded.error ?? `Failed to load Kibi ${label} module`);
1783
- }
1784
- }
1875
+ const prolog = new PrologProcess({
1876
+ timeout: 120000,
1877
+ oneShot: false,
1878
+ ...options.maxOutputBytes === undefined ? {} : { maxOutputBytes: options.maxOutputBytes }
1879
+ });
1880
+ const session = new EngineSession(prolog, branchPath);
1881
+ await session.boot();
1882
+ let attachedIdentity = readEngineAttachmentIdentity(branchPath);
1785
1883
  mkdirSync2(path4.dirname(options.socketPath), { recursive: true, mode: 448 });
1786
1884
  if (existsSync4(options.socketPath)) {
1787
1885
  const isLivePeerSignal = (error) => {
@@ -1880,7 +1978,7 @@ async function runEngineDaemon(options) {
1880
1978
  if (!idleCompactionQueued) {
1881
1979
  idleCompactionQueued = true;
1882
1980
  queue = queue.then(async () => {
1883
- if (activeClients === 0) {
1981
+ if (activeClients === 0 && !session.needsRecycle()) {
1884
1982
  await prolog.query("kb_storage_compact_if_needed");
1885
1983
  }
1886
1984
  }).catch(() => {
@@ -1907,6 +2005,11 @@ async function runEngineDaemon(options) {
1907
2005
  if (path4.resolve(request.workspaceRoot ?? "") !== path4.resolve(options.workspaceRoot) || request.branch !== options.branch) {
1908
2006
  throw new Error("Kibi engine workspace identity mismatch");
1909
2007
  }
2008
+ if (!isEngineLifecycleRequest(request) && await session.ensureLive()) {
2009
+ queryCache.clear();
2010
+ freshnessCache = null;
2011
+ attachedIdentity = readEngineAttachmentIdentity(branchPath);
2012
+ }
1910
2013
  switch (request.method) {
1911
2014
  case "entities": {
1912
2015
  const limit = request.limit ?? 100;
@@ -2067,6 +2170,7 @@ async function runEngineDaemon(options) {
2067
2170
  return cached;
2068
2171
  }
2069
2172
  const result = await prolog.query(request.goal);
2173
+ session.recordModuleLoad(request.goal, result);
2070
2174
  if (result.success && mutatingEngineGoal(request.goal)) {
2071
2175
  fsyncJournaledBranchStore(branchPath);
2072
2176
  }
@@ -2119,7 +2223,7 @@ async function runEngineDaemon(options) {
2119
2223
  if (idleTimer)
2120
2224
  clearTimeout(idleTimer);
2121
2225
  clearInterval(workspaceWatchdog);
2122
- const saved = await prolog.query("kb_save").catch(() => null);
2226
+ const saved = session.needsRecycle() ? null : await prolog.query("kb_save").catch(() => null);
2123
2227
  if (saved?.success) {
2124
2228
  try {
2125
2229
  fsyncJournaledBranchStore(branchPath);
package/dist/index.d.ts CHANGED
@@ -30,7 +30,8 @@ export { RULE_NAMES, RULES, getCanonicalRules, getDefaultRules, getEffectiveRule
30
30
  export type { RuleDefinition, RuleEnforcementClass, StrictWriteSet, Violation, } from "kibi-cli/public/check-types";
31
31
  export type { ChangedFileImpactResult, QualityDiagnostic, } from "kibi-cli/public/impact-diagnostics";
32
32
  export { branchStorePath, branchStoreManifestPath, branchStoresRoot, branchStoreKey, resolveActiveBranch, resolveBranchAttachment, isValidBranchName, copyCleanSnapshot, ensureBranchStoreManifest, getBranchDiagnostic, } from "kibi-cli/public/branch-resolver";
33
- export { KIBI_PROTOCOL_VERSION, operationData, resultVersion, toKibiResult, } from "kibi-cli/operations/result-envelope";
33
+ export { KIBI_PROTOCOL_VERSION, appendPayloadCountField, normalizeResultPayload, operationData, resultVersion, toKibiResult, } from "kibi-cli/operations/result-envelope";
34
+ export type { NormalizedResultPayload } from "kibi-cli/operations/result-envelope";
34
35
  export * from "kibi-cli/public/branch-resolver";
35
36
  export { EngineClient, engineSocketPath } from "kibi-cli/engine";
36
37
  export { PrologProcess, resolveKbPlPath } from "kibi-cli/prolog";