kibi-runtime 2.0.1 → 2.1.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.
@@ -4,36 +4,161 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // ../cli/src/engine-daemon.ts
6
6
  import { writeFileSync as writeFileSync3 } from "node:fs";
7
- import path5 from "node:path";
7
+ import path8 from "node:path";
8
8
  import { pathToFileURL } from "node:url";
9
9
 
10
10
  // ../cli/src/engine.ts
11
- import { createHash as createHash2 } from "node:crypto";
11
+ import { createHash as createHash2, randomUUID } from "node:crypto";
12
12
  import {
13
13
  chmodSync,
14
- closeSync,
15
- existsSync as existsSync4,
14
+ closeSync as closeSync2,
15
+ existsSync as existsSync6,
16
16
  fsyncSync,
17
+ lstatSync as lstatSync3,
17
18
  mkdirSync as mkdirSync2,
18
- openSync,
19
- readFileSync as readFileSync3,
19
+ openSync as openSync2,
20
+ readFileSync as readFileSync4,
20
21
  readdirSync as readdirSync2,
21
- realpathSync,
22
+ realpathSync as realpathSync3,
22
23
  renameSync,
23
24
  rmSync as rmSync2,
24
- statSync as statSync2,
25
+ statSync as statSync4,
25
26
  unlinkSync,
26
27
  writeFileSync as writeFileSync2
27
28
  } from "node:fs";
28
29
  import * as net from "node:net";
29
30
  import * as os from "node:os";
30
- import * as path4 from "node:path";
31
+ import * as path7 from "node:path";
32
+ import { performance as performance2 } from "node:perf_hooks";
33
+
34
+ // ../cli/src/performance-trace.ts
35
+ import { constants } from "node:fs";
36
+ import {
37
+ closeSync,
38
+ existsSync,
39
+ lstatSync,
40
+ openSync,
41
+ realpathSync,
42
+ statSync,
43
+ writeSync
44
+ } from "node:fs";
45
+ import path from "node:path";
46
+ var KIBI_PERF_TRACE_DIR_ENV = "KIBI_PERF_TRACE_DIR";
47
+ var KIBI_PERF_TIMINGS_ENV = "KIBI_PERF_TIMINGS";
48
+ var KIBI_PERF_TRACE_MAX_EVENTS = 5000;
49
+ var KIBI_PERF_TRACE_MAX_BYTES = 512 * 1024;
50
+ var states = new Map;
51
+ var MAX_DURATION_MS = 60000;
52
+ var TRACE_LIMIT_RESERVE_BYTES = 96;
53
+ function isWithin(parent, candidate) {
54
+ const relative = path.relative(parent, candidate);
55
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
56
+ }
57
+ function privateTraceFile() {
58
+ if (process.env[KIBI_PERF_TIMINGS_ENV] !== "1")
59
+ return null;
60
+ const configuredDirectory = process.env[KIBI_PERF_TRACE_DIR_ENV];
61
+ if (!configuredDirectory || !path.isAbsolute(configuredDirectory))
62
+ return null;
63
+ try {
64
+ const directoryInfo = lstatSync(configuredDirectory);
65
+ if (!directoryInfo.isDirectory() || directoryInfo.isSymbolicLink()) {
66
+ return null;
67
+ }
68
+ const directory = realpathSync(configuredDirectory);
69
+ if (path.resolve(configuredDirectory) !== directory)
70
+ return null;
71
+ const directoryStats = statSync(directory);
72
+ if (typeof process.getuid !== "function" || directoryStats.uid !== process.getuid()) {
73
+ return null;
74
+ }
75
+ if ((directoryStats.mode & 63) !== 0) {
76
+ return null;
77
+ }
78
+ const currentDirectory = realpathSync(process.cwd());
79
+ if (isWithin(currentDirectory, directory))
80
+ return null;
81
+ const file = path.join(directory, `kibi-performance-${process.pid}.jsonl`);
82
+ let state = states.get(file);
83
+ if (!state) {
84
+ if (existsSync(file))
85
+ return null;
86
+ state = { bytes: 0, events: 0, disabled: false };
87
+ states.set(file, state);
88
+ }
89
+ if (state.disabled)
90
+ return null;
91
+ return { file, state };
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+ function writeAll(fd, bytes) {
97
+ let offset = 0;
98
+ while (offset < bytes.length) {
99
+ const written = writeSync(fd, bytes, offset, bytes.length - offset);
100
+ if (written <= 0)
101
+ throw new Error("Performance trace write made no progress");
102
+ offset += written;
103
+ }
104
+ }
105
+ function appendLine(file, state, line, exclusive) {
106
+ const fileInfo = existsSync(file) ? lstatSync(file) : null;
107
+ if (exclusive && fileInfo !== null || !exclusive && (!fileInfo?.isFile() || fileInfo.isSymbolicLink() || fileInfo.size !== state.bytes)) {
108
+ throw new Error("Performance trace file changed unexpectedly");
109
+ }
110
+ const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | (constants.O_NOFOLLOW ?? 0) | (exclusive ? constants.O_EXCL : 0);
111
+ const fd = openSync(file, flags, 384);
112
+ try {
113
+ const bytes = Buffer.from(line, "utf8");
114
+ writeAll(fd, bytes);
115
+ state.bytes += bytes.length;
116
+ } finally {
117
+ closeSync(fd);
118
+ }
119
+ }
120
+ function appendLimitMarker(file, state, limit) {
121
+ const marker = `${JSON.stringify({ kind: "trace-limit", pid: process.pid, limit })}
122
+ `;
123
+ const byteLength = Buffer.byteLength(marker, "utf8");
124
+ if (state.bytes + byteLength <= KIBI_PERF_TRACE_MAX_BYTES) {
125
+ appendLine(file, state, marker, state.events === 0);
126
+ }
127
+ state.disabled = true;
128
+ }
129
+ function writePerformanceTraceEvent(event) {
130
+ if (!Number.isFinite(event.durationMs) || event.durationMs < 0 || event.durationMs > MAX_DURATION_MS) {
131
+ return;
132
+ }
133
+ const trace = privateTraceFile();
134
+ if (!trace)
135
+ return;
136
+ const durationMs = Math.round(event.durationMs * 1000) / 1000;
137
+ const line = `${JSON.stringify({
138
+ kind: event.kind,
139
+ durationMs,
140
+ pid: process.pid
141
+ })}
142
+ `;
143
+ const byteLength = Buffer.byteLength(line, "utf8");
144
+ try {
145
+ if (trace.state.events >= KIBI_PERF_TRACE_MAX_EVENTS || trace.state.bytes + byteLength > KIBI_PERF_TRACE_MAX_BYTES - TRACE_LIMIT_RESERVE_BYTES) {
146
+ appendLimitMarker(trace.file, trace.state, trace.state.events >= KIBI_PERF_TRACE_MAX_EVENTS ? "events" : "bytes");
147
+ return;
148
+ }
149
+ appendLine(trace.file, trace.state, line, trace.state.events === 0);
150
+ trace.state.events += 1;
151
+ } catch {
152
+ trace.state.disabled = true;
153
+ }
154
+ }
31
155
 
32
156
  // ../cli/src/prolog.ts
33
157
  import { spawn } from "node:child_process";
34
- import { existsSync } from "node:fs";
158
+ import { existsSync as existsSync3 } from "node:fs";
35
159
  import { createRequire as createRequire2 } from "node:module";
36
- import path from "node:path";
160
+ import path3 from "node:path";
161
+ import { performance } from "node:perf_hooks";
37
162
  import { fileURLToPath } from "node:url";
38
163
 
39
164
  // ../cli/src/env.ts
@@ -383,9 +508,337 @@ function unquoteAtom(value) {
383
508
  return trimmed;
384
509
  }
385
510
 
511
+ // ../cli/src/prolog/swipl-resolver.ts
512
+ import { execFileSync, spawnSync } from "node:child_process";
513
+ import {
514
+ constants as constants2,
515
+ accessSync,
516
+ existsSync as existsSync2,
517
+ readFileSync,
518
+ statSync as statSync2
519
+ } from "node:fs";
520
+ import path2 from "node:path";
521
+ import {
522
+ BUNDLED_BINARY_PATH,
523
+ MANIFEST_FILE,
524
+ PLATFORM_PACKAGES,
525
+ locatePlatformPackage,
526
+ platformKey,
527
+ sha256File,
528
+ validateBuildManifest
529
+ } from "kibi-swipl";
530
+ var MINIMUM_SWIPL_MAJOR = 9;
531
+
532
+ class SwiplResolutionError extends Error {
533
+ code;
534
+ platform;
535
+ packageName;
536
+ constructor(code, message, platform, packageName) {
537
+ super(message);
538
+ this.name = "SwiplResolutionError";
539
+ this.code = code;
540
+ this.platform = platform;
541
+ this.packageName = packageName;
542
+ }
543
+ }
544
+ function detectGlibc() {
545
+ if (process.platform !== "linux")
546
+ return;
547
+ try {
548
+ const report = process.report?.getReport?.();
549
+ if (report !== undefined) {
550
+ return typeof report.header?.glibcVersionRuntime === "string";
551
+ }
552
+ } catch {}
553
+ return !["/lib/ld-musl-x86_64.so.1", "/lib/ld-musl-aarch64.so.1"].some((loader) => existsSync2(loader));
554
+ }
555
+ function defaultRunVersion(bin) {
556
+ return execFileSync(bin, ["--version"], {
557
+ encoding: "utf8",
558
+ timeout: 1e4,
559
+ stdio: ["ignore", "pipe", "pipe"]
560
+ });
561
+ }
562
+ function defaultDeps() {
563
+ return {
564
+ platform: process.platform,
565
+ arch: process.arch,
566
+ glibc: detectGlibc(),
567
+ locatePackage: (name) => locatePlatformPackage(name),
568
+ runVersion: defaultRunVersion
569
+ };
570
+ }
571
+ function parseSwiplVersion(output) {
572
+ const match = /version\s+(\d+)\.(\d+)(?:\.(\d+))?/i.exec(output);
573
+ if (match === null)
574
+ return;
575
+ const [, major, minor, patch] = match;
576
+ return {
577
+ version: `${major}.${minor}${patch === undefined ? "" : `.${patch}`}`,
578
+ major: Number.parseInt(major ?? "0", 10)
579
+ };
580
+ }
581
+ function installCommand(platform) {
582
+ if (platform === "linux") {
583
+ return "sudo apt-add-repository ppa:swi-prolog/stable && sudo apt-get update && sudo apt-get install swi-prolog";
584
+ }
585
+ if (platform === "darwin")
586
+ return "brew install swi-prolog";
587
+ return "download the installer from https://www.swi-prolog.org/download/stable and add swipl to PATH";
588
+ }
589
+ function hostLabel(deps) {
590
+ const libc = deps.platform === "linux" && deps.glibc !== undefined ? ` (${deps.glibc ? "glibc" : "musl"})` : "";
591
+ return `${deps.platform}-${deps.arch}${libc}`;
592
+ }
593
+ function isExecutableFile(candidate) {
594
+ try {
595
+ if (!statSync2(candidate).isFile())
596
+ return false;
597
+ accessSync(candidate, constants2.X_OK);
598
+ return true;
599
+ } catch {
600
+ return false;
601
+ }
602
+ }
603
+ function inspectBundle(deps) {
604
+ const key = platformKey({
605
+ platform: deps.platform,
606
+ arch: deps.arch,
607
+ glibc: deps.glibc
608
+ });
609
+ if (key === undefined)
610
+ return { kind: "unsupported" };
611
+ const packageName = PLATFORM_PACKAGES[key].package;
612
+ const packageDir = deps.locatePackage(packageName);
613
+ if (packageDir === undefined)
614
+ return { kind: "missing", key, packageName };
615
+ const manifestPath = path2.join(packageDir, MANIFEST_FILE);
616
+ const binary = path2.join(packageDir, BUNDLED_BINARY_PATH);
617
+ if (!existsSync2(manifestPath)) {
618
+ if (!existsSync2(binary))
619
+ return { kind: "empty", key, packageName };
620
+ return {
621
+ kind: "corrupt",
622
+ key,
623
+ packageName,
624
+ reason: `${MANIFEST_FILE} is missing next to ${BUNDLED_BINARY_PATH}`
625
+ };
626
+ }
627
+ const corrupt = (reason) => ({
628
+ kind: "corrupt",
629
+ key,
630
+ packageName,
631
+ reason
632
+ });
633
+ let manifest;
634
+ let packageJson;
635
+ try {
636
+ manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
637
+ } catch (error) {
638
+ return corrupt(`${MANIFEST_FILE} is not valid JSON (${error instanceof Error ? error.message : String(error)})`);
639
+ }
640
+ try {
641
+ packageJson = JSON.parse(readFileSync(path2.join(packageDir, "package.json"), "utf8"));
642
+ } catch (error) {
643
+ return corrupt(`package.json is unreadable (${error instanceof Error ? error.message : String(error)})`);
644
+ }
645
+ const packageVersion = packageJson.kibi?.swiplVersion;
646
+ const checked = validateBuildManifest(manifest, {
647
+ target: key,
648
+ swiplVersion: typeof packageVersion === "string" ? packageVersion : undefined
649
+ });
650
+ if (!checked.ok)
651
+ return corrupt(checked.reason);
652
+ if (!isExecutableFile(binary)) {
653
+ return corrupt(`${BUNDLED_BINARY_PATH} is missing or not executable`);
654
+ }
655
+ let actual;
656
+ try {
657
+ actual = sha256File(binary);
658
+ } catch (error) {
659
+ return corrupt(`${BUNDLED_BINARY_PATH} is unreadable (${error instanceof Error ? error.message : String(error)})`);
660
+ }
661
+ if (actual !== checked.binarySha256) {
662
+ return corrupt(`${BUNDLED_BINARY_PATH} SHA-256 ${actual} does not match the manifest (${checked.binarySha256})`);
663
+ }
664
+ const home = path2.join(packageDir, checked.home);
665
+ let homeIsDirectory = false;
666
+ try {
667
+ homeIsDirectory = statSync2(home).isDirectory();
668
+ } catch {}
669
+ if (!homeIsDirectory)
670
+ return corrupt(`${checked.home} is missing`);
671
+ return {
672
+ kind: "ok",
673
+ key,
674
+ packageName,
675
+ resolved: {
676
+ bin: binary,
677
+ home,
678
+ version: checked.swiplVersion,
679
+ source: "bundled"
680
+ }
681
+ };
682
+ }
683
+ function probeVersion(bin, deps) {
684
+ let output;
685
+ try {
686
+ output = deps.runVersion(bin);
687
+ } catch (error) {
688
+ return {
689
+ kind: "unusable",
690
+ detail: error instanceof Error ? error.message : String(error)
691
+ };
692
+ }
693
+ const parsed = parseSwiplVersion(output);
694
+ if (parsed === undefined) {
695
+ return {
696
+ kind: "unusable",
697
+ detail: `unrecognized --version output ${JSON.stringify(output.trim().slice(0, 80))}`
698
+ };
699
+ }
700
+ if (parsed.major < MINIMUM_SWIPL_MAJOR) {
701
+ return { kind: "too-old", version: parsed.version };
702
+ }
703
+ return { kind: "ok", version: parsed.version };
704
+ }
705
+ function findOnPath(env, deps) {
706
+ for (const directory of (env.PATH ?? "").split(path2.delimiter)) {
707
+ if (directory === "" || !path2.isAbsolute(directory))
708
+ continue;
709
+ const candidate = path2.join(directory, "swipl");
710
+ if (!isExecutableFile(candidate))
711
+ continue;
712
+ const probe = probeVersion(candidate, deps);
713
+ if (probe.kind === "ok") {
714
+ return {
715
+ kind: "ok",
716
+ resolved: { bin: candidate, version: probe.version, source: "path" }
717
+ };
718
+ }
719
+ return probe.kind === "too-old" ? { kind: "too-old", bin: candidate, version: probe.version } : { kind: "unusable", bin: candidate, detail: probe.detail };
720
+ }
721
+ return { kind: "absent" };
722
+ }
723
+ function resolveFromEnv(override, deps) {
724
+ const platform = hostLabel(deps);
725
+ const key = platformKey({
726
+ platform: deps.platform,
727
+ arch: deps.arch,
728
+ glibc: deps.glibc
729
+ });
730
+ const packageName = key === undefined ? undefined : PLATFORM_PACKAGES[key].package;
731
+ const invalid = (detail) => new SwiplResolutionError("swipl_env_invalid", `KIBI_SWIPL=${override} ${detail}. Set KIBI_SWIPL to the absolute path of a SWI-Prolog ${MINIMUM_SWIPL_MAJOR}.0+ executable, to "system" to use swipl from PATH, or unset it to use the bundled runtime.`, platform, packageName);
732
+ if (!path2.isAbsolute(override))
733
+ return fail(invalid("is not an absolute path"));
734
+ if (!isExecutableFile(override)) {
735
+ return fail(invalid("is not an executable file"));
736
+ }
737
+ const probe = probeVersion(override, deps);
738
+ if (probe.kind === "unusable") {
739
+ return fail(invalid(`could not report its version (${probe.detail})`));
740
+ }
741
+ if (probe.kind === "too-old") {
742
+ return fail(invalid(`is SWI-Prolog ${probe.version}, but ${MINIMUM_SWIPL_MAJOR}.0 or newer is required`));
743
+ }
744
+ return { bin: override, version: probe.version, source: "env" };
745
+ }
746
+ function fail(error) {
747
+ throw error;
748
+ }
749
+ function notFoundError(deps, bundle, lookup) {
750
+ const platform = hostLabel(deps);
751
+ const command = installCommand(deps.platform);
752
+ const lines = [
753
+ `Kibi could not find a usable SWI-Prolog (${MINIMUM_SWIPL_MAJOR}.0 or newer is required).`,
754
+ `Detected platform: ${platform}.`
755
+ ];
756
+ let packageName;
757
+ if (bundle === "skipped") {
758
+ lines.push("Bundled SWI-Prolog: skipped because KIBI_SWIPL=system.");
759
+ const key = platformKey({
760
+ platform: deps.platform,
761
+ arch: deps.arch,
762
+ glibc: deps.glibc
763
+ });
764
+ if (key !== undefined)
765
+ packageName = PLATFORM_PACKAGES[key].package;
766
+ } else if (bundle.kind === "unsupported") {
767
+ lines.push(`Bundled SWI-Prolog: no bundled build exists for ${platform} (supported: ${Object.keys(PLATFORM_PACKAGES).join(", ")}).`);
768
+ } else {
769
+ packageName = bundle.packageName;
770
+ if (bundle.kind === "empty") {
771
+ lines.push(`Bundled SWI-Prolog: package ${bundle.packageName} is installed but carries no runtime (a source checkout that was not populated from a release build).`);
772
+ } else {
773
+ lines.push(`Bundled SWI-Prolog: package ${bundle.packageName} would cover this platform but is not installed. It is skipped by installs that omit optional dependencies (npm --omit=optional, --no-optional) and by pnpm supportedArchitectures settings that exclude this platform.`);
774
+ lines.push(` Add it with: npm install --save-dev ${bundle.packageName}`);
775
+ }
776
+ }
777
+ if (bundle === "skipped" && packageName !== undefined) {
778
+ lines.push(` To use it, unset KIBI_SWIPL and install ${packageName}.`);
779
+ }
780
+ switch (lookup.kind) {
781
+ case "absent":
782
+ lines.push("System SWI-Prolog: swipl was not found on PATH.");
783
+ break;
784
+ case "too-old":
785
+ lines.push(`System SWI-Prolog: swipl on PATH (${lookup.bin}) is version ${lookup.version}, older than ${MINIMUM_SWIPL_MAJOR}.0.`);
786
+ break;
787
+ case "unusable":
788
+ lines.push(`System SWI-Prolog: swipl on PATH (${lookup.bin}) could not report its version (${lookup.detail}).`);
789
+ break;
790
+ case "ok":
791
+ break;
792
+ }
793
+ lines.push(` Install it with: ${command}`);
794
+ lines.push(`Or set KIBI_SWIPL to the absolute path of a SWI-Prolog ${MINIMUM_SWIPL_MAJOR}.0+ executable.`);
795
+ return new SwiplResolutionError("swipl_not_found", lines.join(`
796
+ `), platform, packageName);
797
+ }
798
+ function resolveSwiplWith(env, deps) {
799
+ const override = env.KIBI_SWIPL?.trim() ?? "";
800
+ if (override !== "" && override !== "system") {
801
+ return resolveFromEnv(override, deps);
802
+ }
803
+ let bundle = "skipped";
804
+ if (override === "") {
805
+ bundle = inspectBundle(deps);
806
+ if (bundle.kind === "ok")
807
+ return bundle.resolved;
808
+ if (bundle.kind === "corrupt") {
809
+ throw new SwiplResolutionError("swipl_bundle_corrupt", [
810
+ `The bundled SWI-Prolog package ${bundle.packageName} is damaged: ${bundle.reason}.`,
811
+ `Detected platform: ${hostLabel(deps)}.`,
812
+ `Reinstall it (for example: npm install --save-dev ${bundle.packageName}), or set KIBI_SWIPL=system to use swipl from PATH instead.`
813
+ ].join(`
814
+ `), hostLabel(deps), bundle.packageName);
815
+ }
816
+ }
817
+ const lookup = findOnPath(env, deps);
818
+ if (lookup.kind === "ok")
819
+ return lookup.resolved;
820
+ throw notFoundError(deps, bundle, lookup);
821
+ }
822
+ var cache = new Map;
823
+ function resolveSwipl(env = process.env) {
824
+ const key = `${env.KIBI_SWIPL ?? ""}\x00${env.PATH ?? ""}`;
825
+ const cached = cache.get(key);
826
+ if (cached !== undefined)
827
+ return cached;
828
+ const resolved = resolveSwiplWith(env, defaultDeps());
829
+ cache.set(key, resolved);
830
+ return resolved;
831
+ }
832
+ function swiplIdentity(resolved) {
833
+ return `${resolved.bin}@${resolved.version}`;
834
+ }
835
+ function swiplChildEnv(resolved) {
836
+ return resolved.home === undefined ? {} : { SWI_HOME_DIR: resolved.home };
837
+ }
838
+
386
839
  // ../cli/src/prolog.ts
387
- var importMetaDir = path.dirname(fileURLToPath(import.meta.url));
388
- var PROLOG_OUTPUT_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
840
+ var importMetaDir = path3.dirname(fileURLToPath(import.meta.url));
841
+ var DEFAULT_PROLOG_OUTPUT_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
389
842
  var PROLOG_OUTPUT_OVERFLOW_ERROR = "Query exceeded bounded Prolog output capacity (ENOBUFS); narrow the operation or reduce stored entity size";
390
843
  var INTERACTIVE_QUERY_FRAME_END = "__KIBI_QUERY_FRAME_END__";
391
844
  var require2 = createRequire2(import.meta.url);
@@ -398,9 +851,9 @@ function packageVersion(packagePath) {
398
851
  }
399
852
  }
400
853
  var KIBI_PACKAGE_VERSIONS = [
401
- `kibi-cli@${packageVersion(path.join(importMetaDir, "..", "package.json"))}`,
402
- `kibi-core@${packageVersion(path.join(importMetaDir, "..", "..", "core", "package.json"))}`,
403
- `kibi-mcp@${packageVersion(path.join(importMetaDir, "..", "..", "mcp", "package.json"))}`
854
+ `kibi-cli@${packageVersion(path3.join(importMetaDir, "..", "package.json"))}`,
855
+ `kibi-core@${packageVersion(path3.join(importMetaDir, "..", "..", "core", "package.json"))}`,
856
+ `kibi-mcp@${packageVersion(path3.join(importMetaDir, "..", "..", "mcp", "package.json"))}`
404
857
  ].join(",");
405
858
  function resolveKbPlPath() {
406
859
  const overrideKbPath = getKbPlPathOverride();
@@ -412,20 +865,30 @@ function resolveKbPlPath() {
412
865
  return require2.resolve("kibi-core/src/kb.pl");
413
866
  } catch {
414
867
  const coreMain = require2.resolve("kibi-core");
415
- const coreDir = path.dirname(coreMain);
416
- return path.join(coreDir, "src", "kb.pl");
868
+ const coreDir = path3.dirname(coreMain);
869
+ return path3.join(coreDir, "src", "kb.pl");
417
870
  }
418
871
  } catch {}
419
872
  let currentDir = importMetaDir;
420
- while (currentDir !== path.dirname(currentDir)) {
421
- const candidate = path.join(currentDir, "packages", "core", "src", "kb.pl");
422
- if (existsSync(candidate)) {
873
+ while (currentDir !== path3.dirname(currentDir)) {
874
+ const candidate = path3.join(currentDir, "packages", "core", "src", "kb.pl");
875
+ if (existsSync3(candidate)) {
423
876
  return candidate;
424
877
  }
425
- currentDir = path.dirname(currentDir);
878
+ currentDir = path3.dirname(currentDir);
426
879
  }
427
880
  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
881
  }
882
+
883
+ class PrologProcessTerminatedError extends Error {
884
+ code = "prolog_process_terminated";
885
+ reason;
886
+ constructor(reason) {
887
+ 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.`);
888
+ this.name = "PrologProcessTerminatedError";
889
+ this.reason = reason;
890
+ }
891
+ }
429
892
  function registerProcessExitOnce(current, handler, on = (event, listener) => process.on(event, listener)) {
430
893
  if (current)
431
894
  return current;
@@ -440,7 +903,8 @@ function bindProcessExitHandler(current, terminate, on = (event, listener) => pr
440
903
 
441
904
  class PrologProcess {
442
905
  process = null;
443
- swiplPath;
906
+ explicitSwiplPath;
907
+ swiplResolver;
444
908
  timeout;
445
909
  outputBuffer = "";
446
910
  outputBufferBytes = 0;
@@ -454,20 +918,43 @@ class PrologProcess {
454
918
  useOneShotMode;
455
919
  attachedKbPath = null;
456
920
  onProcessExit = null;
921
+ maxOutputBytes;
922
+ interactiveStarted = false;
923
+ terminationReason = null;
457
924
  constructor(options = {}) {
458
- this.swiplPath = options.swiplPath || "swipl";
925
+ this.explicitSwiplPath = options.swiplPath || undefined;
926
+ this.swiplResolver = options.swiplResolver ?? (() => resolveSwipl());
459
927
  this.timeout = options.timeout || 30000;
928
+ this.maxOutputBytes = options.maxOutputBytes !== undefined && Number.isInteger(options.maxOutputBytes) && options.maxOutputBytes > 0 ? options.maxOutputBytes : DEFAULT_PROLOG_OUTPUT_MAX_BUFFER_BYTES;
460
929
  this.useOneShotMode = options.oneShot ?? false;
461
930
  }
462
931
  attachProcessExitHandler(on = (event, listener) => process.on(event, listener)) {
463
932
  this.onProcessExit = bindProcessExitHandler(this.onProcessExit, () => this.terminate(), on);
464
933
  }
934
+ get oneShotMode() {
935
+ return this.useOneShotMode || !this.interactiveStarted;
936
+ }
937
+ needsRestart() {
938
+ return this.interactiveStarted && !this.isProcessUsable();
939
+ }
940
+ launchTarget() {
941
+ const explicit = this.explicitSwiplPath;
942
+ if (explicit !== undefined)
943
+ return { bin: explicit, env: {} };
944
+ const resolved = this.swiplResolver();
945
+ return { bin: resolved.bin, env: swiplChildEnv(resolved) };
946
+ }
465
947
  async start() {
466
- if (!existsSync(this.swiplPath) && this.swiplPath !== "swipl") {
467
- throw new Error(`SWI-Prolog not found at ${this.swiplPath}. Please install SWI-Prolog or check your PATH.`);
948
+ const explicit = this.explicitSwiplPath;
949
+ if (explicit !== undefined && !existsSync3(explicit) && explicit !== "swipl") {
950
+ throw new Error(`SWI-Prolog not found at ${explicit}. Please install SWI-Prolog or check your PATH.`);
468
951
  }
952
+ const launch = this.launchTarget();
469
953
  const kbPath = resolveKbPlPath();
470
- this.process = spawn(this.swiplPath, [
954
+ this.interactiveStarted = true;
955
+ this.terminationReason = null;
956
+ this.invalidateCache();
957
+ this.process = spawn(launch.bin, [
471
958
  "-g",
472
959
  `use_module('${kbPath}'), use_module(library(semweb/rdf_db)), set_prolog_flag(answer_write_options, [max_depth(0), quoted(true)])`,
473
960
  "--quiet"
@@ -475,6 +962,7 @@ class PrologProcess {
475
962
  detached: process.platform !== "win32",
476
963
  env: {
477
964
  ...process.env,
965
+ ...launch.env,
478
966
  KIBI_RUNTIME_NAME: process.versions.bun !== undefined ? "bun" : "node",
479
967
  KIBI_RUNTIME_VERSION: process.versions.bun ?? process.versions.node ?? "unknown",
480
968
  KIBI_PACKAGE_VERSIONS: process.env.KIBI_PACKAGE_VERSIONS ?? KIBI_PACKAGE_VERSIONS
@@ -490,6 +978,12 @@ class PrologProcess {
490
978
  this.process.stderr.on("data", (chunk) => {
491
979
  this.appendErrorChunk(chunk);
492
980
  });
981
+ const child = this.process;
982
+ child.stdin?.on("error", (error) => {
983
+ if (this.process === child) {
984
+ this.terminationReason ??= `stdin ${error.code ?? "error"}`;
985
+ }
986
+ });
493
987
  this.process.stdin.write(`true.
494
988
  `);
495
989
  this.attachProcessExitHandler();
@@ -517,12 +1011,22 @@ class PrologProcess {
517
1011
  this.clearQueryBuffers();
518
1012
  }
519
1013
  async query(goal) {
1014
+ if (!this.useOneShotMode && this.needsRestart()) {
1015
+ throw this.lostProcessError();
1016
+ }
520
1017
  const isSingleGoal = typeof goal === "string";
521
1018
  const goalKey = isSingleGoal ? goal : null;
522
1019
  const cacheable = goalKey !== null && this.isCacheableGoal(goalKey);
523
1020
  if (cacheable) {
1021
+ const cacheLookupStartedAt = process.env.KIBI_PERF_TIMINGS === "1" ? performance.now() : null;
524
1022
  const cachedResult = this.cache.get(goalKey);
525
1023
  if (cachedResult) {
1024
+ if (cacheLookupStartedAt !== null) {
1025
+ writePerformanceTraceEvent({
1026
+ kind: "prolog-process-cache-hit",
1027
+ durationMs: performance.now() - cacheLookupStartedAt
1028
+ });
1029
+ }
526
1030
  return cachedResult;
527
1031
  }
528
1032
  }
@@ -543,10 +1047,10 @@ class PrologProcess {
543
1047
  return this.query(batchGoal);
544
1048
  }
545
1049
  if (!this.isProcessUsable()) {
546
- if (this.process === null) {
1050
+ if (this.process === null && !this.interactiveStarted) {
547
1051
  return this.execOneShot(goal, this.attachedKbPath);
548
1052
  }
549
- throw new Error("Prolog process not started");
1053
+ throw this.lostProcessError();
550
1054
  }
551
1055
  const runInteractiveQuery = async () => {
552
1056
  this.clearQueryBuffers();
@@ -576,6 +1080,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
576
1080
  console.error(`[prolog debug] runtime=${runtime ?? "unknown"} packages=${process.env.KIBI_PACKAGE_VERSIONS ?? KIBI_PACKAGE_VERSIONS}`);
577
1081
  }
578
1082
  settled = true;
1083
+ this.terminationReason = `query timeout: ${goalLabel}`;
579
1084
  this.terminate().finally(() => {
580
1085
  reject(new Error(msg));
581
1086
  });
@@ -587,6 +1092,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
587
1092
  if (this.outputOverflowed) {
588
1093
  clearTimeout(timeoutId);
589
1094
  settled = true;
1095
+ this.terminationReason = `output overflow (ENOBUFS): ${goalLabel}`;
590
1096
  this.terminate().finally(() => {
591
1097
  resolve({
592
1098
  success: false,
@@ -599,6 +1105,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
599
1105
  if (!this.isProcessUsable()) {
600
1106
  clearTimeout(timeoutId);
601
1107
  settled = true;
1108
+ this.terminationReason ??= `process exited during query: ${goalLabel}`;
602
1109
  resolve({
603
1110
  success: false,
604
1111
  bindings: {},
@@ -664,12 +1171,22 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
664
1171
  releaseQuery = resolve;
665
1172
  });
666
1173
  await previousQuery;
1174
+ const tracingEnabled = process.env.KIBI_PERF_TIMINGS === "1";
1175
+ let roundTripStartedAt = null;
667
1176
  try {
668
1177
  if (!this.isProcessUsable()) {
669
- throw new Error("Prolog process not started");
1178
+ throw this.lostProcessError();
670
1179
  }
1180
+ if (tracingEnabled)
1181
+ roundTripStartedAt = performance.now();
671
1182
  return await runInteractiveQuery();
672
1183
  } finally {
1184
+ if (roundTripStartedAt !== null) {
1185
+ writePerformanceTraceEvent({
1186
+ kind: "prolog-round-trip",
1187
+ durationMs: performance.now() - roundTripStartedAt
1188
+ });
1189
+ }
673
1190
  releaseQuery();
674
1191
  }
675
1192
  }
@@ -751,10 +1268,12 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
751
1268
  const packageVersions = process.env.KIBI_PACKAGE_VERSIONS ?? KIBI_PACKAGE_VERSIONS;
752
1269
  let child;
753
1270
  try {
754
- child = spawn(this.swiplPath, ["-q", "-g", prologGoal, "-t", "halt"], {
1271
+ const launch = this.launchTarget();
1272
+ child = spawn(launch.bin, ["-q", "-g", prologGoal, "-t", "halt"], {
755
1273
  detached: process.platform !== "win32",
756
1274
  env: {
757
1275
  ...process.env,
1276
+ ...launch.env,
758
1277
  KIBI_GOAL: combinedGoal,
759
1278
  KIBI_RUNTIME_NAME: runtimeName,
760
1279
  KIBI_RUNTIME_VERSION: runtimeVersion,
@@ -784,7 +1303,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
784
1303
  if (outputOverflowed)
785
1304
  return;
786
1305
  const bytes = target === "stdout" ? outputBytes : errorBytes;
787
- const remaining = PROLOG_OUTPUT_MAX_BUFFER_BYTES - bytes;
1306
+ const remaining = this.maxOutputBytes - bytes;
788
1307
  if (chunk.byteLength > remaining) {
789
1308
  const clipped = chunk.subarray(0, Math.max(0, remaining)).toString();
790
1309
  if (target === "stdout")
@@ -897,10 +1416,10 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
897
1416
  appendOutputChunk(chunk) {
898
1417
  if (this.outputOverflowed)
899
1418
  return;
900
- const remaining = PROLOG_OUTPUT_MAX_BUFFER_BYTES - this.outputBufferBytes;
1419
+ const remaining = this.maxOutputBytes - this.outputBufferBytes;
901
1420
  if (chunk.byteLength > remaining) {
902
1421
  this.outputBuffer += chunk.subarray(0, remaining).toString();
903
- this.outputBufferBytes = PROLOG_OUTPUT_MAX_BUFFER_BYTES;
1422
+ this.outputBufferBytes = this.maxOutputBytes;
904
1423
  this.outputOverflowed = true;
905
1424
  return;
906
1425
  }
@@ -910,10 +1429,10 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
910
1429
  appendErrorChunk(chunk) {
911
1430
  if (this.outputOverflowed)
912
1431
  return;
913
- const remaining = PROLOG_OUTPUT_MAX_BUFFER_BYTES - this.errorBufferBytes;
1432
+ const remaining = this.maxOutputBytes - this.errorBufferBytes;
914
1433
  if (chunk.byteLength > remaining) {
915
1434
  this.errorBuffer += chunk.subarray(0, remaining).toString();
916
- this.errorBufferBytes = PROLOG_OUTPUT_MAX_BUFFER_BYTES;
1435
+ this.errorBufferBytes = this.maxOutputBytes;
917
1436
  this.outputOverflowed = true;
918
1437
  return;
919
1438
  }
@@ -1010,6 +1529,18 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1010
1529
  return { message: this.translateError(errorText) };
1011
1530
  }
1012
1531
  translateError(errorText) {
1532
+ const existence = errorText.match(/^__KIBI_ERROR__:error\(existence_error\((key|procedure),([^,()\r\n]+)(?:,|\),)/m);
1533
+ const identifier = existence?.[2]?.trim().replace(/^'([^']+)'$/, "$1");
1534
+ if (existence?.[1] === "key" && identifier !== undefined) {
1535
+ if (/^[A-Za-z][A-Za-z0-9_-]{0,95}$/.test(identifier)) {
1536
+ return `Predicate or file not found (missing dictionary key: ${identifier})`;
1537
+ }
1538
+ }
1539
+ if (existence?.[1] === "procedure" && identifier !== undefined) {
1540
+ if (/^(?:[A-Za-z][A-Za-z0-9_]{0,95}:)?[A-Za-z][A-Za-z0-9_]{0,95}\/[0-9]{1,3}$/.test(identifier)) {
1541
+ return `Predicate or file not found (missing procedure: ${identifier})`;
1542
+ }
1543
+ }
1013
1544
  const cleanError = errorText.replace(/^__KIBI_(?:STAGE|RUNTIME|ERROR)__:[^\r\n]*\r?\n?/gm, "");
1014
1545
  if (cleanError.includes("stale_snapshot")) {
1015
1546
  return "KB snapshot is stale; reattach or refresh the runtime before retrying (stale_snapshot)";
@@ -1040,6 +1571,8 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1040
1571
  if (cleanError.includes("existence_error") || cleanError.includes("Unknown procedure")) {
1041
1572
  return "Predicate or file not found";
1042
1573
  }
1574
+ if (existence !== null)
1575
+ return "Predicate or file not found";
1043
1576
  if (cleanError.includes("permission_error")) {
1044
1577
  return "Access denied or KB locked";
1045
1578
  }
@@ -1059,7 +1592,16 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1059
1592
  getPid() {
1060
1593
  return this.process?.pid || 0;
1061
1594
  }
1595
+ lostProcessError() {
1596
+ if (!this.interactiveStarted) {
1597
+ return new Error("Prolog process not started");
1598
+ }
1599
+ return new PrologProcessTerminatedError(this.terminationReason ?? "process exited");
1600
+ }
1062
1601
  async terminate() {
1602
+ if (this.interactiveStarted) {
1603
+ this.terminationReason ??= "terminated";
1604
+ }
1063
1605
  if (this.terminationPromise) {
1064
1606
  await this.terminationPromise;
1065
1607
  return;
@@ -1090,8 +1632,8 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1090
1632
  }
1091
1633
 
1092
1634
  // ../cli/src/prolog/store-lock.ts
1093
- import { existsSync as existsSync2, rmSync } from "node:fs";
1094
- import { readFileSync } from "node:fs";
1635
+ import { existsSync as existsSync4, rmSync } from "node:fs";
1636
+ import { readFileSync as readFileSync2 } from "node:fs";
1095
1637
  import { dirname, join } from "node:path";
1096
1638
  var cachedBootId;
1097
1639
  function currentBootId() {
@@ -1102,7 +1644,7 @@ function currentBootId() {
1102
1644
  return cachedBootId;
1103
1645
  }
1104
1646
  try {
1105
- const raw = readFileSync("/proc/sys/kernel/random/boot_id", "utf8");
1647
+ const raw = readFileSync2("/proc/sys/kernel/random/boot_id", "utf8");
1106
1648
  cachedBootId = raw.trim();
1107
1649
  } catch {
1108
1650
  cachedBootId = null;
@@ -1141,7 +1683,7 @@ function breakStoreLock(rdfDirectory) {
1141
1683
  join(dirname(rdfDirectory), ".kibi-lock-owner.json")
1142
1684
  ];
1143
1685
  for (const candidate of candidates) {
1144
- if (existsSync2(candidate)) {
1686
+ if (existsSync4(candidate)) {
1145
1687
  rmSync(candidate, { force: true });
1146
1688
  }
1147
1689
  }
@@ -1195,31 +1737,31 @@ import {
1195
1737
  execFileSync as rawExecFileSync,
1196
1738
  execSync as rawExecSync
1197
1739
  } from "node:child_process";
1198
- import * as path3 from "node:path";
1740
+ import * as path5 from "node:path";
1199
1741
 
1200
1742
  // ../cli/src/utils/branch-store-locator.ts
1201
1743
  import { createHash } from "node:crypto";
1202
1744
  import {
1203
- existsSync as existsSync3,
1745
+ existsSync as existsSync5,
1204
1746
  mkdirSync,
1205
- readFileSync as readFileSync2,
1747
+ readFileSync as readFileSync3,
1206
1748
  readdirSync,
1207
- statSync,
1749
+ statSync as statSync3,
1208
1750
  writeFileSync
1209
1751
  } from "node:fs";
1210
- import path2 from "node:path";
1752
+ import path4 from "node:path";
1211
1753
  var BRANCH_STORE_MANIFEST_VERSION = 1;
1212
1754
  function branchStoreKey(branch) {
1213
1755
  return createHash("sha256").update(branch, "utf8").digest("hex");
1214
1756
  }
1215
1757
  function branchStoresRoot(workspaceRoot) {
1216
- return path2.join(path2.resolve(workspaceRoot), ".kb", "branches");
1758
+ return path4.join(path4.resolve(workspaceRoot), ".kb", "branches");
1217
1759
  }
1218
1760
  function branchStorePath(workspaceRoot, branch) {
1219
- return path2.join(branchStoresRoot(workspaceRoot), branchStoreKey(branch));
1761
+ return path4.join(branchStoresRoot(workspaceRoot), branchStoreKey(branch));
1220
1762
  }
1221
1763
  function branchStoreManifestPath(storePath) {
1222
- return path2.join(storePath, "branch.json");
1764
+ return path4.join(storePath, "branch.json");
1223
1765
  }
1224
1766
  function expectedBranchStoreManifest(branch) {
1225
1767
  return {
@@ -1230,10 +1772,10 @@ function expectedBranchStoreManifest(branch) {
1230
1772
  }
1231
1773
  function readBranchStoreManifest(storePath) {
1232
1774
  const manifestPath = branchStoreManifestPath(storePath);
1233
- if (!existsSync3(manifestPath))
1775
+ if (!existsSync5(manifestPath))
1234
1776
  return null;
1235
1777
  try {
1236
- const parsed = JSON.parse(readFileSync2(manifestPath, "utf8"));
1778
+ const parsed = JSON.parse(readFileSync3(manifestPath, "utf8"));
1237
1779
  if (parsed.version !== BRANCH_STORE_MANIFEST_VERSION || typeof parsed.branch !== "string" || typeof parsed.key !== "string") {
1238
1780
  return null;
1239
1781
  }
@@ -1249,7 +1791,7 @@ function readBranchStoreManifest(storePath) {
1249
1791
  function branchStoreManifestMatches(storePath, branch) {
1250
1792
  const manifest = readBranchStoreManifest(storePath);
1251
1793
  const expected = expectedBranchStoreManifest(branch);
1252
- return manifest !== null && manifest.version === expected.version && manifest.branch === expected.branch && manifest.key === expected.key && path2.basename(storePath) === expected.key;
1794
+ return manifest !== null && manifest.version === expected.version && manifest.branch === expected.branch && manifest.key === expected.key && path4.basename(storePath) === expected.key;
1253
1795
  }
1254
1796
  function ensureBranchStoreManifest(workspaceRoot, branch) {
1255
1797
  const storePath = branchStorePath(workspaceRoot, branch);
@@ -1260,7 +1802,7 @@ function ensureBranchStoreManifest(workspaceRoot, branch) {
1260
1802
  throw new Error(`Branch store identity mismatch at ${storePath}; refusing to attach it to '${branch}'.`);
1261
1803
  }
1262
1804
  if (existing === null) {
1263
- if (existsSync3(manifestPath)) {
1805
+ if (existsSync5(manifestPath)) {
1264
1806
  throw new Error(`Branch store manifest is invalid at ${manifestPath}; refusing to overwrite it.`);
1265
1807
  }
1266
1808
  const entries = readdirSync(storePath);
@@ -1274,7 +1816,7 @@ function ensureBranchStoreManifest(workspaceRoot, branch) {
1274
1816
  }
1275
1817
 
1276
1818
  // ../cli/src/utils/branch-resolver.ts
1277
- var defaultDeps = {
1819
+ var defaultDeps2 = {
1278
1820
  execSync: rawExecSync,
1279
1821
  execFileSync: rawExecFileSync
1280
1822
  };
@@ -1294,7 +1836,7 @@ function isValidBranchName(name) {
1294
1836
  const codePoint = character.codePointAt(0) ?? 0;
1295
1837
  return codePoint <= 32 || codePoint === 127;
1296
1838
  });
1297
- if (name.includes("..") || path3.isAbsolute(name) || name.startsWith("/")) {
1839
+ if (name.includes("..") || path5.isAbsolute(name) || name.startsWith("/")) {
1298
1840
  return false;
1299
1841
  }
1300
1842
  if (name.includes("//") || name.endsWith("/") || name.endsWith(".") || name.includes("\\") || name.startsWith("-") || name.includes("@{") || name.includes("..") || hasControlCharacter || /[~^:?*\[\]]/.test(name)) {
@@ -1305,7 +1847,7 @@ function isValidBranchName(name) {
1305
1847
  return false;
1306
1848
  }
1307
1849
  try {
1308
- defaultDeps.execFileSync("git", ["check-ref-format", "--branch", name], {
1850
+ defaultDeps2.execFileSync("git", ["check-ref-format", "--branch", name], {
1309
1851
  stdio: ["ignore", "ignore", "ignore"],
1310
1852
  timeout: 5000
1311
1853
  });
@@ -1315,10 +1857,43 @@ function isValidBranchName(name) {
1315
1857
  }
1316
1858
  }
1317
1859
 
1860
+ // ../cli/src/utils/canonical-path.ts
1861
+ import { lstatSync as lstatSync2, realpathSync as realpathSync2 } from "node:fs";
1862
+ import path6 from "node:path";
1863
+ function canonicalFilesystemPath(candidate) {
1864
+ let existing = path6.resolve(candidate);
1865
+ const missing = [];
1866
+ for (;; ) {
1867
+ try {
1868
+ return path6.resolve(realpathSync2.native(existing), ...missing);
1869
+ } catch (error) {
1870
+ if (error.code !== "ENOENT")
1871
+ throw error;
1872
+ let entry;
1873
+ try {
1874
+ entry = lstatSync2(existing);
1875
+ } catch (statError) {
1876
+ if (statError.code !== "ENOENT")
1877
+ throw statError;
1878
+ }
1879
+ if (entry !== undefined)
1880
+ throw error;
1881
+ const parent = path6.dirname(existing);
1882
+ if (parent === existing)
1883
+ throw error;
1884
+ missing.unshift(path6.basename(existing));
1885
+ existing = parent;
1886
+ }
1887
+ }
1888
+ }
1889
+
1318
1890
  // ../cli/src/engine.ts
1319
1891
  var ENGINE_PROTOCOL_VERSION = 1;
1320
1892
  var ENGINE_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
1321
1893
  var ENGINE_PACKAGE_VERSIONS = process.env.KIBI_PACKAGE_VERSIONS ?? "unknown";
1894
+ function engineSwiplIdentity() {
1895
+ return swiplIdentity(resolveSwipl());
1896
+ }
1322
1897
  var ENGINE_QUERY_CACHE_MAX_ENTRIES = 128;
1323
1898
  var ENGINE_QUERY_CACHE_MAX_RESULT_BYTES = 8 * 1024 * 1024;
1324
1899
  var ENGINE_FRESHNESS_CACHE_MS = 100;
@@ -1329,18 +1904,18 @@ function requestEngineSignalShutdown(shutdown) {
1329
1904
  };
1330
1905
  }
1331
1906
  function readJournalGeneration(branchPath) {
1332
- const currentPath = path4.join(branchPath, "CURRENT");
1333
- if (!existsSync4(currentPath))
1907
+ const currentPath = path7.join(branchPath, "CURRENT");
1908
+ if (!existsSync6(currentPath))
1334
1909
  return "";
1335
- return readFileSync3(currentPath, "utf8").trim();
1910
+ return readFileSync4(currentPath, "utf8").trim();
1336
1911
  }
1337
1912
  function readEngineAttachmentIdentity(branchPath) {
1338
- if (!existsSync4(branchPath))
1913
+ if (!existsSync6(branchPath))
1339
1914
  return null;
1340
- const stats = statSync2(branchPath);
1341
- let resolved = path4.resolve(branchPath);
1915
+ const stats = statSync4(branchPath);
1916
+ let resolved = path7.resolve(branchPath);
1342
1917
  try {
1343
- resolved = realpathSync(branchPath);
1918
+ resolved = realpathSync3(branchPath);
1344
1919
  } catch {}
1345
1920
  return {
1346
1921
  path: resolved,
@@ -1448,11 +2023,12 @@ function frame(value) {
1448
2023
  header.writeUInt32BE(payload.byteLength, 0);
1449
2024
  return Buffer.concat([header, payload]);
1450
2025
  }
2026
+ function ignoreWriteFailure() {}
1451
2027
  function writeSocketFrame(socket, value) {
1452
2028
  if (socket.destroyed || !socket.writable)
1453
2029
  return;
1454
2030
  try {
1455
- socket.write(frame(value));
2031
+ socket.write(frame(value), ignoreWriteFailure);
1456
2032
  } catch {}
1457
2033
  }
1458
2034
  function parseFrames(buffer, onFrame) {
@@ -1471,42 +2047,85 @@ function parseFrames(buffer, onFrame) {
1471
2047
  return buffer.subarray(cursor);
1472
2048
  }
1473
2049
  function runtimeDirectory() {
1474
- const configured = process.env.KIBI_RUNTIME_DIR ?? process.env.XDG_RUNTIME_DIR ?? path4.join(os.tmpdir(), "kibi-runtime");
1475
- const candidates = [configured, path4.join(os.tmpdir(), "kibi-runtime")];
2050
+ const configured = process.env.KIBI_RUNTIME_DIR ?? process.env.XDG_RUNTIME_DIR ?? path7.join(os.tmpdir(), "kibi-runtime");
2051
+ if (process.platform === "win32") {
2052
+ for (const candidate of [
2053
+ configured,
2054
+ path7.join(os.tmpdir(), "kibi-runtime")
2055
+ ]) {
2056
+ try {
2057
+ mkdirSync2(candidate, { recursive: true, mode: 448 });
2058
+ const probe = path7.join(candidate, `.kibi-write-${process.pid}`);
2059
+ writeFileSync2(probe, "", { mode: 384 });
2060
+ unlinkSync(probe);
2061
+ return candidate;
2062
+ } catch {}
2063
+ }
2064
+ throw new Error("Unable to create a writable Kibi engine runtime directory");
2065
+ }
2066
+ const euid = process.geteuid?.();
2067
+ if (euid === undefined)
2068
+ throw new Error("Kibi engine runtime euid unavailable");
2069
+ let configuredIdentity;
2070
+ try {
2071
+ configuredIdentity = canonicalFilesystemPath(configured);
2072
+ } catch {
2073
+ configuredIdentity = path7.resolve(configured);
2074
+ }
2075
+ const configuredKey = createHash2("sha256").update(configuredIdentity).digest("hex").slice(0, 32);
2076
+ const candidates = [
2077
+ configured,
2078
+ path7.join("/tmp", `kb-${euid}-${configuredKey}`)
2079
+ ];
2080
+ const maximumBytes = process.platform === "linux" ? 107 : 103;
2081
+ const socketName = `kibi-${"0".repeat(32)}.sock`;
1476
2082
  for (const candidate of candidates) {
1477
2083
  try {
2084
+ try {
2085
+ if (lstatSync3(candidate).isSymbolicLink())
2086
+ continue;
2087
+ } catch (error) {
2088
+ if (error.code !== "ENOENT")
2089
+ throw error;
2090
+ }
1478
2091
  mkdirSync2(candidate, { recursive: true, mode: 448 });
1479
- const probe = path4.join(candidate, `.kibi-write-${process.pid}`);
1480
- writeFileSync2(probe, "", { mode: 384 });
2092
+ const directory = lstatSync3(candidate);
2093
+ if (!directory.isDirectory() || directory.uid !== euid || (directory.mode & 63) !== 0)
2094
+ continue;
2095
+ const canonical = canonicalFilesystemPath(candidate);
2096
+ if (Buffer.byteLength(path7.join(canonical, socketName), "utf8") > maximumBytes)
2097
+ continue;
2098
+ const probe = path7.join(canonical, `.kibi-write-${randomUUID()}`);
2099
+ writeFileSync2(probe, "", { mode: 384, flag: "wx" });
1481
2100
  unlinkSync(probe);
1482
- return candidate;
2101
+ return canonical;
1483
2102
  } catch {}
1484
2103
  }
1485
2104
  throw new Error("Unable to create a writable Kibi engine runtime directory");
1486
2105
  }
1487
2106
  function fsyncPath(filePath) {
1488
- if (!existsSync4(filePath))
2107
+ if (!existsSync6(filePath))
1489
2108
  return;
1490
2109
  let descriptor = null;
1491
2110
  try {
1492
- descriptor = openSync(filePath, "r");
2111
+ descriptor = openSync2(filePath, "r");
1493
2112
  fsyncSync(descriptor);
1494
2113
  } finally {
1495
2114
  if (descriptor !== null)
1496
- closeSync(descriptor);
2115
+ closeSync2(descriptor);
1497
2116
  }
1498
2117
  }
1499
2118
  function fsyncJournaledBranchStore(branchPath) {
1500
- const rdfRoot = path4.join(branchPath, "rdf");
2119
+ const rdfRoot = path7.join(branchPath, "rdf");
1501
2120
  const directories = [];
1502
- const pending = existsSync4(rdfRoot) ? [rdfRoot] : [];
2121
+ const pending = existsSync6(rdfRoot) ? [rdfRoot] : [];
1503
2122
  while (pending.length > 0) {
1504
2123
  const directory = pending.pop();
1505
2124
  if (directory === undefined)
1506
2125
  break;
1507
2126
  directories.push(directory);
1508
2127
  for (const entry of readdirSync2(directory, { withFileTypes: true })) {
1509
- const entryPath = path4.join(directory, entry.name);
2128
+ const entryPath = path7.join(directory, entry.name);
1510
2129
  if (entry.isDirectory())
1511
2130
  pending.push(entryPath);
1512
2131
  else if (entry.isFile() && entry.name !== "lock")
@@ -1514,7 +2133,7 @@ function fsyncJournaledBranchStore(branchPath) {
1514
2133
  }
1515
2134
  }
1516
2135
  for (const metadata of ["CURRENT", "storage.json", "kb.rdf"]) {
1517
- fsyncPath(path4.join(branchPath, metadata));
2136
+ fsyncPath(path7.join(branchPath, metadata));
1518
2137
  }
1519
2138
  if (process.platform !== "win32") {
1520
2139
  for (const directory of directories.reverse())
@@ -1523,33 +2142,27 @@ function fsyncJournaledBranchStore(branchPath) {
1523
2142
  }
1524
2143
  }
1525
2144
  function engineSocketPath(workspaceRoot, branch) {
1526
- const canonicalRoot = (() => {
1527
- try {
1528
- return realpathSync(workspaceRoot);
1529
- } catch {
1530
- return path4.resolve(workspaceRoot);
1531
- }
1532
- })();
2145
+ const canonicalRoot = canonicalFilesystemPath(workspaceRoot);
1533
2146
  const key = createHash2("sha256").update(`${canonicalRoot}\x00${branch}`).digest("hex").slice(0, 32);
1534
2147
  if (process.platform === "win32") {
1535
2148
  return `\\\\.\\pipe\\kibi-${key}`;
1536
2149
  }
1537
- return path4.join(runtimeDirectory(), `kibi-${key}.sock`);
2150
+ return path7.join(runtimeDirectory(), `kibi-${key}.sock`);
1538
2151
  }
1539
2152
  function enginePidPath(workspaceRoot, branch) {
1540
2153
  const socket = engineSocketPath(workspaceRoot, branch);
1541
2154
  if (process.platform === "win32") {
1542
- return path4.join(runtimeDirectory(), `kibi-${createHash2("sha256").update(`${path4.resolve(workspaceRoot)}\x00${branch}`).digest("hex")}.pid`);
2155
+ return path7.join(runtimeDirectory(), `kibi-${createHash2("sha256").update(`${path7.resolve(workspaceRoot)}\x00${branch}`).digest("hex")}.pid`);
1543
2156
  }
1544
2157
  return `${socket}.pid`;
1545
2158
  }
1546
2159
  function recoverStaleJournalLock(branchPath) {
1547
- const lockPath = path4.join(branchPath, "rdf", "lock");
1548
- if (!existsSync4(lockPath))
2160
+ const lockPath = path7.join(branchPath, "rdf", "lock");
2161
+ if (!existsSync6(lockPath))
1549
2162
  return;
1550
2163
  let contents = "";
1551
2164
  try {
1552
- contents = readFileSync3(lockPath, "utf8");
2165
+ contents = readFileSync4(lockPath, "utf8");
1553
2166
  } catch {
1554
2167
  return;
1555
2168
  }
@@ -1597,27 +2210,27 @@ function connectSocket(socketPath, timeoutMs) {
1597
2210
  }
1598
2211
  async function ensureJournaledBranchStoreAsync(branchPath) {
1599
2212
  mkdirSync2(branchPath, { recursive: true });
1600
- const markerPath = path4.join(branchPath, "storage.json");
1601
- const marker = existsSync4(markerPath) ? readFileSync3(markerPath, "utf8") : "";
2213
+ const markerPath = path7.join(branchPath, "storage.json");
2214
+ const marker = existsSync6(markerPath) ? readFileSync4(markerPath, "utf8") : "";
1602
2215
  if (marker.includes("kibi.rdf-journal.v1")) {
1603
- const rdfDirectory = path4.join(branchPath, "rdf");
1604
- const currentPath = path4.join(branchPath, "CURRENT");
1605
- const sentinelPath = path4.join(branchPath, "kb.rdf");
1606
- if (!existsSync4(rdfDirectory) || !existsSync4(currentPath)) {
2216
+ const rdfDirectory = path7.join(branchPath, "rdf");
2217
+ const currentPath = path7.join(branchPath, "CURRENT");
2218
+ const sentinelPath = path7.join(branchPath, "kb.rdf");
2219
+ if (!existsSync6(rdfDirectory) || !existsSync6(currentPath)) {
1607
2220
  recoverInterruptedGeneration(branchPath);
1608
- if (existsSync4(rdfDirectory) && existsSync4(currentPath)) {
2221
+ if (existsSync6(rdfDirectory) && existsSync6(currentPath)) {
1609
2222
  recoverStaleJournalLock(branchPath);
1610
2223
  archiveLegacyFiles(branchPath);
1611
- if (!existsSync4(sentinelPath)) {
2224
+ if (!existsSync6(sentinelPath)) {
1612
2225
  writeFileSync2(sentinelPath, `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
1613
2226
  `, { mode: 384 });
1614
2227
  }
1615
2228
  return;
1616
2229
  }
1617
- const legacyRdf = path4.join(branchPath, "kb.rdf");
1618
- const legacyAudit = path4.join(branchPath, "audit.log");
1619
- const legacyContents = existsSync4(legacyRdf) ? readFileSync3(legacyRdf, "utf8") : "";
1620
- if (existsSync4(legacyAudit) || existsSync4(legacyRdf) && !legacyContents.includes("kibi.rdf-journal.v1")) {
2230
+ const legacyRdf = path7.join(branchPath, "kb.rdf");
2231
+ const legacyAudit = path7.join(branchPath, "audit.log");
2232
+ const legacyContents = existsSync6(legacyRdf) ? readFileSync4(legacyRdf, "utf8") : "";
2233
+ if (existsSync6(legacyAudit) || existsSync6(legacyRdf) && !legacyContents.includes("kibi.rdf-journal.v1")) {
1621
2234
  rmSync2(rdfDirectory, { recursive: true, force: true });
1622
2235
  rmSync2(currentPath, { force: true });
1623
2236
  rmSync2(markerPath, { force: true });
@@ -1627,36 +2240,36 @@ async function ensureJournaledBranchStoreAsync(branchPath) {
1627
2240
  } else {
1628
2241
  recoverStaleJournalLock(branchPath);
1629
2242
  archiveLegacyFiles(branchPath);
1630
- if (!existsSync4(sentinelPath)) {
2243
+ if (!existsSync6(sentinelPath)) {
1631
2244
  writeFileSync2(sentinelPath, `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
1632
2245
  `, { mode: 384 });
1633
2246
  }
1634
2247
  return;
1635
2248
  }
1636
2249
  }
1637
- const legacyRdf = path4.join(branchPath, "kb.rdf");
1638
- const legacyAudit = path4.join(branchPath, "audit.log");
1639
- if (!existsSync4(legacyRdf) && !existsSync4(legacyAudit)) {
1640
- mkdirSync2(path4.join(branchPath, "rdf"), { recursive: true });
2250
+ const legacyRdf = path7.join(branchPath, "kb.rdf");
2251
+ const legacyAudit = path7.join(branchPath, "audit.log");
2252
+ if (!existsSync6(legacyRdf) && !existsSync6(legacyAudit)) {
2253
+ mkdirSync2(path7.join(branchPath, "rdf"), { recursive: true });
1641
2254
  writeFileSync2(markerPath, `{"format":"kibi.rdf-journal.v1","schemaVersion":1}
1642
2255
  `, { mode: 384 });
1643
- writeFileSync2(path4.join(branchPath, "CURRENT"), `generation-1:0
2256
+ writeFileSync2(path7.join(branchPath, "CURRENT"), `generation-1:0
1644
2257
  `, {
1645
2258
  mode: 384
1646
2259
  });
1647
- writeFileSync2(path4.join(branchPath, "kb.rdf"), `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
2260
+ writeFileSync2(path7.join(branchPath, "kb.rdf"), `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
1648
2261
  `, { mode: 384 });
1649
2262
  return;
1650
2263
  }
1651
- if (existsSync4(legacyRdf)) {
1652
- const legacyContents = readFileSync3(legacyRdf, "utf8");
2264
+ if (existsSync6(legacyRdf)) {
2265
+ const legacyContents = readFileSync4(legacyRdf, "utf8");
1653
2266
  if (!/<rdf:RDF(?:\s|>)/.test(legacyContents)) {
1654
2267
  throw new Error(`Legacy Kibi store is corrupt at ${legacyRdf}: expected RDF/XML root`);
1655
2268
  }
1656
2269
  }
1657
- const parent = path4.dirname(branchPath);
1658
- const migrationKey = createHash2("sha256").update(path4.resolve(branchPath)).digest("hex").slice(0, 16);
1659
- const staging = path4.join(parent, `.${migrationKey}.migration.${process.pid}.${Date.now()}`);
2270
+ const parent = path7.dirname(branchPath);
2271
+ const migrationKey = createHash2("sha256").update(path7.resolve(branchPath)).digest("hex").slice(0, 16);
2272
+ const staging = path7.join(parent, `.${migrationKey}.migration.${process.pid}.${Date.now()}`);
1660
2273
  mkdirSync2(staging, { recursive: true, mode: 448 });
1661
2274
  const testOneShot = false;
1662
2275
  const migrator = new PrologProcess({
@@ -1684,35 +2297,35 @@ async function ensureJournaledBranchStoreAsync(branchPath) {
1684
2297
  await migrator.terminate();
1685
2298
  }
1686
2299
  for (const file of ["rdf", "storage.json", "CURRENT"]) {
1687
- const source = path4.join(staging, file);
1688
- if (!existsSync4(source))
2300
+ const source = path7.join(staging, file);
2301
+ if (!existsSync6(source))
1689
2302
  continue;
1690
- const target = path4.join(branchPath, file);
1691
- if (existsSync4(target))
2303
+ const target = path7.join(branchPath, file);
2304
+ if (existsSync6(target))
1692
2305
  rmSync2(target, { recursive: true, force: true });
1693
2306
  renameSync(source, target);
1694
2307
  }
1695
2308
  archiveLegacyFiles(branchPath);
1696
- writeFileSync2(path4.join(branchPath, "kb.rdf"), `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
2309
+ writeFileSync2(path7.join(branchPath, "kb.rdf"), `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
1697
2310
  `, { mode: 384 });
1698
2311
  fsyncJournaledBranchStore(branchPath);
1699
2312
  rmSync2(staging, { recursive: true, force: true });
1700
2313
  }
1701
2314
  function archiveLegacyFiles(branchPath) {
1702
- const legacyDir = path4.join(branchPath, "legacy");
2315
+ const legacyDir = path7.join(branchPath, "legacy");
1703
2316
  mkdirSync2(legacyDir, { recursive: true, mode: 448 });
1704
2317
  for (const file of ["kb.rdf", "audit.log"]) {
1705
- const source = path4.join(branchPath, file);
1706
- if (!existsSync4(source))
2318
+ const source = path7.join(branchPath, file);
2319
+ if (!existsSync6(source))
1707
2320
  continue;
1708
2321
  if (file === "kb.rdf") {
1709
- const contents = readFileSync3(source, "utf8");
2322
+ const contents = readFileSync4(source, "utf8");
1710
2323
  if (contents.includes("kibi.rdf-journal.v1"))
1711
2324
  continue;
1712
2325
  }
1713
- const destination = path4.join(legacyDir, file);
1714
- if (existsSync4(destination)) {
1715
- renameSync(source, path4.join(legacyDir, `${file}.recovery.${Date.now()}`));
2326
+ const destination = path7.join(legacyDir, file);
2327
+ if (existsSync6(destination)) {
2328
+ renameSync(source, path7.join(legacyDir, `${file}.recovery.${Date.now()}`));
1716
2329
  } else {
1717
2330
  renameSync(source, destination);
1718
2331
  }
@@ -1728,62 +2341,123 @@ function recoverInterruptedGeneration(branchPath) {
1728
2341
  const newest = (prefix) => {
1729
2342
  const candidates = entries.filter((entry) => entry.startsWith(prefix)).sort().reverse();
1730
2343
  const candidate = candidates[0];
1731
- return candidate === undefined ? null : path4.join(branchPath, candidate);
2344
+ return candidate === undefined ? null : path7.join(branchPath, candidate);
1732
2345
  };
1733
2346
  const oldRdf = newest("rdf.old.");
1734
2347
  const oldCurrent = newest("CURRENT.old.");
1735
- const rdfPath = path4.join(branchPath, "rdf");
1736
- const currentPath = path4.join(branchPath, "CURRENT");
1737
- if ((!existsSync4(rdfPath) || !existsSync4(currentPath)) && oldRdf && oldCurrent) {
2348
+ const rdfPath = path7.join(branchPath, "rdf");
2349
+ const currentPath = path7.join(branchPath, "CURRENT");
2350
+ if ((!existsSync6(rdfPath) || !existsSync6(currentPath)) && oldRdf && oldCurrent) {
1738
2351
  try {
1739
- if (existsSync4(rdfPath))
2352
+ if (existsSync6(rdfPath))
1740
2353
  rmSync2(rdfPath, { recursive: true, force: true });
1741
- if (existsSync4(currentPath))
2354
+ if (existsSync6(currentPath))
1742
2355
  rmSync2(currentPath, { force: true });
1743
2356
  renameSync(oldRdf, rdfPath);
1744
2357
  renameSync(oldCurrent, currentPath);
1745
2358
  return;
1746
2359
  } catch {}
1747
2360
  }
1748
- if (!existsSync4(rdfPath) && oldRdf) {
2361
+ if (!existsSync6(rdfPath) && oldRdf) {
1749
2362
  try {
1750
2363
  renameSync(oldRdf, rdfPath);
1751
2364
  } catch {}
1752
2365
  }
1753
- if (!existsSync4(currentPath) && oldCurrent) {
2366
+ if (!existsSync6(currentPath) && oldCurrent) {
1754
2367
  try {
1755
2368
  renameSync(oldCurrent, currentPath);
1756
2369
  } catch {}
1757
2370
  }
1758
2371
  }
1759
- async function runEngineDaemon(options) {
1760
- if (!isValidBranchName(options.branch)) {
1761
- throw new Error(`Invalid Kibi engine branch name: ${options.branch}`);
2372
+ function isEngineLifecycleRequest(request) {
2373
+ if (request.method === "stop" || request.method === "cancel" || request.method === "handshake")
2374
+ return true;
2375
+ const kind = request.command?.kind;
2376
+ return request.method === "command" && (kind === "stop" || kind === "cancel" || kind === "lifecycle");
2377
+ }
2378
+ var ENGINE_PRELOADED_MODULES = [
2379
+ ["status.pl", "status"],
2380
+ ["discovery.pl", "discovery"]
2381
+ ];
2382
+ var USE_MODULE_GOAL = /^use_module\('(?:[^']|'')*'\)$/u;
2383
+
2384
+ class EngineSession {
2385
+ prolog;
2386
+ branchPath;
2387
+ modules = [];
2388
+ constructor(prolog, branchPath) {
2389
+ this.prolog = prolog;
2390
+ this.branchPath = branchPath;
2391
+ const coreModuleDir = path7.dirname(resolveKbPlPath());
2392
+ for (const [fileName, label] of ENGINE_PRELOADED_MODULES) {
2393
+ const modulePath = path7.join(coreModuleDir, fileName);
2394
+ this.modules.push({
2395
+ goal: `use_module('${quoteProlog(modulePath.replaceAll("\\", "/"))}')`,
2396
+ label
2397
+ });
2398
+ }
2399
+ }
2400
+ async boot() {
2401
+ await this.prolog.start();
2402
+ const attached = await this.prolog.query(`kb_attach('${quoteProlog(this.branchPath)}')`);
2403
+ if (!attached.success) {
2404
+ const recovered = await retryAttachAfterBreakingStaleLock(this.prolog, this.branchPath, attached);
2405
+ if (!recovered.success)
2406
+ throw new Error(recovered.error ?? "Failed to attach branch KB");
2407
+ }
2408
+ for (const { goal, label } of this.modules) {
2409
+ const loaded = await this.prolog.query(goal);
2410
+ if (!loaded.success) {
2411
+ throw new Error(loaded.error ?? `Failed to load Kibi ${label} module`);
2412
+ }
2413
+ }
2414
+ }
2415
+ recordModuleLoad(goal, result) {
2416
+ const normalized = goal.trim().replace(/\.+\s*$/u, "");
2417
+ if (result.success && USE_MODULE_GOAL.test(normalized) && !this.modules.some((module) => module.goal === normalized)) {
2418
+ this.modules.push({ goal: normalized, label: "client-loaded" });
2419
+ }
2420
+ }
2421
+ needsRecycle() {
2422
+ return this.prolog.needsRestart();
2423
+ }
2424
+ async ensureLive() {
2425
+ if (!this.prolog.needsRestart())
2426
+ return false;
2427
+ console.error("[KIBI] engine Prolog session was lost (output overflow, timeout, or crash); restarting and reattaching the branch store");
2428
+ try {
2429
+ await this.boot();
2430
+ } catch (error) {
2431
+ await this.prolog.terminate().catch(() => {
2432
+ return;
2433
+ });
2434
+ const message = error instanceof Error ? error.message : String(error);
2435
+ throw new Error(`Kibi engine failed to recycle its Prolog session: ${message}`);
2436
+ }
2437
+ return true;
2438
+ }
2439
+ }
2440
+ async function runEngineDaemon(requestedOptions) {
2441
+ if (!isValidBranchName(requestedOptions.branch)) {
2442
+ throw new Error(`Invalid Kibi engine branch name: ${requestedOptions.branch}`);
1762
2443
  }
2444
+ const options = {
2445
+ ...requestedOptions,
2446
+ workspaceRoot: canonicalFilesystemPath(requestedOptions.workspaceRoot)
2447
+ };
1763
2448
  const branchPath = ensureBranchStoreManifest(options.workspaceRoot, options.branch);
1764
2449
  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
- }
1785
- mkdirSync2(path4.dirname(options.socketPath), { recursive: true, mode: 448 });
1786
- if (existsSync4(options.socketPath)) {
2450
+ const prolog = new PrologProcess({
2451
+ timeout: 120000,
2452
+ oneShot: false,
2453
+ ...options.maxOutputBytes === undefined ? {} : { maxOutputBytes: options.maxOutputBytes }
2454
+ });
2455
+ const session = new EngineSession(prolog, branchPath);
2456
+ await session.boot();
2457
+ let attachedIdentity = readEngineAttachmentIdentity(branchPath);
2458
+ const serverPrologIdentity = engineSwiplIdentity();
2459
+ mkdirSync2(path7.dirname(options.socketPath), { recursive: true, mode: 448 });
2460
+ if (existsSync6(options.socketPath)) {
1787
2461
  const isLivePeerSignal = (error) => {
1788
2462
  if (error && typeof error === "object") {
1789
2463
  const code = "code" in error ? String(error.code) : "";
@@ -1835,6 +2509,10 @@ async function runEngineDaemon(options) {
1835
2509
  let freshnessCache = null;
1836
2510
  const clients = new Set;
1837
2511
  let shuttingDown = false;
2512
+ let finishShutdown;
2513
+ const shutdownFinished = new Promise((resolve) => {
2514
+ finishShutdown = resolve;
2515
+ });
1838
2516
  const exactBranchStatus = (result) => {
1839
2517
  if (!result.success || typeof result.bindings.JsonString !== "string") {
1840
2518
  return result;
@@ -1863,7 +2541,7 @@ async function runEngineDaemon(options) {
1863
2541
  const workspaceWatchdog = setInterval(() => {
1864
2542
  if (shuttingDown)
1865
2543
  return;
1866
- if (!existsSync4(options.workspaceRoot)) {
2544
+ if (!existsSync6(options.workspaceRoot)) {
1867
2545
  workspaceMissingStreak += 1;
1868
2546
  if (workspaceMissingStreak < 2)
1869
2547
  return;
@@ -1875,12 +2553,12 @@ async function runEngineDaemon(options) {
1875
2553
  }, engineWorkspaceWatchdogMs());
1876
2554
  workspaceWatchdog.unref();
1877
2555
  const scheduleIdleExit = () => {
1878
- if (activeClients > 0)
2556
+ if (shuttingDown || activeClients > 0)
1879
2557
  return;
1880
2558
  if (!idleCompactionQueued) {
1881
2559
  idleCompactionQueued = true;
1882
2560
  queue = queue.then(async () => {
1883
- if (activeClients === 0) {
2561
+ if (activeClients === 0 && !session.needsRecycle()) {
1884
2562
  await prolog.query("kb_storage_compact_if_needed");
1885
2563
  }
1886
2564
  }).catch(() => {
@@ -1904,9 +2582,17 @@ async function runEngineDaemon(options) {
1904
2582
  if (expectedPackages !== undefined && request.packageVersions !== expectedPackages) {
1905
2583
  throw new Error(`Kibi engine package-version mismatch: client=${request.packageVersions ?? "missing"}, server=${expectedPackages}`);
1906
2584
  }
1907
- if (path4.resolve(request.workspaceRoot ?? "") !== path4.resolve(options.workspaceRoot) || request.branch !== options.branch) {
2585
+ if (canonicalFilesystemPath(request.workspaceRoot ?? "") !== options.workspaceRoot || request.branch !== options.branch) {
1908
2586
  throw new Error("Kibi engine workspace identity mismatch");
1909
2587
  }
2588
+ if (request.method !== "handshake" && request.method !== "stop" && request.prologIdentity !== serverPrologIdentity) {
2589
+ throw new Error(`Kibi engine SWI-Prolog mismatch: client=${request.prologIdentity ?? "missing"}, server=${serverPrologIdentity}`);
2590
+ }
2591
+ if (!isEngineLifecycleRequest(request) && await session.ensureLive()) {
2592
+ queryCache.clear();
2593
+ freshnessCache = null;
2594
+ attachedIdentity = readEngineAttachmentIdentity(branchPath);
2595
+ }
1910
2596
  switch (request.method) {
1911
2597
  case "entities": {
1912
2598
  const limit = request.limit ?? 100;
@@ -2042,7 +2728,14 @@ async function runEngineDaemon(options) {
2042
2728
  }
2043
2729
  }
2044
2730
  case "kbStatus": {
2731
+ const freshnessCacheLookupStartedAt = process.env.KIBI_PERF_TIMINGS === "1" ? performance2.now() : null;
2045
2732
  if (freshnessCache !== null && Date.now() - freshnessCache.capturedAt <= ENGINE_FRESHNESS_CACHE_MS) {
2733
+ if (freshnessCacheLookupStartedAt !== null) {
2734
+ writePerformanceTraceEvent({
2735
+ kind: "engine-freshness-cache-hit",
2736
+ durationMs: performance2.now() - freshnessCacheLookupStartedAt
2737
+ });
2738
+ }
2046
2739
  return freshnessCache.result;
2047
2740
  }
2048
2741
  const result = exactBranchStatus(await prolog.query("status:kb_status_json(JsonString)"));
@@ -2062,11 +2755,20 @@ async function runEngineDaemon(options) {
2062
2755
  freshnessCache = null;
2063
2756
  }
2064
2757
  if (cacheableEngineGoal(request.goal)) {
2758
+ const queryCacheLookupStartedAt = process.env.KIBI_PERF_TIMINGS === "1" ? performance2.now() : null;
2065
2759
  const cached = queryCache.get(request.goal);
2066
- if (cached !== undefined)
2760
+ if (cached !== undefined) {
2761
+ if (queryCacheLookupStartedAt !== null) {
2762
+ writePerformanceTraceEvent({
2763
+ kind: "engine-query-cache-hit",
2764
+ durationMs: performance2.now() - queryCacheLookupStartedAt
2765
+ });
2766
+ }
2067
2767
  return cached;
2768
+ }
2068
2769
  }
2069
2770
  const result = await prolog.query(request.goal);
2771
+ session.recordModuleLoad(request.goal, result);
2070
2772
  if (result.success && mutatingEngineGoal(request.goal)) {
2071
2773
  fsyncJournaledBranchStore(branchPath);
2072
2774
  }
@@ -2100,6 +2802,8 @@ async function runEngineDaemon(options) {
2100
2802
  if (typeof request.targetDirectory !== "string")
2101
2803
  throw new Error("export.targetDirectory is required");
2102
2804
  return prolog.query(`kb_storage_export('${quoteProlog(request.targetDirectory)}')`);
2805
+ case "handshake":
2806
+ return { prologIdentity: serverPrologIdentity };
2103
2807
  case "stop":
2104
2808
  setImmediate(() => void shutdown());
2105
2809
  return { stopped: true };
@@ -2119,7 +2823,11 @@ async function runEngineDaemon(options) {
2119
2823
  if (idleTimer)
2120
2824
  clearTimeout(idleTimer);
2121
2825
  clearInterval(workspaceWatchdog);
2122
- const saved = await prolog.query("kb_save").catch(() => null);
2826
+ server.close();
2827
+ await queue.catch(() => {
2828
+ return;
2829
+ });
2830
+ const saved = session.needsRecycle() ? null : await prolog.query("kb_save").catch(() => null);
2123
2831
  if (saved?.success) {
2124
2832
  try {
2125
2833
  fsyncJournaledBranchStore(branchPath);
@@ -2130,22 +2838,29 @@ async function runEngineDaemon(options) {
2130
2838
  });
2131
2839
  for (const client of clients)
2132
2840
  client.destroy();
2133
- server.close();
2134
2841
  server.unref();
2135
2842
  try {
2136
- if (existsSync4(options.socketPath))
2843
+ if (existsSync6(options.socketPath))
2137
2844
  rmSync2(options.socketPath, { force: true });
2138
- if (existsSync4(enginePidPath(options.workspaceRoot, options.branch)))
2845
+ if (existsSync6(enginePidPath(options.workspaceRoot, options.branch)))
2139
2846
  rmSync2(enginePidPath(options.workspaceRoot, options.branch), {
2140
2847
  force: true
2141
2848
  });
2142
2849
  } catch {}
2143
2850
  setTimeout(() => process.exit(0), 50).unref();
2851
+ finishShutdown();
2144
2852
  };
2145
2853
  const requestSignalShutdown = requestEngineSignalShutdown(shutdown);
2146
2854
  process.once("SIGTERM", requestSignalShutdown);
2147
2855
  process.once("SIGINT", requestSignalShutdown);
2148
2856
  server.on("connection", (socket) => {
2857
+ socket.on("error", () => {
2858
+ return;
2859
+ });
2860
+ if (shuttingDown) {
2861
+ socket.destroy();
2862
+ return;
2863
+ }
2149
2864
  clients.add(socket);
2150
2865
  activeClients += 1;
2151
2866
  const cancelledRequests = new Set;
@@ -2155,6 +2870,15 @@ async function runEngineDaemon(options) {
2155
2870
  const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
2156
2871
  buffer = parseFrames(Buffer.concat([buffer, bytes]), (value) => {
2157
2872
  const request = value;
2873
+ if (shuttingDown) {
2874
+ writeSocketFrame(socket, {
2875
+ id: request.id,
2876
+ ok: false,
2877
+ error: "Kibi engine is shutting down; request was not executed",
2878
+ serverPackageVersions: ENGINE_PACKAGE_VERSIONS
2879
+ });
2880
+ return;
2881
+ }
2158
2882
  if (request.method === "cancel" && typeof request.cancelOf === "number") {
2159
2883
  cancelledRequests.add(request.cancelOf);
2160
2884
  }
@@ -2213,6 +2937,7 @@ async function runEngineDaemon(options) {
2213
2937
  await new Promise((resolve) => {
2214
2938
  server.once("close", resolve);
2215
2939
  });
2940
+ await shutdownFinished;
2216
2941
  }
2217
2942
 
2218
2943
  // ../cli/src/engine-daemon.ts
@@ -2224,7 +2949,7 @@ function isEngineDaemonEntrypoint(argv = process.argv, moduleUrl = import.meta.u
2224
2949
  if (typeof entry !== "string" || entry.length === 0)
2225
2950
  return false;
2226
2951
  try {
2227
- return moduleUrl === pathToFileURL(path5.resolve(entry)).href;
2952
+ return moduleUrl === pathToFileURL(path8.resolve(entry)).href;
2228
2953
  } catch {
2229
2954
  return false;
2230
2955
  }