kibi-runtime 1.0.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,26 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
2
4
 
3
5
  // ../cli/src/engine-daemon.ts
4
6
  import { writeFileSync as writeFileSync3 } from "node:fs";
7
+ import path5 from "node:path";
8
+ import { pathToFileURL } from "node:url";
5
9
 
6
10
  // ../cli/src/engine.ts
7
- import { spawn as spawn2 } from "node:child_process";
8
11
  import { createHash as createHash2 } from "node:crypto";
9
12
  import {
10
13
  chmodSync,
11
14
  closeSync,
12
- existsSync as existsSync3,
15
+ existsSync as existsSync4,
13
16
  fsyncSync,
14
17
  mkdirSync as mkdirSync2,
15
18
  openSync,
16
- readFileSync as readFileSync2,
19
+ readFileSync as readFileSync3,
17
20
  readdirSync as readdirSync2,
18
21
  realpathSync,
19
22
  renameSync,
20
- rmSync,
23
+ rmSync as rmSync2,
21
24
  statSync as statSync2,
22
25
  unlinkSync,
23
26
  writeFileSync as writeFileSync2
@@ -25,12 +28,11 @@ import {
25
28
  import * as net from "node:net";
26
29
  import * as os from "node:os";
27
30
  import * as path4 from "node:path";
28
- import { fileURLToPath as fileURLToPath2 } from "node:url";
29
31
 
30
32
  // ../cli/src/prolog.ts
31
33
  import { spawn } from "node:child_process";
32
34
  import { existsSync } from "node:fs";
33
- import { createRequire } from "node:module";
35
+ import { createRequire as createRequire2 } from "node:module";
34
36
  import path from "node:path";
35
37
  import { fileURLToPath } from "node:url";
36
38
 
@@ -46,12 +48,347 @@ function isPrologDebugEnabled() {
46
48
  return Boolean(getEnvValue("KIBI_PROLOG_DEBUG"));
47
49
  }
48
50
 
51
+ // ../cli/src/prolog/codec.ts
52
+ function escapeAtom(value) {
53
+ return value.replaceAll("\\", "\\\\").replaceAll("\x07", "\\a").replaceAll("\b", "\\b").replaceAll("\f", "\\f").replaceAll(`
54
+ `, "\\n").replaceAll("\r", "\\r").replaceAll("\t", "\\t").replaceAll("\v", "\\v").replaceAll("'", "''");
55
+ }
56
+ function splitTopLevelGeneral(str, delimiter) {
57
+ return scanTopLevelGeneral(str, delimiter).parts;
58
+ }
59
+ function scanTopLevelGeneral(str, delimiter) {
60
+ const results = [];
61
+ let current = "";
62
+ let quote = null;
63
+ const delimiters = [];
64
+ let balanced = true;
65
+ for (let i = 0;i < str.length; i++) {
66
+ const char = str[i];
67
+ if (quote !== null) {
68
+ current += char;
69
+ if (char === "\\") {
70
+ const escaped = str[i + 1];
71
+ if (escaped !== undefined) {
72
+ current += escaped;
73
+ i++;
74
+ }
75
+ } else if (char === quote) {
76
+ const doubledPrologQuote = quote === "'" && str[i + 1] === "'";
77
+ if (doubledPrologQuote) {
78
+ current += str[i + 1] ?? "";
79
+ i++;
80
+ } else {
81
+ quote = null;
82
+ }
83
+ }
84
+ continue;
85
+ }
86
+ if (char === "'" || char === '"') {
87
+ quote = char;
88
+ current += char;
89
+ continue;
90
+ }
91
+ if (char === "[" || char === "(" || char === "{") {
92
+ delimiters.push(char);
93
+ current += char;
94
+ continue;
95
+ }
96
+ if (char === "]" || char === ")" || char === "}") {
97
+ const expectedOpening = char === "]" ? "[" : char === ")" ? "(" : "{";
98
+ if (delimiters.at(-1) !== expectedOpening) {
99
+ balanced = false;
100
+ } else {
101
+ delimiters.pop();
102
+ }
103
+ current += char;
104
+ continue;
105
+ }
106
+ if (delimiters.length === 0 && delimiter.length > 0 && str.startsWith(delimiter, i)) {
107
+ if (current) {
108
+ results.push(current);
109
+ current = "";
110
+ }
111
+ i += delimiter.length - 1;
112
+ } else {
113
+ current += char;
114
+ }
115
+ }
116
+ if (current) {
117
+ results.push(current);
118
+ }
119
+ return {
120
+ parts: results,
121
+ balanced: balanced && quote === null && delimiters.length === 0
122
+ };
123
+ }
124
+
125
+ // ../cli/src/prolog/error-terms.ts
126
+ function extractPrologErrorRecord(text) {
127
+ const match = text.match(/^__KIBI_ERROR__:(.+)$/m);
128
+ const term = match?.[1];
129
+ if (term === undefined)
130
+ return null;
131
+ return parsePrologErrorTerm(term);
132
+ }
133
+ function parsePrologErrorTerm(termText) {
134
+ const trimmed = termText.trim();
135
+ const formalMatch = trimmed.match(/^error\((.*)\)$/s);
136
+ const formalAndContext = formalMatch?.[1];
137
+ if (formalAndContext === undefined)
138
+ return null;
139
+ const [formalRaw, contextRaw] = splitTopLevelGeneral(formalAndContext, ",");
140
+ if (!formalRaw)
141
+ return null;
142
+ const context = contextRaw ? parseContextTerm(contextRaw) : null;
143
+ const parsed = parseFormalTerm(formalRaw);
144
+ if (!parsed)
145
+ return null;
146
+ switch (parsed.kind) {
147
+ case "invalid_relationship": {
148
+ const formatted = contextRaw ? parseInvalidRelationshipContext(contextRaw) : null;
149
+ const relationship = formatted !== null ? substituteRelationshipArgs(formatted.formatTemplate, formatted.formatArgs) : null;
150
+ if (relationship === null) {
151
+ return {
152
+ code: "invalid_relationship",
153
+ message: "Invalid relationship type or direction"
154
+ };
155
+ }
156
+ return {
157
+ code: "invalid_relationship",
158
+ relationship,
159
+ message: `Invalid relationship: ${relationship.relType} from ${relationship.fromType} to ${relationship.toType}`
160
+ };
161
+ }
162
+ case "stale_snapshot":
163
+ return {
164
+ code: "stale_snapshot",
165
+ message: "KB snapshot is stale; reattach or refresh the runtime before retrying (stale_snapshot)"
166
+ };
167
+ case "audit_locked":
168
+ return {
169
+ code: "audit_locked",
170
+ message: "Audit journal is locked by another Kibi runtime; restart the stale MCP/CLI session before retrying"
171
+ };
172
+ case "store_locked": {
173
+ const detail = parseStoreLockedContext(contextRaw);
174
+ const holder = detail.owner;
175
+ const holderSummary = holder === null ? "an unidentified holder" : `pid ${holder.pid ?? "?"}${holder.workspaceRoot !== undefined ? ` for ${holder.workspaceRoot}` : ""}${holder.startedAt !== undefined ? ` since ${holder.startedAt}` : ""}`;
176
+ return {
177
+ code: "permission_denied",
178
+ storeLocked: detail,
179
+ message: `Branch store is locked by ${holderSummary}; close that session or, if it is dead, retry so Kibi can break the stale lock (store_locked)`
180
+ };
181
+ }
182
+ case "permission_denied":
183
+ return {
184
+ code: "permission_denied",
185
+ message: "Access denied or KB locked"
186
+ };
187
+ case "entity_not_found": {
188
+ const entityId = unquoteAtom(parsed.entityId);
189
+ const role = parseEntityRole(context?.message);
190
+ const verb = role === "source" ? "Source entity" : role === "target" ? "Target entity" : "Entity";
191
+ const record = {
192
+ code: "entity_not_found",
193
+ entityId,
194
+ message: `${verb} does not exist: ${entityId}`
195
+ };
196
+ return role === undefined ? record : { ...record, role };
197
+ }
198
+ case "contradiction": {
199
+ const conflicts = parsed.pairs.map((pair) => ({
200
+ reason: pair[0],
201
+ otherId: pair[1]
202
+ }));
203
+ return {
204
+ code: "contradiction",
205
+ conflicts,
206
+ message: context?.message ?? "Contradiction detected: conflicting requirements without a supersedes relationship"
207
+ };
208
+ }
209
+ case "validation_error":
210
+ return {
211
+ code: "validation_error",
212
+ message: parsed.message
213
+ };
214
+ }
215
+ }
216
+ function parseContextTerm(contextRaw) {
217
+ const contextMatch = contextRaw.trim().match(/^context\((.*)\)$/s);
218
+ if (contextMatch?.[1] === undefined) {
219
+ const message = unquoteAtom(contextRaw.trim());
220
+ return message === "" ? null : { message };
221
+ }
222
+ const parts = splitTopLevelGeneral(contextMatch[1], ",");
223
+ if (parts.length === 0)
224
+ return null;
225
+ const predicate = parts[0]?.trim();
226
+ const message = parts[1] ? unquoteAtom(parts[1].trim()) : "";
227
+ return {
228
+ ...predicate ? { predicate } : {},
229
+ ...message ? { message } : {}
230
+ };
231
+ }
232
+ function parseStoreLockedContext(contextRaw) {
233
+ const empty = {
234
+ owner: null,
235
+ lockDirectory: ""
236
+ };
237
+ if (!contextRaw)
238
+ return empty;
239
+ const inner = contextRaw.trim().match(/^kb_store_locked\((.*)\)$/s)?.[1];
240
+ if (inner === undefined)
241
+ return empty;
242
+ const parts = splitTopLevelGeneral(inner, ",");
243
+ const ownerRaw = parts[0]?.trim();
244
+ const lockRaw = parts[1]?.trim();
245
+ const lockDirectory = lockRaw ? unquoteAtom(lockRaw) : "";
246
+ const originalRaw = parts[2]?.trim();
247
+ const originalError = originalRaw ? unquoteAtom(originalRaw) : "";
248
+ if (!ownerRaw || ownerRaw === '""' || ownerRaw === "''") {
249
+ return {
250
+ owner: null,
251
+ lockDirectory,
252
+ ...originalError !== "" ? { originalError } : {}
253
+ };
254
+ }
255
+ try {
256
+ const parsed = JSON.parse(unquoteAtom(ownerRaw));
257
+ if (parsed === null || typeof parsed !== "object") {
258
+ return {
259
+ owner: null,
260
+ lockDirectory,
261
+ ...originalError !== "" ? { originalError } : {}
262
+ };
263
+ }
264
+ return {
265
+ owner: {
266
+ ...typeof parsed.pid === "number" ? { pid: parsed.pid } : {},
267
+ ...typeof parsed.workspaceRoot === "string" ? { workspaceRoot: parsed.workspaceRoot } : {},
268
+ ...typeof parsed.bootId === "string" ? { bootId: parsed.bootId } : {},
269
+ ...typeof parsed.startedAt === "string" ? { startedAt: parsed.startedAt } : {}
270
+ },
271
+ lockDirectory,
272
+ ...originalError !== "" ? { originalError } : {}
273
+ };
274
+ } catch {
275
+ return {
276
+ owner: null,
277
+ lockDirectory,
278
+ ...originalError !== "" ? { originalError } : {}
279
+ };
280
+ }
281
+ }
282
+ function parseFormalTerm(formalRaw) {
283
+ const formal = formalRaw.trim();
284
+ const functorMatch = formal.match(/^([a-z][A-Za-z0-9_]*)\((.*)\)$/s);
285
+ if (functorMatch?.[1] === undefined || functorMatch[2] === undefined) {
286
+ return null;
287
+ }
288
+ const functor = functorMatch[1];
289
+ const args = functorMatch[2].trim() ? splitTopLevelGeneral(functorMatch[2], ",") : [];
290
+ if (functor === "permission_error") {
291
+ const operation = args[0]?.trim();
292
+ const resource = args[1]?.trim();
293
+ const detail = args[2]?.trim();
294
+ if (detail === "stale_snapshot")
295
+ return { kind: "stale_snapshot" };
296
+ if (operation === "lock" && resource === "audit_log") {
297
+ return { kind: "audit_locked" };
298
+ }
299
+ if (operation === "attach" && resource === "kb_store") {
300
+ return { kind: "store_locked" };
301
+ }
302
+ return { kind: "permission_denied" };
303
+ }
304
+ if (functor === "existence_error" && args[0]?.trim() === "entity") {
305
+ const entityId = args[1]?.trim();
306
+ if (entityId === undefined)
307
+ return null;
308
+ return { kind: "entity_not_found", entityId: unquoteAtom(entityId) };
309
+ }
310
+ if (functor === "type_error" && args[0]?.trim() === "relationship") {
311
+ return { kind: "invalid_relationship" };
312
+ }
313
+ if (functor === "kb_contradiction") {
314
+ const listText = args[0]?.trim() ?? "[]";
315
+ const pairs = parseContradictionPairs(listText);
316
+ return { kind: "contradiction", pairs };
317
+ }
318
+ if (functor === "validation_error") {
319
+ const message = args[0]?.trim();
320
+ if (message === undefined)
321
+ return null;
322
+ return { kind: "validation_error", message: unquoteAtom(message) };
323
+ }
324
+ return null;
325
+ }
326
+ function parseInvalidRelationshipContext(contextRaw) {
327
+ const contextMatch = contextRaw.trim().match(/^context\((.*)\)$/s);
328
+ const inner = contextMatch?.[1];
329
+ if (inner === undefined)
330
+ return null;
331
+ const parts = splitTopLevelGeneral(inner, ",");
332
+ const formatted = parts[1]?.trim();
333
+ if (formatted === undefined)
334
+ return null;
335
+ const formatMatch = formatted.match(/^(.*)-\[(.*)\]$/s);
336
+ const template = formatMatch?.[1];
337
+ const argsText = formatMatch?.[2];
338
+ if (template === undefined || argsText === undefined)
339
+ return null;
340
+ const formatArgs = argsText.trim() ? splitTopLevelGeneral(argsText, ",").map((arg) => unquoteAtom(arg.trim())) : [];
341
+ return { formatTemplate: unquoteAtom(template.trim()), formatArgs };
342
+ }
343
+ function substituteRelationshipArgs(template, args) {
344
+ if (template === null || args === null || args.length < 3)
345
+ return null;
346
+ const substituted = template.split("~w").map((chunk, index) => index === 0 ? chunk : `${args[index - 1] ?? ""}${chunk}`).join("");
347
+ const match = substituted.match(/^Invalid relationship:\s*(\S+)\s+from\s+(\S+)\s+to\s+(\S+)\s*$/);
348
+ if (!match?.[1] || !match[2] || !match[3])
349
+ return null;
350
+ return {
351
+ relType: match[1],
352
+ fromType: match[2],
353
+ toType: match[3]
354
+ };
355
+ }
356
+ function parseContradictionPairs(listText) {
357
+ const trimmed = listText.trim();
358
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]"))
359
+ return [];
360
+ const inner = trimmed.slice(1, -1).trim();
361
+ if (inner === "")
362
+ return [];
363
+ return splitTopLevelGeneral(inner, ",").map((item) => splitTopLevelGeneral(item.trim(), "-")).map((pair) => {
364
+ const reason = pair[0] ?? "";
365
+ const otherId = pair[1] ?? "";
366
+ return [unquoteAtom(reason.trim()), unquoteAtom(otherId.trim())];
367
+ }).filter((pair) => pair[0] !== "" && pair[1] !== "");
368
+ }
369
+ function parseEntityRole(message) {
370
+ if (message?.includes("Source entity does not exist"))
371
+ return "source";
372
+ if (message?.includes("Target entity does not exist"))
373
+ return "target";
374
+ return;
375
+ }
376
+ function unquoteAtom(value) {
377
+ const trimmed = value.trim();
378
+ if (trimmed.length >= 2 && trimmed.startsWith("'") && trimmed.endsWith("'")) {
379
+ const inner = trimmed.slice(1, -1);
380
+ return inner.replace(/\\'/g, "'").replace(/\\\\/g, "\\").replace(/\\n/g, `
381
+ `).replace(/\\t/g, "\t");
382
+ }
383
+ return trimmed;
384
+ }
385
+
49
386
  // ../cli/src/prolog.ts
50
387
  var importMetaDir = path.dirname(fileURLToPath(import.meta.url));
51
388
  var PROLOG_OUTPUT_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
52
389
  var PROLOG_OUTPUT_OVERFLOW_ERROR = "Query exceeded bounded Prolog output capacity (ENOBUFS); narrow the operation or reduce stored entity size";
53
390
  var INTERACTIVE_QUERY_FRAME_END = "__KIBI_QUERY_FRAME_END__";
54
- var require2 = createRequire(import.meta.url);
391
+ var require2 = createRequire2(import.meta.url);
55
392
  function packageVersion(packagePath) {
56
393
  try {
57
394
  const packageJson = require2(packagePath);
@@ -89,6 +426,17 @@ function resolveKbPlPath() {
89
426
  }
90
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.");
91
428
  }
429
+ function registerProcessExitOnce(current, handler, on = (event, listener) => process.on(event, listener)) {
430
+ if (current)
431
+ return current;
432
+ on("exit", handler);
433
+ return handler;
434
+ }
435
+ function bindProcessExitHandler(current, terminate, on = (event, listener) => process.on(event, listener)) {
436
+ return registerProcessExitOnce(current, () => {
437
+ terminate();
438
+ }, on);
439
+ }
92
440
 
93
441
  class PrologProcess {
94
442
  process = null;
@@ -111,6 +459,9 @@ class PrologProcess {
111
459
  this.timeout = options.timeout || 30000;
112
460
  this.useOneShotMode = options.oneShot ?? false;
113
461
  }
462
+ attachProcessExitHandler(on = (event, listener) => process.on(event, listener)) {
463
+ this.onProcessExit = bindProcessExitHandler(this.onProcessExit, () => this.terminate(), on);
464
+ }
114
465
  async start() {
115
466
  if (!existsSync(this.swiplPath) && this.swiplPath !== "swipl") {
116
467
  throw new Error(`SWI-Prolog not found at ${this.swiplPath}. Please install SWI-Prolog or check your PATH.`);
@@ -141,19 +492,14 @@ class PrologProcess {
141
492
  });
142
493
  this.process.stdin.write(`true.
143
494
  `);
144
- if (!this.onProcessExit) {
145
- this.onProcessExit = () => {
146
- this.terminate();
147
- };
148
- process.on("exit", this.onProcessExit);
149
- }
495
+ this.attachProcessExitHandler();
150
496
  await this.waitForReady();
151
497
  }
152
498
  async waitForReady() {
153
499
  const start = Date.now();
154
500
  const maxStartWait = 2000;
155
501
  while (Date.now() - start < maxStartWait) {
156
- if (!this.process || this.process.killed) {
502
+ if (!this.process || this.process.killed || this.process.exitCode !== null || this.process.signalCode !== null) {
157
503
  throw new Error(`Prolog process terminated unexpectedly during startup: ${this.translateError(this.errorBuffer)}`);
158
504
  }
159
505
  if (this.errorBuffer.includes("ERROR")) {
@@ -209,7 +555,8 @@ class PrologProcess {
209
555
  const goalLabel = this.goalLabel(normalizedGoal);
210
556
  const shouldPersist = this.isMutatingGoal(normalizedGoal) && !normalizedGoal.includes("kb_save") && !normalizedGoal.includes("kb_commit_upsert");
211
557
  const goalWithPersistence = shouldPersist ? `(${normalizedGoal}, kb_save)` : normalizedGoal;
212
- const wrappedGoal = /^once\s*\(/.test(goalWithPersistence) ? `catch(${goalWithPersistence}, _E, (print_message(error, _E), fail))` : `catch(once((${goalWithPersistence})), _E, (print_message(error, _E), fail))`;
558
+ const errorReport = `(write(user_error, '__KIBI_ERROR__:'), write_term(user_error, _E, [quoted(true), max_depth(8)]), nl(user_error), print_message(error, _E), fail)`;
559
+ const wrappedGoal = /^once\s*\(/.test(goalWithPersistence) ? `catch(${goalWithPersistence}, _E, ${errorReport})` : `catch(once((${goalWithPersistence})), _E, ${errorReport})`;
213
560
  const start = Date.now();
214
561
  if (debug) {
215
562
  console.error(`[prolog debug] start query: ${goalLabel}`);
@@ -221,7 +568,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
221
568
  let settled = false;
222
569
  const timeoutId = setTimeout(() => {
223
570
  const stage = this.lastDiagnosticStage(this.errorBuffer) ?? "unknown";
224
- 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"}, goal=${goalLabel})`;
571
+ 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.`;
225
572
  if (debug) {
226
573
  console.error(`[prolog debug] timeout: ${msg}`);
227
574
  const runtime = this.errorBuffer.split(`
@@ -266,10 +613,12 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
266
613
  console.error(`[prolog debug] query error: ${goalLabel} error=${this.errorBuffer.split(`
267
614
  `)[0]}`);
268
615
  }
616
+ const classified = this.classifyErrorOutput(this.errorBuffer);
269
617
  resolve({
270
618
  success: false,
271
619
  bindings: {},
272
- error: this.addDiagnosticStage(this.translateError(this.errorBuffer), normalizedGoal, this.errorBuffer)
620
+ error: this.addDiagnosticStage(classified.message, normalizedGoal, this.errorBuffer),
621
+ ...classified.record ? { errorRecord: classified.record } : {}
273
622
  });
274
623
  return;
275
624
  }
@@ -392,7 +741,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
392
741
  "read_term_from_atom(GoalAtom, Goal, [variable_names(Vars)])",
393
742
  kbPath ? "getenv('KIBI_KB_PATH', KBPath), kb_attach(KBPath)" : "true",
394
743
  isBatch ? "WrappedGoal = rdf_transaction(Goal)" : "WrappedGoal = Goal",
395
- "(catch(call(WrappedGoal), E, (print_message(error, E), fail)) -> QuerySucceeded = true ; QuerySucceeded = false)",
744
+ "(catch(call(WrappedGoal), E, (write(user_error, '__KIBI_ERROR__:'), write_term(user_error, E, [quoted(true), max_depth(8)]), nl(user_error), print_message(error, E), fail)) -> QuerySucceeded = true ; QuerySucceeded = false)",
396
745
  kbPath && (explicitSaveRequested || goalList.some((item) => this.isMutatingGoal(item))) ? "(QuerySucceeded == true -> kb_save ; true)" : "true",
397
746
  kbPath ? "kb_detach" : "true",
398
747
  "(QuerySucceeded == true -> (forall(member(Name=Value, Vars), (write(Name), write('='), write_term(Value, [quoted(true), max_depth(0)]), writeln('.'))), writeln('__KIBI_TRUE__.')) ; writeln('__KIBI_FALSE__.'))"
@@ -505,10 +854,12 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
505
854
  };
506
855
  }
507
856
  if (stderr.includes("ERROR")) {
857
+ const classified = this.classifyErrorOutput(stderr);
508
858
  return {
509
859
  success: false,
510
860
  bindings: {},
511
- error: this.addDiagnosticStage(this.translateError(stderr), combinedGoal ?? "true", stderr)
861
+ error: this.addDiagnosticStage(classified.message, combinedGoal ?? "true", stderr),
862
+ ...classified.record ? { errorRecord: classified.record } : {}
512
863
  };
513
864
  }
514
865
  if (stdout.includes("__KIBI_TRUE__")) {
@@ -622,7 +973,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
622
973
  }
623
974
  }
624
975
  isProcessUsable() {
625
- return Boolean(this.process?.stdin && !this.process?.killed && this.process?.exitCode === null);
976
+ return Boolean(this.process?.stdin && !this.process?.killed && this.process?.exitCode === null && this.process?.signalCode === null);
626
977
  }
627
978
  extractBindings(output) {
628
979
  const bindings = {};
@@ -651,8 +1002,15 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
651
1002
  flush();
652
1003
  return bindings;
653
1004
  }
1005
+ classifyErrorOutput(errorText) {
1006
+ const record = extractPrologErrorRecord(errorText);
1007
+ if (record) {
1008
+ return { message: record.message, record };
1009
+ }
1010
+ return { message: this.translateError(errorText) };
1011
+ }
654
1012
  translateError(errorText) {
655
- const cleanError = errorText.replace(/^__KIBI_(?:STAGE|RUNTIME)__:[^\r\n]*\r?\n?/gm, "");
1013
+ const cleanError = errorText.replace(/^__KIBI_(?:STAGE|RUNTIME|ERROR)__:[^\r\n]*\r?\n?/gm, "");
656
1014
  if (cleanError.includes("stale_snapshot")) {
657
1015
  return "KB snapshot is stale; reattach or refresh the runtime before retrying (stale_snapshot)";
658
1016
  }
@@ -696,7 +1054,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
696
1054
  return simpleError || "Unknown error";
697
1055
  }
698
1056
  isRunning() {
699
- return Boolean(this.process && !this.process.killed && this.process?.exitCode === null);
1057
+ return Boolean(this.process && !this.process.killed && this.process?.exitCode === null && this.process?.signalCode === null);
700
1058
  }
701
1059
  getPid() {
702
1060
  return this.process?.pid || 0;
@@ -731,223 +1089,105 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
731
1089
  }
732
1090
  }
733
1091
 
734
- // ../cli/src/prolog/codec.ts
735
- function parseListOfLists(listStr) {
736
- const cleaned = listStr.trim().replace(/^\[/, "").replace(/\]$/, "");
737
- if (cleaned === "") {
738
- return [];
1092
+ // ../cli/src/prolog/store-lock.ts
1093
+ import { existsSync as existsSync2, rmSync } from "node:fs";
1094
+ import { readFileSync } from "node:fs";
1095
+ import { dirname, join } from "node:path";
1096
+ var cachedBootId;
1097
+ function currentBootId() {
1098
+ if (cachedBootId !== undefined)
1099
+ return cachedBootId;
1100
+ if (process.platform !== "linux") {
1101
+ cachedBootId = null;
1102
+ return cachedBootId;
739
1103
  }
740
- const results = [];
741
- let depth = 0;
742
- let current = "";
743
- let currentList = [];
744
- for (let i = 0;i < cleaned.length; i++) {
745
- const char = cleaned[i];
746
- if (char === "[") {
747
- depth++;
748
- if (depth > 1)
749
- current += char;
750
- } else if (char === "]") {
751
- depth--;
752
- if (depth === 0) {
753
- if (current) {
754
- currentList.push(current.trim());
755
- current = "";
756
- }
757
- if (currentList.length > 0) {
758
- results.push(currentList);
759
- currentList = [];
760
- }
761
- } else {
762
- current += char;
763
- }
764
- } else if (char === "," && depth === 1) {
765
- if (current) {
766
- currentList.push(current.trim());
767
- current = "";
768
- }
769
- } else if (char === "," && depth === 0) {} else {
770
- current += char;
771
- }
1104
+ try {
1105
+ const raw = readFileSync("/proc/sys/kernel/random/boot_id", "utf8");
1106
+ cachedBootId = raw.trim();
1107
+ } catch {
1108
+ cachedBootId = null;
772
1109
  }
773
- return results;
1110
+ return cachedBootId;
774
1111
  }
775
- function parseEntityFromList(data) {
776
- if (data.length < 3) {
777
- return {};
778
- }
779
- const [idPart, typePart, propsPart] = data;
780
- if (idPart === undefined || typePart === undefined || propsPart === undefined) {
781
- return {};
1112
+ function isProcessAlive(pid) {
1113
+ if (!Number.isInteger(pid) || pid <= 0)
1114
+ return false;
1115
+ try {
1116
+ process.kill(pid, 0);
1117
+ return true;
1118
+ } catch (error) {
1119
+ const code = error?.code;
1120
+ return code === "EPERM";
782
1121
  }
783
- const id = idPart.trim();
784
- const type = typePart.trim();
785
- const propsStr = propsPart.trim();
786
- const props = parsePropertyList(propsStr);
787
- return { ...props, id: normalizeEntityId(stripOuterQuotes(id)), type };
788
1122
  }
789
- function parsePropertyList(propsStr) {
790
- const props = {};
791
- let cleaned = propsStr.trim();
792
- if (cleaned.startsWith("[")) {
793
- cleaned = cleaned.substring(1);
794
- }
795
- if (cleaned.endsWith("]")) {
796
- cleaned = cleaned.substring(0, cleaned.length - 1);
797
- }
798
- const pairs = splitTopLevelGeneral(cleaned, ",");
799
- for (const pair of pairs) {
800
- const eqIndex = pair.indexOf("=");
801
- if (eqIndex === -1)
802
- continue;
803
- const key = pair.substring(0, eqIndex).trim();
804
- const value = pair.substring(eqIndex + 1).trim();
805
- if (key === "..." || value === "..." || value === "...|...") {
806
- continue;
807
- }
808
- let parsed = parsePrologValue(value);
809
- if ((key === "rule_ir" || key === "semantic_inventory" || key === "verification_receipts" || key === "verification_contract") && typeof parsed === "string") {
810
- try {
811
- parsed = JSON.parse(parsed);
812
- } catch {}
813
- }
814
- if (!Object.hasOwn(props, key)) {
815
- props[key] = parsed;
816
- continue;
817
- }
818
- const existing = props[key];
819
- props[key] = Array.isArray(existing) ? [...existing, parsed] : [existing, parsed];
820
- }
821
- return props;
822
- }
823
- function parsePrologValue(valueInput) {
824
- const value = valueInput.trim();
825
- if (value.startsWith("^^(")) {
826
- const innerStart = value.indexOf("(") + 1;
827
- let depth = 1;
828
- let innerEnd = innerStart;
829
- for (let i = innerStart;i < value.length; i++) {
830
- if (value[i] === "(")
831
- depth++;
832
- if (value[i] === ")") {
833
- depth--;
834
- if (depth === 0) {
835
- innerEnd = i;
836
- break;
837
- }
838
- }
839
- }
840
- const innerContent = value.substring(innerStart, innerEnd);
841
- const parts = splitTopLevelGeneral(innerContent, ",");
842
- if (parts.length >= 2) {
843
- const literalPart = parts[0];
844
- const datatypePart = parts[1];
845
- if (literalPart === undefined || datatypePart === undefined) {
846
- return value;
847
- }
848
- let literalValue = literalPart.trim();
849
- const datatype = datatypePart.trim();
850
- if (literalValue.startsWith('"') && literalValue.endsWith('"')) {
851
- literalValue = literalValue.substring(1, literalValue.length - 1);
852
- }
853
- if (datatype.includes("#integer")) {
854
- return Number.parseInt(literalValue, 10);
855
- }
856
- if (datatype.includes("#decimal") || datatype.includes("#double")) {
857
- return Number.parseFloat(literalValue);
858
- }
859
- if (datatype.includes("#boolean")) {
860
- return literalValue === "true";
861
- }
862
- if (literalValue.startsWith("[") && literalValue.endsWith("]")) {
863
- const listContent = literalValue.substring(1, literalValue.length - 1);
864
- if (listContent === "") {
865
- return [];
866
- }
867
- return splitTopLevelGeneral(listContent, ",").map((item) => item.trim());
868
- }
869
- return literalValue;
870
- }
871
- }
872
- if (value.startsWith("file:///")) {
873
- const lastSlash = value.lastIndexOf("/");
874
- if (lastSlash !== -1) {
875
- return value.substring(lastSlash + 1);
876
- }
877
- return value;
878
- }
879
- if (value.startsWith('"') && value.endsWith('"')) {
880
- try {
881
- return JSON.parse(value);
882
- } catch {
883
- return value.substring(1, value.length - 1);
884
- }
885
- }
886
- if (value.startsWith("'") && value.endsWith("'")) {
887
- return value.substring(1, value.length - 1);
888
- }
889
- if (value.startsWith("[") && value.endsWith("]")) {
890
- const listContent = value.substring(1, value.length - 1);
891
- if (listContent === "") {
892
- return [];
893
- }
894
- const items = splitTopLevelGeneral(listContent, ",").map((item) => {
895
- return parsePrologValue(item.trim());
896
- });
897
- return items;
898
- }
899
- return value;
1123
+ function classifyStoreLockHolder(owner) {
1124
+ if (owner === null || typeof owner.pid !== "number")
1125
+ return "unknown";
1126
+ if (owner.pid === process.pid)
1127
+ return "self";
1128
+ const alive = isProcessAlive(owner.pid);
1129
+ if (!alive)
1130
+ return "dead";
1131
+ const bootId = currentBootId();
1132
+ if (bootId !== null && owner.bootId !== undefined && owner.bootId !== "") {
1133
+ return owner.bootId === bootId ? "alive" : "dead";
1134
+ }
1135
+ return "alive";
900
1136
  }
901
- function splitTopLevelGeneral(str, delimiter) {
902
- const results = [];
903
- let current = "";
904
- let depth = 0;
905
- let inDoubleQuotes = false;
906
- let inSingleQuotes = false;
907
- for (let i = 0;i < str.length; i++) {
908
- const char = str[i];
909
- const prevChar = i > 0 ? str[i - 1] : "";
910
- if (char === '"' && !inSingleQuotes && prevChar !== "\\") {
911
- inDoubleQuotes = !inDoubleQuotes;
912
- current += char;
913
- } else if (char === "'" && !inDoubleQuotes && prevChar !== "\\") {
914
- inSingleQuotes = !inSingleQuotes;
915
- current += char;
916
- } else if (!inDoubleQuotes && !inSingleQuotes && (char === "[" || char === "(")) {
917
- depth++;
918
- current += char;
919
- } else if (!inDoubleQuotes && !inSingleQuotes && (char === "]" || char === ")")) {
920
- depth--;
921
- current += char;
922
- } else if (!inDoubleQuotes && !inSingleQuotes && depth === 0 && char === delimiter) {
923
- if (current) {
924
- results.push(current);
925
- current = "";
926
- }
927
- } else {
928
- current += char;
1137
+ function breakStoreLock(rdfDirectory) {
1138
+ const candidates = [
1139
+ join(rdfDirectory, "lock"),
1140
+ join(rdfDirectory, ".kibi-lock-owner.json"),
1141
+ join(dirname(rdfDirectory), ".kibi-lock-owner.json")
1142
+ ];
1143
+ for (const candidate of candidates) {
1144
+ if (existsSync2(candidate)) {
1145
+ rmSync(candidate, { force: true });
929
1146
  }
930
1147
  }
931
- if (current) {
932
- results.push(current);
933
- }
934
- return results;
935
1148
  }
936
- function stripOuterQuotes(value) {
937
- if (value.startsWith("'") && value.endsWith("'")) {
938
- return value.slice(1, -1);
939
- }
940
- if (value.startsWith('"') && value.endsWith('"')) {
941
- return value.slice(1, -1);
1149
+ function decideStoreLockTakeover(owner) {
1150
+ const state = classifyStoreLockHolder(owner);
1151
+ switch (state) {
1152
+ case "dead":
1153
+ return {
1154
+ action: "break",
1155
+ reason: `recorded holder pid ${owner?.pid ?? "?"} is no longer running`
1156
+ };
1157
+ case "unknown":
1158
+ return {
1159
+ action: "surface",
1160
+ reason: "no ownership journal; the holder cannot be verified"
1161
+ };
1162
+ case "self":
1163
+ case "alive":
1164
+ return {
1165
+ action: "surface",
1166
+ reason: `holder pid ${owner?.pid ?? "?"} is still running${owner?.workspaceRoot !== undefined ? ` for ${owner.workspaceRoot}` : ""}`
1167
+ };
942
1168
  }
943
- return value;
944
1169
  }
945
- function normalizeEntityId(value) {
946
- if (!value.startsWith("file:///")) {
947
- return value;
1170
+ async function retryAttachAfterBreakingStaleLock(prolog, kbPath, failed) {
1171
+ const record = failed.errorRecord;
1172
+ const storeLocked = record?.storeLocked;
1173
+ if (storeLocked === undefined)
1174
+ return failed;
1175
+ const decision = decideStoreLockTakeover(storeLocked.owner);
1176
+ if (decision.action !== "break") {
1177
+ const holderSummary = storeLocked.owner === null ? decision.reason : `pid ${storeLocked.owner.pid ?? "?"} (${decision.reason})`;
1178
+ return {
1179
+ ...failed,
1180
+ error: `${failed.error}
1181
+ Store lock holder: ${holderSummary}. Close that session, or run 'kibi engine stop' for its workspace, then retry.`
1182
+ };
1183
+ }
1184
+ const lockDirectory = storeLocked.lockDirectory !== "" ? storeLocked.lockDirectory : join(kbPath, "rdf");
1185
+ breakStoreLock(lockDirectory);
1186
+ const retried = await prolog.query(`kb_attach('${escapeAtom(kbPath)}')`);
1187
+ if (retried.success) {
1188
+ console.warn(`[KIBI] Broke a stale branch-store lock (${decision.reason}) and reattached.`);
948
1189
  }
949
- const idx = value.lastIndexOf("/");
950
- return idx === -1 ? value : value.slice(idx + 1);
1190
+ return retried;
951
1191
  }
952
1192
 
953
1193
  // ../cli/src/utils/branch-resolver.ts
@@ -960,9 +1200,9 @@ import * as path3 from "node:path";
960
1200
  // ../cli/src/utils/branch-store-locator.ts
961
1201
  import { createHash } from "node:crypto";
962
1202
  import {
963
- existsSync as existsSync2,
1203
+ existsSync as existsSync3,
964
1204
  mkdirSync,
965
- readFileSync,
1205
+ readFileSync as readFileSync2,
966
1206
  readdirSync,
967
1207
  statSync,
968
1208
  writeFileSync
@@ -990,10 +1230,10 @@ function expectedBranchStoreManifest(branch) {
990
1230
  }
991
1231
  function readBranchStoreManifest(storePath) {
992
1232
  const manifestPath = branchStoreManifestPath(storePath);
993
- if (!existsSync2(manifestPath))
1233
+ if (!existsSync3(manifestPath))
994
1234
  return null;
995
1235
  try {
996
- const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
1236
+ const parsed = JSON.parse(readFileSync2(manifestPath, "utf8"));
997
1237
  if (parsed.version !== BRANCH_STORE_MANIFEST_VERSION || typeof parsed.branch !== "string" || typeof parsed.key !== "string") {
998
1238
  return null;
999
1239
  }
@@ -1020,7 +1260,7 @@ function ensureBranchStoreManifest(workspaceRoot, branch) {
1020
1260
  throw new Error(`Branch store identity mismatch at ${storePath}; refusing to attach it to '${branch}'.`);
1021
1261
  }
1022
1262
  if (existing === null) {
1023
- if (existsSync2(manifestPath)) {
1263
+ if (existsSync3(manifestPath)) {
1024
1264
  throw new Error(`Branch store manifest is invalid at ${manifestPath}; refusing to overwrite it.`);
1025
1265
  }
1026
1266
  const entries = readdirSync(storePath);
@@ -1082,11 +1322,41 @@ var ENGINE_PACKAGE_VERSIONS = process.env.KIBI_PACKAGE_VERSIONS ?? "unknown";
1082
1322
  var ENGINE_QUERY_CACHE_MAX_ENTRIES = 128;
1083
1323
  var ENGINE_QUERY_CACHE_MAX_RESULT_BYTES = 8 * 1024 * 1024;
1084
1324
  var ENGINE_FRESHNESS_CACHE_MS = 100;
1085
- var ENGINE_PUBLICATION_LOCK_STALE_MS = 5000;
1325
+ var ENGINE_WORKSPACE_WATCHDOG_MS = 30000;
1326
+ function requestEngineSignalShutdown(shutdown) {
1327
+ return () => {
1328
+ shutdown();
1329
+ };
1330
+ }
1331
+ function readJournalGeneration(branchPath) {
1332
+ const currentPath = path4.join(branchPath, "CURRENT");
1333
+ if (!existsSync4(currentPath))
1334
+ return "";
1335
+ return readFileSync3(currentPath, "utf8").trim();
1336
+ }
1337
+ function readEngineAttachmentIdentity(branchPath) {
1338
+ if (!existsSync4(branchPath))
1339
+ return null;
1340
+ const stats = statSync2(branchPath);
1341
+ let resolved = path4.resolve(branchPath);
1342
+ try {
1343
+ resolved = realpathSync(branchPath);
1344
+ } catch {}
1345
+ return {
1346
+ path: resolved,
1347
+ generation: readJournalGeneration(branchPath),
1348
+ dev: stats.dev,
1349
+ ino: stats.ino
1350
+ };
1351
+ }
1086
1352
  function engineIdleTimeoutMs() {
1087
1353
  const configured = Number.parseInt(process.env.KIBI_ENGINE_IDLE_TIMEOUT_MS ?? "", 10);
1088
1354
  return Number.isFinite(configured) && configured >= 100 ? configured : ENGINE_IDLE_TIMEOUT_MS;
1089
1355
  }
1356
+ function engineWorkspaceWatchdogMs() {
1357
+ const configured = Number.parseInt(process.env.KIBI_ENGINE_WORKSPACE_WATCHDOG_MS ?? "", 10);
1358
+ return Number.isFinite(configured) && configured >= 100 ? configured : ENGINE_WORKSPACE_WATCHDOG_MS;
1359
+ }
1090
1360
  function quoteProlog(value) {
1091
1361
  return value.replaceAll("'", "''");
1092
1362
  }
@@ -1215,7 +1485,7 @@ function runtimeDirectory() {
1215
1485
  throw new Error("Unable to create a writable Kibi engine runtime directory");
1216
1486
  }
1217
1487
  function fsyncPath(filePath) {
1218
- if (!existsSync3(filePath))
1488
+ if (!existsSync4(filePath))
1219
1489
  return;
1220
1490
  let descriptor = null;
1221
1491
  try {
@@ -1229,7 +1499,7 @@ function fsyncPath(filePath) {
1229
1499
  function fsyncJournaledBranchStore(branchPath) {
1230
1500
  const rdfRoot = path4.join(branchPath, "rdf");
1231
1501
  const directories = [];
1232
- const pending = existsSync3(rdfRoot) ? [rdfRoot] : [];
1502
+ const pending = existsSync4(rdfRoot) ? [rdfRoot] : [];
1233
1503
  while (pending.length > 0) {
1234
1504
  const directory = pending.pop();
1235
1505
  if (directory === undefined)
@@ -1273,115 +1543,13 @@ function enginePidPath(workspaceRoot, branch) {
1273
1543
  }
1274
1544
  return `${socket}.pid`;
1275
1545
  }
1276
- function engineStartLockPath(workspaceRoot, branch) {
1277
- if (process.platform === "win32") {
1278
- return path4.join(runtimeDirectory(), `kibi-${createHash2("sha256").update(`${path4.resolve(workspaceRoot)}\x00${branch}`).digest("hex")}.start.lock`);
1279
- }
1280
- return `${engineSocketPath(workspaceRoot, branch)}.start.lock`;
1281
- }
1282
- function enginePublicationLockPath(workspaceRoot, branch) {
1283
- return `${engineSocketPath(workspaceRoot, branch)}.publish.lock`;
1284
- }
1285
- function publicationLockOwner(lockPath) {
1286
- try {
1287
- const [pidText, createdText] = readFileSync2(lockPath, "utf8").trim().split(":");
1288
- const pid = Number.parseInt(pidText ?? "", 10);
1289
- const createdAt = Number.parseInt(createdText ?? "", 10);
1290
- if (!Number.isInteger(pid) || pid <= 1 || !Number.isFinite(createdAt)) {
1291
- return null;
1292
- }
1293
- return { pid, createdAt };
1294
- } catch {
1295
- return null;
1296
- }
1297
- }
1298
- function processIsAlive(pid) {
1299
- try {
1300
- process.kill(pid, 0);
1301
- return true;
1302
- } catch {
1303
- return false;
1304
- }
1305
- }
1306
- function clearStalePublicationLock(lockPath) {
1307
- if (!existsSync3(lockPath))
1308
- return true;
1309
- const owner = publicationLockOwner(lockPath);
1310
- let age = 0;
1311
- try {
1312
- age = Date.now() - statSync2(lockPath).mtimeMs;
1313
- } catch {
1314
- return false;
1315
- }
1316
- if ((owner === null || !processIsAlive(owner.pid)) && age > ENGINE_PUBLICATION_LOCK_STALE_MS) {
1317
- try {
1318
- unlinkSync(lockPath);
1319
- return true;
1320
- } catch {
1321
- return false;
1322
- }
1323
- }
1324
- return false;
1325
- }
1326
- function publicationLockIsActive(lockPath) {
1327
- if (!existsSync3(lockPath))
1328
- return false;
1329
- if (clearStalePublicationLock(lockPath))
1330
- return false;
1331
- return existsSync3(lockPath);
1332
- }
1333
- function tryAcquireStartLock(lockPath) {
1334
- try {
1335
- const descriptor = openSync(lockPath, "wx", 384);
1336
- writeFileSync2(lockPath, `${process.pid}:${Date.now()}
1337
- `, { flag: "w" });
1338
- return descriptor;
1339
- } catch {
1340
- try {
1341
- const stamp = readFileSync2(lockPath, "utf8").trim().split(":");
1342
- const pid = Number.parseInt(stamp[0] ?? "", 10);
1343
- const created = Number.parseInt(stamp[1] ?? "", 10);
1344
- let age = 0;
1345
- try {
1346
- age = Date.now() - statSync2(lockPath).mtimeMs;
1347
- } catch {
1348
- age = 0;
1349
- }
1350
- if (!Number.isFinite(created) && age <= 5000)
1351
- return null;
1352
- let alive = false;
1353
- if (Number.isFinite(pid) && pid > 0) {
1354
- try {
1355
- process.kill(pid, 0);
1356
- alive = true;
1357
- } catch {
1358
- alive = false;
1359
- }
1360
- }
1361
- if (!alive && (Number.isFinite(created) ? Date.now() - created > 5000 : age > 5000)) {
1362
- unlinkSync(lockPath);
1363
- }
1364
- } catch {}
1365
- return null;
1366
- }
1367
- }
1368
- function releaseStartLock(lockPath, descriptor) {
1369
- if (descriptor !== null) {
1370
- try {
1371
- closeSync(descriptor);
1372
- } catch {}
1373
- }
1374
- try {
1375
- unlinkSync(lockPath);
1376
- } catch {}
1377
- }
1378
1546
  function recoverStaleJournalLock(branchPath) {
1379
1547
  const lockPath = path4.join(branchPath, "rdf", "lock");
1380
- if (!existsSync3(lockPath))
1548
+ if (!existsSync4(lockPath))
1381
1549
  return;
1382
1550
  let contents = "";
1383
1551
  try {
1384
- contents = readFileSync2(lockPath, "utf8");
1552
+ contents = readFileSync3(lockPath, "utf8");
1385
1553
  } catch {
1386
1554
  return;
1387
1555
  }
@@ -1396,25 +1564,12 @@ function recoverStaleJournalLock(branchPath) {
1396
1564
  return;
1397
1565
  } catch {
1398
1566
  try {
1399
- rmSync(lockPath, { force: true });
1567
+ rmSync2(lockPath, { force: true });
1400
1568
  } catch {}
1401
1569
  }
1402
1570
  }
1403
- function daemonEntry() {
1404
- const moduleDirectory = path4.dirname(fileURLToPath2(import.meta.url));
1405
- const candidates = [
1406
- path4.join(moduleDirectory, "engine-daemon.js"),
1407
- path4.join(moduleDirectory, "..", "dist", "engine-daemon.js"),
1408
- path4.join(moduleDirectory, "..", "node_modules", "kibi-cli", "dist", "engine-daemon.js")
1409
- ];
1410
- for (const candidate of candidates) {
1411
- if (existsSync3(candidate))
1412
- return candidate;
1413
- }
1414
- throw new Error("The Kibi engine is not built. Run `npm run build:cli` before using the CLI engine.");
1415
- }
1416
1571
  function connectSocket(socketPath, timeoutMs) {
1417
- return new Promise((resolve3, reject) => {
1572
+ return new Promise((resolve, reject) => {
1418
1573
  const socket = net.createConnection(socketPath);
1419
1574
  let settled = false;
1420
1575
  const timer = setTimeout(() => {
@@ -1429,7 +1584,7 @@ function connectSocket(socketPath, timeoutMs) {
1429
1584
  return;
1430
1585
  settled = true;
1431
1586
  clearTimeout(timer);
1432
- resolve3(socket);
1587
+ resolve(socket);
1433
1588
  });
1434
1589
  socket.on("error", (error) => {
1435
1590
  if (settled)
@@ -1440,409 +1595,39 @@ function connectSocket(socketPath, timeoutMs) {
1440
1595
  });
1441
1596
  });
1442
1597
  }
1443
- async function connectWithRetry(socketPath, timeoutMs) {
1444
- const deadline = Date.now() + timeoutMs;
1445
- let lastError;
1446
- while (Date.now() < deadline) {
1447
- const errorPath = `${socketPath}.error`;
1448
- if (existsSync3(errorPath)) {
1449
- try {
1450
- const diagnostic = readFileSync2(errorPath, "utf8").trim();
1451
- if (diagnostic)
1452
- throw new Error(diagnostic);
1453
- } catch (error) {
1454
- throw new Error(`Kibi engine failed to start: ${error instanceof Error ? error.message : String(error)}`);
1455
- }
1456
- }
1457
- if (process.platform !== "win32" && !existsSync3(socketPath)) {
1458
- await new Promise((resolve3) => setTimeout(resolve3, 25));
1459
- continue;
1460
- }
1461
- try {
1462
- return await connectSocket(socketPath, Math.min(250, deadline - Date.now()));
1463
- } catch (error) {
1464
- lastError = error;
1465
- await new Promise((resolve3) => setTimeout(resolve3, 25));
1466
- }
1467
- }
1468
- throw new Error(`Unable to connect to Kibi engine at ${socketPath}: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
1469
- }
1470
-
1471
- class EngineClient {
1472
- workspaceRoot;
1473
- branch;
1474
- timeout;
1475
- allowPublicationLock;
1476
- socket = null;
1477
- daemon = null;
1478
- inputBuffer = Buffer.alloc(0);
1479
- requestId = 0;
1480
- pending = new Map;
1481
- requestTail = Promise.resolve();
1482
- lastResult = null;
1483
- constructor(options) {
1484
- if (!isValidBranchName(options.branch)) {
1485
- throw new Error(`Invalid Kibi engine branch name: ${options.branch}`);
1486
- }
1487
- const resolvedWorkspaceRoot = path4.resolve(options.workspaceRoot);
1488
- try {
1489
- this.workspaceRoot = realpathSync(resolvedWorkspaceRoot);
1490
- } catch {
1491
- this.workspaceRoot = resolvedWorkspaceRoot;
1492
- }
1493
- this.branch = options.branch;
1494
- this.timeout = options.timeout ?? 120000;
1495
- this.allowPublicationLock = options.allowPublicationLock ?? false;
1496
- }
1497
- async start(allowSpawn = true) {
1498
- if (this.socket !== null && !this.socket.destroyed)
1499
- return;
1500
- const socketPath = engineSocketPath(this.workspaceRoot, this.branch);
1501
- const publicationLockPath = enginePublicationLockPath(this.workspaceRoot, this.branch);
1502
- if (!this.allowPublicationLock && publicationLockIsActive(publicationLockPath)) {
1503
- throw new Error(`Kibi engine publication is in progress: ${publicationLockPath}`);
1504
- }
1505
- try {
1506
- if (existsSync3(socketPath)) {
1507
- this.socket = await connectWithRetry(socketPath, 150);
1508
- } else {
1509
- throw new Error("engine socket is absent");
1510
- }
1511
- } catch {
1512
- if (!allowSpawn)
1513
- return;
1514
- const nodePath = process.env.KIBI_NODE_PATH ?? "node";
1515
- const entry = daemonEntry();
1516
- const lockPath = engineStartLockPath(this.workspaceRoot, this.branch);
1517
- const startDeadline = Date.now() + this.timeout;
1518
- let lock = null;
1519
- while (lock === null && Date.now() < startDeadline) {
1520
- if (existsSync3(socketPath)) {
1521
- try {
1522
- this.socket = await connectWithRetry(socketPath, 100);
1523
- break;
1524
- } catch {}
1525
- }
1526
- if (this.socket === null) {
1527
- lock = tryAcquireStartLock(lockPath);
1528
- if (lock === null) {
1529
- await new Promise((resolve3) => setTimeout(resolve3, 50));
1530
- }
1531
- }
1532
- }
1533
- if (this.socket === null && lock === null) {
1534
- throw new Error(`Unable to connect to Kibi engine at ${socketPath} while another starter owns ${lockPath}`);
1535
- }
1536
- if (lock !== null) {
1537
- try {
1538
- rmSync(`${socketPath}.error`, { force: true });
1539
- try {
1540
- const daemon = spawn2(nodePath, [
1541
- entry,
1542
- "--workspace",
1543
- this.workspaceRoot,
1544
- "--branch",
1545
- this.branch,
1546
- "--socket",
1547
- socketPath
1548
- ], {
1549
- detached: true,
1550
- stdio: "ignore",
1551
- env: {
1552
- ...process.env,
1553
- KIBI_ENGINE_PROTOCOL: String(ENGINE_PROTOCOL_VERSION)
1554
- }
1555
- });
1556
- await new Promise((resolve3, reject) => {
1557
- daemon.once("spawn", resolve3);
1558
- daemon.once("error", reject);
1559
- });
1560
- this.daemon = daemon;
1561
- } catch (error) {
1562
- throw new Error(`Kibi engine requires Node.js >=18 to host the daemon; failed to start '${nodePath}': ${error instanceof Error ? error.message : String(error)}`);
1563
- }
1564
- this.daemon.unref();
1565
- this.socket = await connectWithRetry(socketPath, this.timeout);
1566
- } finally {
1567
- releaseStartLock(lockPath, lock);
1568
- }
1569
- }
1570
- }
1571
- if (this.socket === null)
1572
- return;
1573
- this.attachSocket(this.socket);
1574
- }
1575
- attachSocket(socket) {
1576
- socket.on("data", (chunk) => {
1577
- try {
1578
- const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
1579
- this.inputBuffer = parseFrames(Buffer.concat([this.inputBuffer, bytes]), (value) => this.handleResponse(value));
1580
- } catch (error) {
1581
- socket.destroy(error instanceof Error ? error : new Error(String(error)));
1582
- }
1583
- });
1584
- socket.on("close", () => {
1585
- this.socket = null;
1586
- const error = new Error("Kibi engine connection closed");
1587
- for (const pending of this.pending.values())
1588
- pending.reject(error);
1589
- this.pending.clear();
1590
- });
1591
- socket.on("error", (error) => {
1592
- for (const pending of this.pending.values())
1593
- pending.reject(error);
1594
- this.pending.clear();
1595
- });
1596
- }
1597
- handleResponse(value) {
1598
- if (value === null || typeof value !== "object")
1599
- return;
1600
- const response = value;
1601
- if (typeof response.id !== "number")
1602
- return;
1603
- const pending = this.pending.get(response.id);
1604
- if (!pending)
1605
- return;
1606
- this.pending.delete(response.id);
1607
- if (response.ok)
1608
- pending.resolve(response.result);
1609
- else
1610
- pending.reject(new Error(response.error ?? "Kibi engine request failed"));
1611
- }
1612
- async request(request, signal) {
1613
- await this.start();
1614
- const socket = this.socket;
1615
- if (socket === null || socket.destroyed)
1616
- throw new Error("Kibi engine is not connected");
1617
- const id = ++this.requestId;
1618
- const result = new Promise((resolve3, reject) => {
1619
- this.pending.set(id, {
1620
- resolve: resolve3,
1621
- reject
1622
- });
1623
- try {
1624
- socket.write(frame({
1625
- ...request,
1626
- id,
1627
- protocolVersion: ENGINE_PROTOCOL_VERSION,
1628
- packageVersions: ENGINE_PACKAGE_VERSIONS,
1629
- workspaceRoot: this.workspaceRoot,
1630
- branch: this.branch
1631
- }));
1632
- } catch (error) {
1633
- this.pending.delete(id);
1634
- reject(error instanceof Error ? error : new Error(String(error)));
1635
- return;
1636
- }
1637
- if (signal !== undefined) {
1638
- const abort = () => {
1639
- if (!this.pending.has(id))
1640
- return;
1641
- this.pending.delete(id);
1642
- this.cancel(id);
1643
- reject(new Error("Kibi engine request cancelled"));
1644
- };
1645
- if (signal.aborted)
1646
- abort();
1647
- else
1648
- signal.addEventListener("abort", abort, { once: true });
1649
- }
1650
- });
1651
- return result;
1652
- }
1653
- async query(goal, signal) {
1654
- let result;
1655
- const previous = this.requestTail;
1656
- let release;
1657
- this.requestTail = new Promise((resolve3) => {
1658
- release = resolve3;
1659
- });
1660
- await previous;
1661
- try {
1662
- result = await this.request({ method: "query", goal }, signal);
1663
- this.lastResult = result;
1664
- return result;
1665
- } finally {
1666
- release();
1667
- }
1668
- }
1669
- async command(command, signal) {
1670
- return this.request({ method: "command", command }, signal);
1671
- }
1672
- async queryBatch(goals) {
1673
- const normalized = goals.map((goal) => goal.trim().replace(/\.+\s*$/, ""));
1674
- return this.query(`rdf_transaction((${normalized.join(", ")}))`);
1675
- }
1676
- async queryEntities(input, signal) {
1677
- const result = await this.command({
1678
- version: 1,
1679
- kind: "entities",
1680
- ...input.type !== undefined ? { type: input.type } : {},
1681
- ...input.id !== undefined ? { id: input.id } : {},
1682
- ...input.tags !== undefined ? { tags: input.tags } : {},
1683
- ...input.sourceFile !== undefined ? { sourceFile: input.sourceFile } : {},
1684
- limit: input.limit,
1685
- offset: input.offset
1686
- }, signal);
1687
- if (!result.success) {
1688
- throw new Error(result.error ?? "Indexed entity query failed");
1689
- }
1690
- const rows = result.bindings.Rows ? parseListOfLists(result.bindings.Rows).map(parseEntityFromList) : [];
1691
- const count = Number.parseInt(result.bindings.Count ?? "0", 10);
1692
- return {
1693
- entities: rows,
1694
- count: Number.isFinite(count) ? count : rows.length
1695
- };
1696
- }
1697
- async searchEntities(input, signal) {
1698
- const result = await this.command({
1699
- version: 1,
1700
- kind: "search",
1701
- query: input.query,
1702
- ...input.type !== undefined ? { type: input.type } : {},
1703
- limit: input.limit,
1704
- offset: input.offset
1705
- }, signal);
1706
- if (!result.success) {
1707
- throw new Error(result.error ?? "Indexed search candidate query failed");
1708
- }
1709
- const rows = result.bindings.Rows ? parseListOfLists(result.bindings.Rows).map(parseEntityFromList) : [];
1710
- const count = Number.parseInt(result.bindings.Count ?? "0", 10);
1711
- return {
1712
- entities: rows,
1713
- count: Number.isFinite(count) ? count : rows.length
1714
- };
1715
- }
1716
- cancel(requestId) {
1717
- const socket = this.socket;
1718
- if (socket === null || socket.destroyed)
1719
- return;
1720
- const id = ++this.requestId;
1721
- socket.write(frame({
1722
- id,
1723
- method: "cancel",
1724
- cancelOf: requestId,
1725
- protocolVersion: ENGINE_PROTOCOL_VERSION,
1726
- packageVersions: ENGINE_PACKAGE_VERSIONS,
1727
- workspaceRoot: this.workspaceRoot,
1728
- branch: this.branch
1729
- }));
1730
- }
1731
- async nextSolution() {
1732
- const result = this.lastResult;
1733
- this.lastResult = null;
1734
- return result;
1735
- }
1736
- async save(signal) {
1737
- return this.query("kb_save", signal);
1738
- }
1739
- async storageStatus() {
1740
- return this.request({ method: "status" });
1741
- }
1742
- async checkpoint() {
1743
- return this.command({ version: 1, kind: "checkpoint" });
1744
- }
1745
- async queryStatusJson() {
1746
- return this.command({ version: 1, kind: "status" });
1747
- }
1748
- async compact() {
1749
- return this.command({ version: 1, kind: "compact" });
1750
- }
1751
- async exportStorage(targetDirectory) {
1752
- return this.request({
1753
- method: "export",
1754
- targetDirectory
1755
- });
1756
- }
1757
- invalidateCache() {
1758
- this.lastResult = null;
1759
- }
1760
- isRunning() {
1761
- return this.socket !== null && !this.socket.destroyed;
1762
- }
1763
- getPid() {
1764
- const pidPath = enginePidPath(this.workspaceRoot, this.branch);
1765
- try {
1766
- return Number.parseInt(readFileSync2(pidPath, "utf8").trim(), 10) || 0;
1767
- } catch {
1768
- return 0;
1769
- }
1770
- }
1771
- async stop(startIfMissing = true) {
1772
- const socketPath = engineSocketPath(this.workspaceRoot, this.branch);
1773
- if (!this.isRunning()) {
1774
- try {
1775
- await this.start(startIfMissing);
1776
- } catch {
1777
- if (!startIfMissing)
1778
- return;
1779
- throw new Error("Unable to start Kibi engine for stop request");
1780
- }
1781
- }
1782
- if (!this.isRunning()) {
1783
- if (existsSync3(socketPath)) {
1784
- throw new Error(`Kibi engine socket remains present but is not reachable: ${socketPath}`);
1785
- }
1786
- return;
1787
- }
1788
- let requestError;
1789
- try {
1790
- await this.request({ method: "stop" });
1791
- } catch (error) {
1792
- requestError = error;
1793
- }
1794
- this.socket?.destroy();
1795
- this.socket = null;
1796
- const deadline = Date.now() + 5000;
1797
- while (existsSync3(socketPath) && Date.now() < deadline) {
1798
- await new Promise((resolve3) => setTimeout(resolve3, 25));
1799
- }
1800
- if (existsSync3(socketPath)) {
1801
- const detail = requestError instanceof Error ? `: ${requestError.message}` : "";
1802
- throw new Error(`Kibi engine did not stop within 5 seconds${detail}`);
1803
- }
1804
- }
1805
- async terminate() {
1806
- this.socket?.end();
1807
- this.socket?.destroy();
1808
- this.socket = null;
1809
- this.daemon = null;
1810
- this.lastResult = null;
1811
- }
1812
- }
1813
1598
  async function ensureJournaledBranchStoreAsync(branchPath) {
1814
1599
  mkdirSync2(branchPath, { recursive: true });
1815
1600
  const markerPath = path4.join(branchPath, "storage.json");
1816
- const marker = existsSync3(markerPath) ? readFileSync2(markerPath, "utf8") : "";
1601
+ const marker = existsSync4(markerPath) ? readFileSync3(markerPath, "utf8") : "";
1817
1602
  if (marker.includes("kibi.rdf-journal.v1")) {
1818
1603
  const rdfDirectory = path4.join(branchPath, "rdf");
1819
1604
  const currentPath = path4.join(branchPath, "CURRENT");
1820
1605
  const sentinelPath = path4.join(branchPath, "kb.rdf");
1821
- if (!existsSync3(rdfDirectory) || !existsSync3(currentPath)) {
1606
+ if (!existsSync4(rdfDirectory) || !existsSync4(currentPath)) {
1822
1607
  recoverInterruptedGeneration(branchPath);
1823
- if (existsSync3(rdfDirectory) && existsSync3(currentPath)) {
1608
+ if (existsSync4(rdfDirectory) && existsSync4(currentPath)) {
1824
1609
  recoverStaleJournalLock(branchPath);
1825
1610
  archiveLegacyFiles(branchPath);
1826
- if (!existsSync3(sentinelPath)) {
1611
+ if (!existsSync4(sentinelPath)) {
1827
1612
  writeFileSync2(sentinelPath, `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
1828
1613
  `, { mode: 384 });
1829
1614
  }
1830
1615
  return;
1831
1616
  }
1832
- const legacyRdf2 = path4.join(branchPath, "kb.rdf");
1833
- const legacyAudit2 = path4.join(branchPath, "audit.log");
1834
- const legacyContents = existsSync3(legacyRdf2) ? readFileSync2(legacyRdf2, "utf8") : "";
1835
- if (existsSync3(legacyAudit2) || existsSync3(legacyRdf2) && !legacyContents.includes("kibi.rdf-journal.v1")) {
1836
- rmSync(rdfDirectory, { recursive: true, force: true });
1837
- rmSync(currentPath, { force: true });
1838
- rmSync(markerPath, { force: true });
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")) {
1621
+ rmSync2(rdfDirectory, { recursive: true, force: true });
1622
+ rmSync2(currentPath, { force: true });
1623
+ rmSync2(markerPath, { force: true });
1839
1624
  } else {
1840
1625
  throw new Error(`Journaled branch store is incomplete at ${branchPath}; restore the immutable legacy/ backup or remove the interrupted generation before retrying`);
1841
1626
  }
1842
1627
  } else {
1843
1628
  recoverStaleJournalLock(branchPath);
1844
1629
  archiveLegacyFiles(branchPath);
1845
- if (!existsSync3(sentinelPath)) {
1630
+ if (!existsSync4(sentinelPath)) {
1846
1631
  writeFileSync2(sentinelPath, `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
1847
1632
  `, { mode: 384 });
1848
1633
  }
@@ -1851,7 +1636,7 @@ async function ensureJournaledBranchStoreAsync(branchPath) {
1851
1636
  }
1852
1637
  const legacyRdf = path4.join(branchPath, "kb.rdf");
1853
1638
  const legacyAudit = path4.join(branchPath, "audit.log");
1854
- if (!existsSync3(legacyRdf) && !existsSync3(legacyAudit)) {
1639
+ if (!existsSync4(legacyRdf) && !existsSync4(legacyAudit)) {
1855
1640
  mkdirSync2(path4.join(branchPath, "rdf"), { recursive: true });
1856
1641
  writeFileSync2(markerPath, `{"format":"kibi.rdf-journal.v1","schemaVersion":1}
1857
1642
  `, { mode: 384 });
@@ -1863,8 +1648,8 @@ async function ensureJournaledBranchStoreAsync(branchPath) {
1863
1648
  `, { mode: 384 });
1864
1649
  return;
1865
1650
  }
1866
- if (existsSync3(legacyRdf)) {
1867
- const legacyContents = readFileSync2(legacyRdf, "utf8");
1651
+ if (existsSync4(legacyRdf)) {
1652
+ const legacyContents = readFileSync3(legacyRdf, "utf8");
1868
1653
  if (!/<rdf:RDF(?:\s|>)/.test(legacyContents)) {
1869
1654
  throw new Error(`Legacy Kibi store is corrupt at ${legacyRdf}: expected RDF/XML root`);
1870
1655
  }
@@ -1893,40 +1678,40 @@ async function ensureJournaledBranchStoreAsync(branchPath) {
1893
1678
  throw new Error(migrated.error ?? "Legacy KB migration failed");
1894
1679
  fsyncJournaledBranchStore(staging);
1895
1680
  } catch (error) {
1896
- rmSync(staging, { recursive: true, force: true });
1681
+ rmSync2(staging, { recursive: true, force: true });
1897
1682
  throw error;
1898
1683
  } finally {
1899
1684
  await migrator.terminate();
1900
1685
  }
1901
1686
  for (const file of ["rdf", "storage.json", "CURRENT"]) {
1902
1687
  const source = path4.join(staging, file);
1903
- if (!existsSync3(source))
1688
+ if (!existsSync4(source))
1904
1689
  continue;
1905
1690
  const target = path4.join(branchPath, file);
1906
- if (existsSync3(target))
1907
- rmSync(target, { recursive: true, force: true });
1691
+ if (existsSync4(target))
1692
+ rmSync2(target, { recursive: true, force: true });
1908
1693
  renameSync(source, target);
1909
1694
  }
1910
1695
  archiveLegacyFiles(branchPath);
1911
1696
  writeFileSync2(path4.join(branchPath, "kb.rdf"), `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
1912
1697
  `, { mode: 384 });
1913
1698
  fsyncJournaledBranchStore(branchPath);
1914
- rmSync(staging, { recursive: true, force: true });
1699
+ rmSync2(staging, { recursive: true, force: true });
1915
1700
  }
1916
1701
  function archiveLegacyFiles(branchPath) {
1917
1702
  const legacyDir = path4.join(branchPath, "legacy");
1918
1703
  mkdirSync2(legacyDir, { recursive: true, mode: 448 });
1919
1704
  for (const file of ["kb.rdf", "audit.log"]) {
1920
1705
  const source = path4.join(branchPath, file);
1921
- if (!existsSync3(source))
1706
+ if (!existsSync4(source))
1922
1707
  continue;
1923
1708
  if (file === "kb.rdf") {
1924
- const contents = readFileSync2(source, "utf8");
1709
+ const contents = readFileSync3(source, "utf8");
1925
1710
  if (contents.includes("kibi.rdf-journal.v1"))
1926
1711
  continue;
1927
1712
  }
1928
1713
  const destination = path4.join(legacyDir, file);
1929
- if (existsSync3(destination)) {
1714
+ if (existsSync4(destination)) {
1930
1715
  renameSync(source, path4.join(legacyDir, `${file}.recovery.${Date.now()}`));
1931
1716
  } else {
1932
1717
  renameSync(source, destination);
@@ -1949,23 +1734,23 @@ function recoverInterruptedGeneration(branchPath) {
1949
1734
  const oldCurrent = newest("CURRENT.old.");
1950
1735
  const rdfPath = path4.join(branchPath, "rdf");
1951
1736
  const currentPath = path4.join(branchPath, "CURRENT");
1952
- if ((!existsSync3(rdfPath) || !existsSync3(currentPath)) && oldRdf && oldCurrent) {
1737
+ if ((!existsSync4(rdfPath) || !existsSync4(currentPath)) && oldRdf && oldCurrent) {
1953
1738
  try {
1954
- if (existsSync3(rdfPath))
1955
- rmSync(rdfPath, { recursive: true, force: true });
1956
- if (existsSync3(currentPath))
1957
- rmSync(currentPath, { force: true });
1739
+ if (existsSync4(rdfPath))
1740
+ rmSync2(rdfPath, { recursive: true, force: true });
1741
+ if (existsSync4(currentPath))
1742
+ rmSync2(currentPath, { force: true });
1958
1743
  renameSync(oldRdf, rdfPath);
1959
1744
  renameSync(oldCurrent, currentPath);
1960
1745
  return;
1961
1746
  } catch {}
1962
1747
  }
1963
- if (!existsSync3(rdfPath) && oldRdf) {
1748
+ if (!existsSync4(rdfPath) && oldRdf) {
1964
1749
  try {
1965
1750
  renameSync(oldRdf, rdfPath);
1966
1751
  } catch {}
1967
1752
  }
1968
- if (!existsSync3(currentPath) && oldCurrent) {
1753
+ if (!existsSync4(currentPath) && oldCurrent) {
1969
1754
  try {
1970
1755
  renameSync(oldCurrent, currentPath);
1971
1756
  } catch {}
@@ -1980,26 +1765,63 @@ async function runEngineDaemon(options) {
1980
1765
  const prolog = new PrologProcess({ timeout: 120000, oneShot: false });
1981
1766
  await prolog.start();
1982
1767
  const attached = await prolog.query(`kb_attach('${quoteProlog(branchPath)}')`);
1983
- if (!attached.success)
1984
- throw new Error(attached.error ?? "Failed to attach branch KB");
1985
- const statusModulePath = path4.join(path4.dirname(resolveKbPlPath()), "status.pl");
1986
- const statusLoaded = await prolog.query(`use_module('${quoteProlog(statusModulePath.replaceAll("\\", "/"))}')`);
1987
- if (!statusLoaded.success) {
1988
- throw new Error(statusLoaded.error ?? "Failed to load Kibi status module");
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
+ }
1989
1784
  }
1990
1785
  mkdirSync2(path4.dirname(options.socketPath), { recursive: true, mode: 448 });
1991
- if (existsSync3(options.socketPath)) {
1786
+ if (existsSync4(options.socketPath)) {
1787
+ const isLivePeerSignal = (error) => {
1788
+ if (error && typeof error === "object") {
1789
+ const code = "code" in error ? String(error.code) : "";
1790
+ if (code === "EPIPE" || code === "ECONNRESET")
1791
+ return true;
1792
+ const errno = "errno" in error ? Number(error.errno) : Number.NaN;
1793
+ if (errno === -32)
1794
+ return true;
1795
+ }
1796
+ const message = error instanceof Error ? error.message : String(error);
1797
+ return /\bEPIPE\b|\bECONNRESET\b|broken pipe/i.test(message);
1798
+ };
1992
1799
  let live = false;
1993
1800
  try {
1994
1801
  const existing = await connectSocket(options.socketPath, 100);
1995
- existing.destroy();
1802
+ existing.on("error", () => {
1803
+ return;
1804
+ });
1996
1805
  live = true;
1997
- } catch {}
1806
+ try {
1807
+ existing.destroy();
1808
+ } catch {}
1809
+ existing.unref?.();
1810
+ } catch (error) {
1811
+ if (isLivePeerSignal(error)) {
1812
+ live = true;
1813
+ }
1814
+ }
1998
1815
  if (live) {
1816
+ try {
1817
+ await prolog.terminate();
1818
+ } catch (error) {
1819
+ if (!isLivePeerSignal(error)) {}
1820
+ }
1999
1821
  throw new Error(`A Kibi engine is already listening at ${options.socketPath}`);
2000
1822
  }
2001
1823
  try {
2002
- rmSync(options.socketPath, { force: true });
1824
+ rmSync2(options.socketPath, { force: true });
2003
1825
  } catch {}
2004
1826
  }
2005
1827
  writeFileSync2(enginePidPath(options.workspaceRoot, options.branch), `${process.pid}
@@ -2011,7 +1833,6 @@ async function runEngineDaemon(options) {
2011
1833
  let idleCompactionQueued = false;
2012
1834
  const queryCache = new Map;
2013
1835
  let freshnessCache = null;
2014
- const cancelledRequests = new Set;
2015
1836
  const clients = new Set;
2016
1837
  let shuttingDown = false;
2017
1838
  const exactBranchStatus = (result) => {
@@ -2021,6 +1842,12 @@ async function runEngineDaemon(options) {
2021
1842
  try {
2022
1843
  const decoded = JSON.parse(JSON.parse(result.bindings.JsonString));
2023
1844
  decoded.branch = options.branch;
1845
+ if (attachedIdentity !== null) {
1846
+ decoded.attachedPath = attachedIdentity.path;
1847
+ decoded.attachedGeneration = attachedIdentity.generation;
1848
+ decoded.attachedDev = attachedIdentity.dev;
1849
+ decoded.attachedIno = attachedIdentity.ino;
1850
+ }
2024
1851
  return {
2025
1852
  ...result,
2026
1853
  bindings: {
@@ -2032,6 +1859,21 @@ async function runEngineDaemon(options) {
2032
1859
  return result;
2033
1860
  }
2034
1861
  };
1862
+ let workspaceMissingStreak = 0;
1863
+ const workspaceWatchdog = setInterval(() => {
1864
+ if (shuttingDown)
1865
+ return;
1866
+ if (!existsSync4(options.workspaceRoot)) {
1867
+ workspaceMissingStreak += 1;
1868
+ if (workspaceMissingStreak < 2)
1869
+ return;
1870
+ console.error(`[KIBI] workspace ${options.workspaceRoot} no longer exists; stopping engine`);
1871
+ shutdown();
1872
+ return;
1873
+ }
1874
+ workspaceMissingStreak = 0;
1875
+ }, engineWorkspaceWatchdogMs());
1876
+ workspaceWatchdog.unref();
2035
1877
  const scheduleIdleExit = () => {
2036
1878
  if (activeClients > 0)
2037
1879
  return;
@@ -2054,7 +1896,7 @@ async function runEngineDaemon(options) {
2054
1896
  shutdown();
2055
1897
  }, engineIdleTimeoutMs());
2056
1898
  };
2057
- const handle = async (request) => {
1899
+ const handle = async (request, cancelledRequests) => {
2058
1900
  if (request.protocolVersion !== ENGINE_PROTOCOL_VERSION) {
2059
1901
  throw new Error(`Kibi engine protocol mismatch: client=${request.protocolVersion ?? "missing"}, server=${ENGINE_PROTOCOL_VERSION}`);
2060
1902
  }
@@ -2276,6 +2118,7 @@ async function runEngineDaemon(options) {
2276
2118
  process.off("SIGINT", requestSignalShutdown);
2277
2119
  if (idleTimer)
2278
2120
  clearTimeout(idleTimer);
2121
+ clearInterval(workspaceWatchdog);
2279
2122
  const saved = await prolog.query("kb_save").catch(() => null);
2280
2123
  if (saved?.success) {
2281
2124
  try {
@@ -2290,23 +2133,22 @@ async function runEngineDaemon(options) {
2290
2133
  server.close();
2291
2134
  server.unref();
2292
2135
  try {
2293
- if (existsSync3(options.socketPath))
2294
- rmSync(options.socketPath, { force: true });
2295
- if (existsSync3(enginePidPath(options.workspaceRoot, options.branch)))
2296
- rmSync(enginePidPath(options.workspaceRoot, options.branch), {
2136
+ if (existsSync4(options.socketPath))
2137
+ rmSync2(options.socketPath, { force: true });
2138
+ if (existsSync4(enginePidPath(options.workspaceRoot, options.branch)))
2139
+ rmSync2(enginePidPath(options.workspaceRoot, options.branch), {
2297
2140
  force: true
2298
2141
  });
2299
2142
  } catch {}
2300
2143
  setTimeout(() => process.exit(0), 50).unref();
2301
2144
  };
2302
- const requestSignalShutdown = () => {
2303
- shutdown();
2304
- };
2145
+ const requestSignalShutdown = requestEngineSignalShutdown(shutdown);
2305
2146
  process.once("SIGTERM", requestSignalShutdown);
2306
2147
  process.once("SIGINT", requestSignalShutdown);
2307
2148
  server.on("connection", (socket) => {
2308
2149
  clients.add(socket);
2309
2150
  activeClients += 1;
2151
+ const cancelledRequests = new Set;
2310
2152
  let buffer = Buffer.alloc(0);
2311
2153
  socket.on("data", (chunk) => {
2312
2154
  try {
@@ -2327,7 +2169,7 @@ async function runEngineDaemon(options) {
2327
2169
  return;
2328
2170
  }
2329
2171
  try {
2330
- const result = await handle(request);
2172
+ const result = await handle(request, cancelledRequests);
2331
2173
  writeSocketFrame(socket, {
2332
2174
  id: request.id,
2333
2175
  ok: true,
@@ -2341,6 +2183,8 @@ async function runEngineDaemon(options) {
2341
2183
  error: error instanceof Error ? error.message : String(error),
2342
2184
  serverPackageVersions: ENGINE_PACKAGE_VERSIONS
2343
2185
  });
2186
+ } finally {
2187
+ cancelledRequests.delete(request.id);
2344
2188
  }
2345
2189
  }).catch(() => {
2346
2190
  return;
@@ -2356,9 +2200,9 @@ async function runEngineDaemon(options) {
2356
2200
  scheduleIdleExit();
2357
2201
  });
2358
2202
  });
2359
- await new Promise((resolve3, reject) => {
2203
+ await new Promise((resolve, reject) => {
2360
2204
  server.once("error", reject);
2361
- server.listen(options.socketPath, () => resolve3());
2205
+ server.listen(options.socketPath, () => resolve());
2362
2206
  });
2363
2207
  if (process.platform !== "win32") {
2364
2208
  try {
@@ -2366,12 +2210,25 @@ async function runEngineDaemon(options) {
2366
2210
  } catch {}
2367
2211
  }
2368
2212
  scheduleIdleExit();
2369
- await new Promise((resolve3) => {
2370
- server.once("close", resolve3);
2213
+ await new Promise((resolve) => {
2214
+ server.once("close", resolve);
2371
2215
  });
2372
2216
  }
2373
2217
 
2374
2218
  // ../cli/src/engine-daemon.ts
2219
+ function isEngineDaemonEntrypoint(argv = process.argv, moduleUrl = import.meta.url, isMain = __require.main == __require.module) {
2220
+ if (isMain === true) {
2221
+ return moduleUrl === import.meta.url;
2222
+ }
2223
+ const entry = argv[1];
2224
+ if (typeof entry !== "string" || entry.length === 0)
2225
+ return false;
2226
+ try {
2227
+ return moduleUrl === pathToFileURL(path5.resolve(entry)).href;
2228
+ } catch {
2229
+ return false;
2230
+ }
2231
+ }
2375
2232
  function requiredArg(args, name) {
2376
2233
  const index = args.indexOf(name);
2377
2234
  const value = index >= 0 ? args[index + 1] : undefined;
@@ -2380,23 +2237,35 @@ function requiredArg(args, name) {
2380
2237
  }
2381
2238
  return value;
2382
2239
  }
2383
- var args = process.argv.slice(2);
2384
- try {
2385
- await runEngineDaemon({
2386
- workspaceRoot: requiredArg(args, "--workspace"),
2387
- branch: requiredArg(args, "--branch"),
2388
- socketPath: requiredArg(args, "--socket")
2389
- });
2390
- } catch (error) {
2391
- const message = error instanceof Error ? error.message : String(error);
2392
- console.error(message);
2393
- const socketIndex = args.indexOf("--socket");
2394
- const socketPath = socketIndex >= 0 ? args[socketIndex + 1] : undefined;
2395
- if (socketPath) {
2396
- try {
2397
- writeFileSync3(`${socketPath}.error`, `${message}
2240
+ async function runEngineDaemonCli(args = process.argv.slice(2)) {
2241
+ try {
2242
+ await runEngineDaemon({
2243
+ workspaceRoot: requiredArg(args, "--workspace"),
2244
+ branch: requiredArg(args, "--branch"),
2245
+ socketPath: requiredArg(args, "--socket")
2246
+ });
2247
+ } catch (error) {
2248
+ const message = error instanceof Error ? error.message : String(error);
2249
+ console.error(message);
2250
+ const socketIndex = args.indexOf("--socket");
2251
+ const socketPath = socketIndex >= 0 ? args[socketIndex + 1] : undefined;
2252
+ if (socketPath) {
2253
+ try {
2254
+ writeFileSync3(`${socketPath}.error`, `${message}
2398
2255
  `, { mode: 384 });
2399
- } catch {}
2256
+ } catch {}
2257
+ }
2258
+ process.exitCode = 1;
2400
2259
  }
2401
- process.exitCode = 1;
2402
2260
  }
2261
+ async function runEngineDaemonIfEntrypoint(isEntrypoint = isEngineDaemonEntrypoint()) {
2262
+ if (!isEntrypoint)
2263
+ return;
2264
+ await runEngineDaemonCli();
2265
+ }
2266
+ await runEngineDaemonIfEntrypoint();
2267
+ export {
2268
+ isEngineDaemonEntrypoint,
2269
+ runEngineDaemonCli,
2270
+ runEngineDaemonIfEntrypoint
2271
+ };