kibi-runtime 2.1.0 → 2.3.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.
@@ -31,6 +31,60 @@ import * as os from "node:os";
31
31
  import * as path7 from "node:path";
32
32
  import { performance as performance2 } from "node:perf_hooks";
33
33
 
34
+ // ../cli/src/engine-limits.ts
35
+ var QUERY_LIMIT_EXCEEDED_CODE = "QUERY_LIMIT_EXCEEDED";
36
+ var ENGINE_READ_TIME_LIMIT_ENV = "KIBI_ENGINE_READ_TIME_LIMIT_MS";
37
+ var ENGINE_READ_INFERENCE_LIMIT_ENV = "KIBI_ENGINE_READ_INFERENCE_LIMIT";
38
+ var ENGINE_READ_TIME_LIMIT_CAP_MS = 115000;
39
+ var ENGINE_READ_INFERENCE_LIMIT_CAP = 1000000000000;
40
+ function boundedInteger(value, max) {
41
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= max ? value : undefined;
42
+ }
43
+ function normalizeQueryLimits(value) {
44
+ if (value === null || typeof value !== "object")
45
+ return;
46
+ const raw = value;
47
+ const requestedTime = boundedInteger(raw.timeMs, Number.MAX_SAFE_INTEGER);
48
+ const timeMs = requestedTime === undefined ? undefined : Math.min(requestedTime, ENGINE_READ_TIME_LIMIT_CAP_MS);
49
+ const inferences = boundedInteger(raw.inferences, ENGINE_READ_INFERENCE_LIMIT_CAP);
50
+ if (timeMs === undefined && inferences === undefined)
51
+ return;
52
+ return {
53
+ ...timeMs !== undefined ? { timeMs } : {},
54
+ ...inferences !== undefined ? { inferences } : {}
55
+ };
56
+ }
57
+ var BOUNDED_READ_HELPER_DEFINITION = `(current_predicate(kibi_engine_bounded_read/4) -> true ; ${[
58
+ "assertz((kibi_engine_bounded_read(Goal, Seconds, Inferences, kibi_read_outcome(Outcome)) :- get_time(T0), statistics(inferences, I0), (catch(kibi_engine_bounded_call(Goal, Seconds, Inferences, Inner), time_limit_exceeded, Inner = time) -> Failed = false ; Failed = true), get_time(T1), statistics(inferences, I1), (Inner == time -> Outcome = time ; Inner == inferences -> Outcome = inferences ; number(Seconds), T1 - T0 >= Seconds -> Outcome = time ; integer(Inferences), I1 - I0 >= Inferences -> Outcome = inferences ; Failed == true -> fail ; Outcome = none)))",
59
+ "assertz((kibi_engine_bounded_call(Goal, Seconds, Inferences, Inner) :- (number(Seconds) -> call_with_time_limit(Seconds, kibi_engine_inference_call(Goal, Inferences, Inner)) ; kibi_engine_inference_call(Goal, Inferences, Inner))))",
60
+ "assertz((kibi_engine_inference_call(Goal, Inferences, Inner) :- (integer(Inferences) -> call_with_inference_limit(Goal, Inferences, Result), (Result == inference_limit_exceeded -> Inner = inferences ; Inner = none) ; once(Goal), Inner = none)))"
61
+ ].join(", ")})`;
62
+ var OUTCOME_BINDING = "KibiReadOutcome";
63
+ function boundedReadGoal(goal, limits) {
64
+ const seconds = limits.timeMs === undefined ? "none" : (limits.timeMs / 1000).toFixed(3);
65
+ const inferences = limits.inferences === undefined ? "none" : String(limits.inferences);
66
+ return `(kibi_engine_bounded_read((${goal}), ${seconds}, ${inferences}, ${OUTCOME_BINDING}))`;
67
+ }
68
+ function settleBoundedRead(result, limits) {
69
+ if (!result.success)
70
+ return result;
71
+ const { [OUTCOME_BINDING]: rawOutcome, ...bindings } = result.bindings;
72
+ const outcome = rawOutcome?.match(/^kibi_read_outcome\((\w+)\)$/)?.[1];
73
+ const hit = outcome === "time" && limits.timeMs !== undefined ? { kind: "time", limit: limits.timeMs } : outcome === "inferences" && limits.inferences !== undefined ? { kind: "inferences", limit: limits.inferences } : null;
74
+ if (hit === null)
75
+ return { ...result, bindings };
76
+ return {
77
+ success: false,
78
+ bindings: {},
79
+ error: queryLimitMessage(hit),
80
+ limitExceeded: hit
81
+ };
82
+ }
83
+ function queryLimitMessage(hit) {
84
+ const budget = hit.kind === "time" ? `time limit of ${hit.limit} ms (${ENGINE_READ_TIME_LIMIT_ENV})` : `inference limit of ${hit.limit} (${ENGINE_READ_INFERENCE_LIMIT_ENV})`;
85
+ return `${QUERY_LIMIT_EXCEEDED_CODE}: read stopped at its ${budget} before computing an answer. Narrow the query or raise the limit.`;
86
+ }
87
+
34
88
  // ../cli/src/performance-trace.ts
35
89
  import { constants } from "node:fs";
36
90
  import {
@@ -921,6 +975,8 @@ class PrologProcess {
921
975
  maxOutputBytes;
922
976
  interactiveStarted = false;
923
977
  terminationReason = null;
978
+ outputWaiter = null;
979
+ outputWakeScheduled = false;
924
980
  constructor(options = {}) {
925
981
  this.explicitSwiplPath = options.swiplPath || undefined;
926
982
  this.swiplResolver = options.swiplResolver ?? (() => resolveSwipl());
@@ -1070,6 +1126,15 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1070
1126
  `);
1071
1127
  return new Promise((resolve, reject) => {
1072
1128
  let settled = false;
1129
+ let pollTimer = null;
1130
+ const settle = () => {
1131
+ settled = true;
1132
+ clearTimeout(timeoutId);
1133
+ if (pollTimer !== null)
1134
+ clearTimeout(pollTimer);
1135
+ if (this.outputWaiter === wake)
1136
+ this.outputWaiter = null;
1137
+ };
1073
1138
  const timeoutId = setTimeout(() => {
1074
1139
  const stage = this.lastDiagnosticStage(this.errorBuffer) ?? "unknown";
1075
1140
  const msg = `Query timeout after ${this.timeout / 1000}s (stage=${stage}, pid=${this.process?.pid ?? 0}, killed=${this.process?.killed ? "yes" : "no"}, exitCode=${this.process?.exitCode ?? "null"}, signal=${this.process?.signalCode ?? "null"}, goal=${goalLabel}). If several fresh Kibi commands hang or crawl the same way, the engine state is likely wedged: run 'kibi engine stop' (then 'kibi sync --rebuild' if needed) and retry.`;
@@ -1079,7 +1144,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1079
1144
  `).find((line) => line.includes("__KIBI_RUNTIME__"));
1080
1145
  console.error(`[prolog debug] runtime=${runtime ?? "unknown"} packages=${process.env.KIBI_PACKAGE_VERSIONS ?? KIBI_PACKAGE_VERSIONS}`);
1081
1146
  }
1082
- settled = true;
1147
+ settle();
1083
1148
  this.terminationReason = `query timeout: ${goalLabel}`;
1084
1149
  this.terminate().finally(() => {
1085
1150
  reject(new Error(msg));
@@ -1090,8 +1155,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1090
1155
  return;
1091
1156
  }
1092
1157
  if (this.outputOverflowed) {
1093
- clearTimeout(timeoutId);
1094
- settled = true;
1158
+ settle();
1095
1159
  this.terminationReason = `output overflow (ENOBUFS): ${goalLabel}`;
1096
1160
  this.terminate().finally(() => {
1097
1161
  resolve({
@@ -1103,8 +1167,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1103
1167
  return;
1104
1168
  }
1105
1169
  if (!this.isProcessUsable()) {
1106
- clearTimeout(timeoutId);
1107
- settled = true;
1170
+ settle();
1108
1171
  this.terminationReason ??= `process exited during query: ${goalLabel}`;
1109
1172
  resolve({
1110
1173
  success: false,
@@ -1114,8 +1177,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1114
1177
  return;
1115
1178
  }
1116
1179
  if (this.errorBuffer.length > 0 && this.errorBuffer.includes("ERROR")) {
1117
- clearTimeout(timeoutId);
1118
- settled = true;
1180
+ settle();
1119
1181
  if (debug) {
1120
1182
  console.error(`[prolog debug] query error: ${goalLabel} error=${this.errorBuffer.split(`
1121
1183
  `)[0]}`);
@@ -1131,8 +1193,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1131
1193
  }
1132
1194
  const frameEnd = this.outputBuffer.indexOf(INTERACTIVE_QUERY_FRAME_END);
1133
1195
  if (frameEnd >= 0) {
1134
- clearTimeout(timeoutId);
1135
- settled = true;
1196
+ settle();
1136
1197
  const framedOutput = this.outputBuffer.slice(0, frameEnd).trimEnd();
1137
1198
  if (/(?:^|\n)(?:false|fail)\.\s*$/.test(framedOutput)) {
1138
1199
  resolve({
@@ -1160,8 +1221,23 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1160
1221
  resolve(result);
1161
1222
  return;
1162
1223
  }
1163
- setTimeout(checkResult, 50);
1224
+ if (pollTimer === null) {
1225
+ pollTimer = setTimeout(() => {
1226
+ pollTimer = null;
1227
+ checkResult();
1228
+ }, 50);
1229
+ }
1230
+ };
1231
+ let frameScanFrom = 0;
1232
+ const wake = () => {
1233
+ if (settled)
1234
+ return;
1235
+ const found = this.outputBuffer.indexOf(INTERACTIVE_QUERY_FRAME_END, frameScanFrom) >= 0;
1236
+ frameScanFrom = Math.max(0, this.outputBuffer.length - INTERACTIVE_QUERY_FRAME_END.length);
1237
+ if (found || this.outputOverflowed)
1238
+ checkResult();
1164
1239
  };
1240
+ this.outputWaiter = wake;
1165
1241
  checkResult();
1166
1242
  });
1167
1243
  };
@@ -1425,6 +1501,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1425
1501
  }
1426
1502
  this.outputBuffer += chunk.toString();
1427
1503
  this.outputBufferBytes += chunk.byteLength;
1504
+ this.wakeOutputWaiter();
1428
1505
  }
1429
1506
  appendErrorChunk(chunk) {
1430
1507
  if (this.outputOverflowed)
@@ -1438,6 +1515,16 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
1438
1515
  }
1439
1516
  this.errorBuffer += chunk.toString();
1440
1517
  this.errorBufferBytes += chunk.byteLength;
1518
+ this.wakeOutputWaiter();
1519
+ }
1520
+ wakeOutputWaiter() {
1521
+ if (this.outputWaiter === null || this.outputWakeScheduled)
1522
+ return;
1523
+ this.outputWakeScheduled = true;
1524
+ setImmediate(() => {
1525
+ this.outputWakeScheduled = false;
1526
+ this.outputWaiter?.();
1527
+ });
1441
1528
  }
1442
1529
  goalLabel(goal) {
1443
1530
  const match = goal.trim().match(/^([a-z][A-Za-z0-9_]*(?::[a-z][A-Za-z0-9_]*)?)/);
@@ -1888,6 +1975,12 @@ function canonicalFilesystemPath(candidate) {
1888
1975
  }
1889
1976
 
1890
1977
  // ../cli/src/engine.ts
1978
+ var READ_LIMITED_METHODS = new Set([
1979
+ "query",
1980
+ "command",
1981
+ "entities",
1982
+ "search"
1983
+ ]);
1891
1984
  var ENGINE_PROTOCOL_VERSION = 1;
1892
1985
  var ENGINE_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
1893
1986
  var ENGINE_PACKAGE_VERSIONS = process.env.KIBI_PACKAGE_VERSIONS ?? "unknown";
@@ -2506,6 +2599,18 @@ async function runEngineDaemon(requestedOptions) {
2506
2599
  let queue = Promise.resolve();
2507
2600
  let idleCompactionQueued = false;
2508
2601
  const queryCache = new Map;
2602
+ let boundedReadHelperPid = null;
2603
+ const runBoundedRead = async (goal, limits) => {
2604
+ const pid = prolog.getPid();
2605
+ if (boundedReadHelperPid !== pid) {
2606
+ const defined = await prolog.query(BOUNDED_READ_HELPER_DEFINITION);
2607
+ if (!defined.success) {
2608
+ throw new Error(defined.error ?? "Failed to define the bounded-read helper");
2609
+ }
2610
+ boundedReadHelperPid = pid;
2611
+ }
2612
+ return settleBoundedRead(await prolog.query(boundedReadGoal(goal, limits)), limits);
2613
+ };
2509
2614
  let freshnessCache = null;
2510
2615
  const clients = new Set;
2511
2616
  let shuttingDown = false;
@@ -2593,6 +2698,8 @@ async function runEngineDaemon(requestedOptions) {
2593
2698
  freshnessCache = null;
2594
2699
  attachedIdentity = readEngineAttachmentIdentity(branchPath);
2595
2700
  }
2701
+ const readLimits = normalizeQueryLimits(request.limits);
2702
+ const runRead = (goal) => readLimits === undefined ? prolog.query(goal) : runBoundedRead(goal, readLimits);
2596
2703
  switch (request.method) {
2597
2704
  case "entities": {
2598
2705
  const limit = request.limit ?? 100;
@@ -2604,8 +2711,8 @@ async function runEngineDaemon(requestedOptions) {
2604
2711
  const id = request.entityId === undefined ? "none" : `'${quoteProlog(request.entityId)}'`;
2605
2712
  const tags = `[${(request.tags ?? []).map((tag) => `'${quoteProlog(tag)}'`).join(",")}]`;
2606
2713
  const source = request.sourceFile === undefined ? "none" : `'${quoteProlog(request.sourceFile)}'`;
2607
- const result = await prolog.query(`kb_query_entities(${type}, ${id}, ${tags}, ${source}, ${limit}, ${offset}, Rows, Count)`);
2608
- if (!result.success) {
2714
+ const result = await runRead(`kb_query_entities(${type}, ${id}, ${tags}, ${source}, ${limit}, ${offset}, Rows, Count)`);
2715
+ if (!result.success && result.limitExceeded === undefined) {
2609
2716
  throw new Error(result.error ?? "Indexed entity query failed");
2610
2717
  }
2611
2718
  return result;
@@ -2620,8 +2727,8 @@ async function runEngineDaemon(requestedOptions) {
2620
2727
  throw new Error("search.limit/offset must be bounded integers");
2621
2728
  }
2622
2729
  const type = request.type === undefined ? "none" : `'${quoteProlog(request.type)}'`;
2623
- const result = await prolog.query(`kb_search_entities(${type}, '${quoteProlog(request.searchQuery)}', ${limit}, ${offset}, Rows, Count)`);
2624
- if (!result.success) {
2730
+ const result = await runRead(`kb_search_entities(${type}, '${quoteProlog(request.searchQuery)}', ${limit}, ${offset}, Rows, Count)`);
2731
+ if (!result.success && result.limitExceeded === undefined) {
2625
2732
  throw new Error(result.error ?? "Indexed search candidate query failed");
2626
2733
  }
2627
2734
  return result;
@@ -2658,7 +2765,7 @@ async function runEngineDaemon(requestedOptions) {
2658
2765
  if (command.rule !== undefined && !/^[a-z][a-z0-9_]*$/.test(command.rule)) {
2659
2766
  throw new Error("engine.check.rule must be a lowercase rule name");
2660
2767
  }
2661
- return prolog.query("checks:check_all_json(JsonString)");
2768
+ return runRead("checks:check_all_json(JsonString)");
2662
2769
  }
2663
2770
  case "relationship": {
2664
2771
  if (!command.type || !command.from || !command.to) {
@@ -2712,7 +2819,7 @@ async function runEngineDaemon(requestedOptions) {
2712
2819
  const id = command.id === undefined ? "none" : `'${quoteProlog(command.id)}'`;
2713
2820
  const tags = `[${(command.tags ?? []).map((tag) => `'${quoteProlog(tag)}'`).join(",")}]`;
2714
2821
  const source = command.sourceFile === undefined ? "none" : `'${quoteProlog(command.sourceFile)}'`;
2715
- return prolog.query(`kb_query_entities(${type}, ${id}, ${tags}, ${source}, ${command.limit}, ${command.offset}, Rows, Count)`);
2822
+ return runRead(`kb_query_entities(${type}, ${id}, ${tags}, ${source}, ${command.limit}, ${command.offset}, Rows, Count)`);
2716
2823
  }
2717
2824
  case "search": {
2718
2825
  if (!command.query.trim())
@@ -2721,7 +2828,7 @@ async function runEngineDaemon(requestedOptions) {
2721
2828
  throw new Error("engine.search.limit/offset must be bounded integers");
2722
2829
  }
2723
2830
  const type = command.type === undefined ? "none" : `'${quoteProlog(command.type)}'`;
2724
- return prolog.query(`kb_search_entities(${type}, '${quoteProlog(command.query)}', ${command.limit}, ${command.offset}, Rows, Count)`);
2831
+ return runRead(`kb_search_entities(${type}, '${quoteProlog(command.query)}', ${command.limit}, ${command.offset}, Rows, Count)`);
2725
2832
  }
2726
2833
  default:
2727
2834
  throw new Error("engine.command kind is unsupported");
@@ -2767,7 +2874,8 @@ async function runEngineDaemon(requestedOptions) {
2767
2874
  return cached;
2768
2875
  }
2769
2876
  }
2770
- const result = await prolog.query(request.goal);
2877
+ const boundable = !mutatingEngineGoal(request.goal) && !/\b(?:use_module|consult|load_files|ensure_loaded)\s*\(/.test(request.goal);
2878
+ const result = boundable ? await runRead(request.goal) : await prolog.query(request.goal);
2771
2879
  session.recordModuleLoad(request.goal, result);
2772
2880
  if (result.success && mutatingEngineGoal(request.goal)) {
2773
2881
  fsyncJournaledBranchStore(branchPath);
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export type { Clock, OperationContext, OperationPlugins, OperationRuntime, Runti
8
8
  export type { EngineCommandV1, EnginePort } from "./engine.js";
9
9
  export { executeOperation } from "kibi-cli/operations/runtime-types";
10
10
  export type { KibiResult, KibiResultStatus, OperationContent, OperationEffect, OperationName, OperationResult, OperationSpec, } from "kibi-cli/operations";
11
- export { VALID_ENTITY_TYPES, OPERATION_CATALOG, applyPlanSpec, planBootstrapSpec, checkSpec, prepareImpactReviewSpec, executePrepareImpactReview, compileIntentSpec, coverageSpec, deleteSpec, dedupeEntities, executeApplyPlan, executePlanBootstrap, executeCompileIntent, executeCoverage, executeIngestProof, executeFindGaps, executeGraph, executeIntentSearch, executeQuery, executeSearch, executeSemanticAdvisor, executeStatus, buildEntityGoal, findGapsSpec, getSpec, graphSpec, ingestProofSpec, loadEntities, listSpecs, modelRequirementSpec, nodeFilesystem, nodeGit, nodeNetwork, paginateResults, presentBootstrap, querySpec, searchSpec, selectBootstrapCandidates, semanticAdvisorSpec, sparqlRemoteSpec, statusSpec, suggestPredicatesSpec, upsertSpec, validateEntityType, validateUpsertSpec, withContractDefaults, } from "kibi-cli/operations";
11
+ export { VALID_ENTITY_TYPES, OPERATION_CATALOG, MODEL_ROUTES, SKILL_ROUTES, dispatchComposite, modelSpec, skillsSpec, applyPlanSpec, planBootstrapSpec, checkSpec, prepareImpactReviewSpec, executePrepareImpactReview, compileIntentSpec, coverageSpec, deleteSpec, dedupeEntities, executeApplyPlan, executePlanBootstrap, executeCompileIntent, executeCoverage, executeIngestProof, executeFindGaps, executeGraph, executeIntentSearch, executeQuery, executeSearch, executeSemanticAdvisor, executeStatus, buildEntityGoal, findGapsSpec, getSpec, graphSpec, ingestProofSpec, loadEntities, listSpecs, modelRequirementSpec, nodeFilesystem, nodeGit, nodeNetwork, paginateResults, presentBootstrap, querySpec, searchSpec, selectBootstrapCandidates, semanticAdvisorSpec, sparqlRemoteSpec, statusSpec, suggestPredicatesSpec, upsertSpec, validateEntityType, validateUpsertSpec, withContractDefaults, } from "kibi-cli/operations";
12
12
  export type { ImpactReviewPreparationScope, PrepareImpactReviewInput, } from "kibi-cli/operations";
13
13
  export type { BootstrapAction, BootstrapPlanV1 } from "kibi-cli/operations";
14
14
  export { skillsListSpec, skillsLoadSpec, skillsReadSpec, } from "./skill-operations.js";
@@ -34,7 +34,8 @@ export { branchStorePath, branchStoreManifestPath, branchStoresRoot, branchStore
34
34
  export { KIBI_PROTOCOL_VERSION, appendPayloadCountField, normalizeResultPayload, operationData, resultVersion, toKibiResult, } from "kibi-cli/operations/result-envelope";
35
35
  export type { NormalizedResultPayload } from "kibi-cli/operations/result-envelope";
36
36
  export * from "kibi-cli/public/branch-resolver";
37
- export { EngineClient, engineSocketPath } from "kibi-cli/engine";
37
+ export { EngineClient, EngineQueryLimitError, QUERY_LIMIT_EXCEEDED_CODE, engineSocketPath, queryLimitExceededOf, } from "kibi-cli/engine";
38
+ export type { EngineLimitExceeded, EngineQueryLimits } from "kibi-cli/engine";
38
39
  export { PrologProcess, resolveKbPlPath } from "kibi-cli/prolog";
39
40
  export { escapeAtomContent, splitTopLevel, } from "kibi-cli/prolog/codec";
40
41
  export { createCliRuntime } from "kibi-cli/runtime/cli-runtime";