kibi-runtime 1.0.1 → 2.0.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.
@@ -1,7 +1,11 @@
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
11
  import { spawn as spawn2 } from "node:child_process";
@@ -9,15 +13,15 @@ import { createHash as createHash2 } from "node:crypto";
9
13
  import {
10
14
  chmodSync,
11
15
  closeSync,
12
- existsSync as existsSync3,
16
+ existsSync as existsSync4,
13
17
  fsyncSync,
14
18
  mkdirSync as mkdirSync2,
15
19
  openSync,
16
- readFileSync as readFileSync2,
20
+ readFileSync as readFileSync3,
17
21
  readdirSync as readdirSync2,
18
22
  realpathSync,
19
23
  renameSync,
20
- rmSync,
24
+ rmSync as rmSync2,
21
25
  statSync as statSync2,
22
26
  unlinkSync,
23
27
  writeFileSync as writeFileSync2
@@ -30,7 +34,7 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
30
34
  // ../cli/src/prolog.ts
31
35
  import { spawn } from "node:child_process";
32
36
  import { existsSync } from "node:fs";
33
- import { createRequire } from "node:module";
37
+ import { createRequire as createRequire2 } from "node:module";
34
38
  import path from "node:path";
35
39
  import { fileURLToPath } from "node:url";
36
40
 
@@ -46,12 +50,646 @@ function isPrologDebugEnabled() {
46
50
  return Boolean(getEnvValue("KIBI_PROLOG_DEBUG"));
47
51
  }
48
52
 
53
+ // ../cli/src/prolog/codec.ts
54
+ function escapeAtom(value) {
55
+ return value.replaceAll("\\", "\\\\").replaceAll("\x07", "\\a").replaceAll("\b", "\\b").replaceAll("\f", "\\f").replaceAll(`
56
+ `, "\\n").replaceAll("\r", "\\r").replaceAll("\t", "\\t").replaceAll("\v", "\\v").replaceAll("'", "''");
57
+ }
58
+ function parseListOfLists(listStr) {
59
+ return parseListRows(listStr).flatMap((row) => {
60
+ const scanned = scanTopLevelGeneral(row, ",");
61
+ if (!scanned.balanced)
62
+ return [];
63
+ const columns = scanned.parts.map((column) => column.trim());
64
+ return columns.length === 0 || columns.length === 1 && columns[0] === "" ? [] : [columns];
65
+ });
66
+ }
67
+ function firstTwoDefinedParts(parts) {
68
+ const first = parts[0];
69
+ const second = parts[1];
70
+ if (first === undefined || second === undefined) {
71
+ return;
72
+ }
73
+ return [first, second];
74
+ }
75
+ function typedLiteralFromParts(parts, original) {
76
+ const pair = firstTwoDefinedParts(parts);
77
+ if (!pair) {
78
+ return original;
79
+ }
80
+ const [literalPart, datatypePart] = pair;
81
+ let literalValue = literalPart.trim();
82
+ const datatype = datatypePart.trim();
83
+ const decodedLiteral = decodePrologQuotedLiteral(literalValue, '"');
84
+ if (decodedLiteral !== undefined) {
85
+ literalValue = decodedLiteral;
86
+ } else if (literalValue.startsWith('"') && literalValue.endsWith('"')) {
87
+ literalValue = literalValue.substring(1, literalValue.length - 1);
88
+ }
89
+ if (datatype.includes("#integer")) {
90
+ return Number.parseInt(literalValue, 10);
91
+ }
92
+ if (datatype.includes("#decimal") || datatype.includes("#double")) {
93
+ return Number.parseFloat(literalValue);
94
+ }
95
+ if (datatype.includes("#boolean")) {
96
+ return literalValue === "true";
97
+ }
98
+ if (literalValue.startsWith("[") && literalValue.endsWith("]")) {
99
+ const listContent = literalValue.substring(1, literalValue.length - 1);
100
+ if (listContent === "") {
101
+ return [];
102
+ }
103
+ return splitTopLevelGeneral(listContent, ",").map((item) => item.trim());
104
+ }
105
+ return literalValue;
106
+ }
107
+ function fileUriLeaf(value) {
108
+ const lastSlash = value.lastIndexOf("/");
109
+ if (lastSlash !== -1) {
110
+ return value.substring(lastSlash + 1);
111
+ }
112
+ return value;
113
+ }
114
+ function parseEntityFromList(data) {
115
+ if (data.length < 3) {
116
+ return {};
117
+ }
118
+ const [idPart, typePart, propsPart] = data;
119
+ if (idPart === undefined || typePart === undefined || propsPart === undefined) {
120
+ return {};
121
+ }
122
+ const id = idPart.trim();
123
+ const type = typePart.trim();
124
+ const propsStr = propsPart.trim();
125
+ const props = parsePropertyList(propsStr);
126
+ return { ...props, id: normalizeEntityId(stripOuterQuotes(id)), type };
127
+ }
128
+ function parsePropertyList(propsStr) {
129
+ const props = {};
130
+ let cleaned = propsStr.trim();
131
+ if (cleaned.startsWith("[")) {
132
+ cleaned = cleaned.substring(1);
133
+ }
134
+ if (cleaned.endsWith("]")) {
135
+ cleaned = cleaned.substring(0, cleaned.length - 1);
136
+ }
137
+ const pairs = splitTopLevelGeneral(cleaned, ",");
138
+ for (const pair of pairs) {
139
+ const eqIndex = pair.indexOf("=");
140
+ if (eqIndex === -1)
141
+ continue;
142
+ const key = pair.substring(0, eqIndex).trim();
143
+ const value = pair.substring(eqIndex + 1).trim();
144
+ if (key === "..." || value === "..." || value === "...|...") {
145
+ continue;
146
+ }
147
+ let parsed = parsePrologValue(value);
148
+ if ((key === "rule_ir" || key === "semantic_inventory" || key === "proof_receipts" || key === "proof_contract" || key === "proof_bindings") && typeof parsed === "string") {
149
+ try {
150
+ parsed = JSON.parse(parsed);
151
+ } catch {}
152
+ }
153
+ if (!Object.hasOwn(props, key)) {
154
+ props[key] = parsed;
155
+ continue;
156
+ }
157
+ const existing = props[key];
158
+ props[key] = Array.isArray(existing) ? [...existing, parsed] : [existing, parsed];
159
+ }
160
+ return props;
161
+ }
162
+ function parsePrologValue(valueInput) {
163
+ const value = valueInput.trim();
164
+ if (value.startsWith("^^(")) {
165
+ const innerStart = value.indexOf("(") + 1;
166
+ let depth = 1;
167
+ let innerEnd = innerStart;
168
+ for (let i = innerStart;i < value.length; i++) {
169
+ if (value[i] === "(")
170
+ depth++;
171
+ if (value[i] === ")") {
172
+ depth--;
173
+ if (depth === 0) {
174
+ innerEnd = i;
175
+ break;
176
+ }
177
+ }
178
+ }
179
+ const innerContent = value.substring(innerStart, innerEnd);
180
+ const parts = splitTopLevelGeneral(innerContent, ",");
181
+ if (parts.length >= 2) {
182
+ return typedLiteralFromParts(parts, value);
183
+ }
184
+ }
185
+ if (value.startsWith("file:///")) {
186
+ return fileUriLeaf(value);
187
+ }
188
+ if (value.startsWith('"') && value.endsWith('"')) {
189
+ return decodePrologQuotedLiteral(value, '"') ?? value.substring(1, value.length - 1);
190
+ }
191
+ if (value.startsWith("'") && value.endsWith("'")) {
192
+ return decodePrologQuotedLiteral(value, "'") ?? value.substring(1, value.length - 1);
193
+ }
194
+ if (value.startsWith("[") && value.endsWith("]")) {
195
+ const listContent = value.substring(1, value.length - 1);
196
+ if (listContent === "") {
197
+ return [];
198
+ }
199
+ const items = splitTopLevelGeneral(listContent, ",").map((item) => {
200
+ return parsePrologValue(item.trim());
201
+ });
202
+ return items;
203
+ }
204
+ return value;
205
+ }
206
+ function splitTopLevelGeneral(str, delimiter) {
207
+ return scanTopLevelGeneral(str, delimiter).parts;
208
+ }
209
+ function scanTopLevelGeneral(str, delimiter) {
210
+ const results = [];
211
+ let current = "";
212
+ let quote = null;
213
+ const delimiters = [];
214
+ let balanced = true;
215
+ for (let i = 0;i < str.length; i++) {
216
+ const char = str[i];
217
+ if (quote !== null) {
218
+ current += char;
219
+ if (char === "\\") {
220
+ const escaped = str[i + 1];
221
+ if (escaped !== undefined) {
222
+ current += escaped;
223
+ i++;
224
+ }
225
+ } else if (char === quote) {
226
+ const doubledPrologQuote = quote === "'" && str[i + 1] === "'";
227
+ if (doubledPrologQuote) {
228
+ current += str[i + 1] ?? "";
229
+ i++;
230
+ } else {
231
+ quote = null;
232
+ }
233
+ }
234
+ continue;
235
+ }
236
+ if (char === "'" || char === '"') {
237
+ quote = char;
238
+ current += char;
239
+ continue;
240
+ }
241
+ if (char === "[" || char === "(" || char === "{") {
242
+ delimiters.push(char);
243
+ current += char;
244
+ continue;
245
+ }
246
+ if (char === "]" || char === ")" || char === "}") {
247
+ const expectedOpening = char === "]" ? "[" : char === ")" ? "(" : "{";
248
+ if (delimiters.at(-1) !== expectedOpening) {
249
+ balanced = false;
250
+ } else {
251
+ delimiters.pop();
252
+ }
253
+ current += char;
254
+ continue;
255
+ }
256
+ if (delimiters.length === 0 && delimiter.length > 0 && str.startsWith(delimiter, i)) {
257
+ if (current) {
258
+ results.push(current);
259
+ current = "";
260
+ }
261
+ i += delimiter.length - 1;
262
+ } else {
263
+ current += char;
264
+ }
265
+ }
266
+ if (current) {
267
+ results.push(current);
268
+ }
269
+ return {
270
+ parts: results,
271
+ balanced: balanced && quote === null && delimiters.length === 0
272
+ };
273
+ }
274
+ function stripOuterQuotes(value) {
275
+ if (value.startsWith("'") && value.endsWith("'")) {
276
+ return decodePrologQuotedLiteral(value, "'") ?? value.slice(1, -1);
277
+ }
278
+ if (value.startsWith('"') && value.endsWith('"')) {
279
+ return decodePrologQuotedLiteral(value, '"') ?? value.slice(1, -1);
280
+ }
281
+ return value;
282
+ }
283
+ function normalizeEntityId(value) {
284
+ if (value.startsWith("kb:entity/")) {
285
+ return value.slice("kb:entity/".length);
286
+ }
287
+ if (!value.startsWith("file:///")) {
288
+ return value;
289
+ }
290
+ const idx = value.lastIndexOf("/");
291
+ return idx === -1 ? value : value.slice(idx + 1);
292
+ }
293
+ function parseListRows(raw) {
294
+ const trimmed = raw.trim();
295
+ if (trimmed === "[]" || trimmed.length === 0) {
296
+ return [];
297
+ }
298
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
299
+ return [];
300
+ }
301
+ const content = unwrapList(trimmed);
302
+ if (content.length === 0) {
303
+ return [];
304
+ }
305
+ const scanned = scanTopLevelGeneral(content, ",");
306
+ if (!scanned.balanced) {
307
+ return [];
308
+ }
309
+ const rows = [];
310
+ for (const term of scanned.parts) {
311
+ const row = term.trim();
312
+ if (row.length === 0)
313
+ continue;
314
+ if (!row.startsWith("[") || !row.endsWith("]")) {
315
+ return [];
316
+ }
317
+ rows.push(row.slice(1, -1).trim());
318
+ }
319
+ return rows;
320
+ }
321
+ function unwrapList(value) {
322
+ if (value.startsWith("[") && value.endsWith("]")) {
323
+ return value.slice(1, -1).trim();
324
+ }
325
+ return value;
326
+ }
327
+ function decodePrologQuotedLiteral(value, quote) {
328
+ if (!value.startsWith(quote) || !value.endsWith(quote) || value.length < 2) {
329
+ return;
330
+ }
331
+ const content = value.slice(1, -1);
332
+ let decoded = "";
333
+ for (let index = 0;index < content.length; index++) {
334
+ const char = content[index];
335
+ if (char === undefined)
336
+ continue;
337
+ if (quote === "'" && char === "'") {
338
+ if (content[index + 1] !== "'")
339
+ return;
340
+ decoded += "'";
341
+ index++;
342
+ continue;
343
+ }
344
+ if (char !== "\\") {
345
+ decoded += char;
346
+ continue;
347
+ }
348
+ const escaped = content[index + 1];
349
+ if (escaped === undefined)
350
+ return;
351
+ index++;
352
+ switch (escaped) {
353
+ case "a":
354
+ decoded += "\x07";
355
+ break;
356
+ case "b":
357
+ decoded += "\b";
358
+ break;
359
+ case "e":
360
+ decoded += "\x1B";
361
+ break;
362
+ case "f":
363
+ decoded += "\f";
364
+ break;
365
+ case "n":
366
+ decoded += `
367
+ `;
368
+ break;
369
+ case "r":
370
+ decoded += "\r";
371
+ break;
372
+ case "t":
373
+ decoded += "\t";
374
+ break;
375
+ case "v":
376
+ decoded += "\v";
377
+ break;
378
+ case "\\":
379
+ decoded += "\\";
380
+ break;
381
+ case "'":
382
+ decoded += "'";
383
+ break;
384
+ case '"':
385
+ decoded += '"';
386
+ break;
387
+ case "u": {
388
+ const code = content.slice(index + 1, index + 5);
389
+ if (!/^[0-9A-Fa-f]{4}$/.test(code))
390
+ return;
391
+ decoded += String.fromCodePoint(Number.parseInt(code, 16));
392
+ index += 4;
393
+ break;
394
+ }
395
+ case "U": {
396
+ const code = content.slice(index + 1, index + 9);
397
+ if (!/^[0-9A-Fa-f]{8}$/.test(code))
398
+ return;
399
+ const codePoint = Number.parseInt(code, 16);
400
+ if (codePoint > 1114111)
401
+ return;
402
+ decoded += String.fromCodePoint(codePoint);
403
+ index += 8;
404
+ break;
405
+ }
406
+ case "x": {
407
+ let end = index + 1;
408
+ while (/[0-9A-Fa-f]/.test(content[end] ?? ""))
409
+ end++;
410
+ if (end === index + 1 || content[end] !== "\\")
411
+ return;
412
+ const codePoint = Number.parseInt(content.slice(index + 1, end), 16);
413
+ if (codePoint > 1114111)
414
+ return;
415
+ decoded += String.fromCodePoint(codePoint);
416
+ index = end;
417
+ break;
418
+ }
419
+ default:
420
+ return;
421
+ }
422
+ }
423
+ return decoded;
424
+ }
425
+
426
+ // ../cli/src/prolog/error-terms.ts
427
+ function extractPrologErrorRecord(text) {
428
+ const match = text.match(/^__KIBI_ERROR__:(.+)$/m);
429
+ const term = match?.[1];
430
+ if (term === undefined)
431
+ return null;
432
+ return parsePrologErrorTerm(term);
433
+ }
434
+ function parsePrologErrorTerm(termText) {
435
+ const trimmed = termText.trim();
436
+ const formalMatch = trimmed.match(/^error\((.*)\)$/s);
437
+ const formalAndContext = formalMatch?.[1];
438
+ if (formalAndContext === undefined)
439
+ return null;
440
+ const [formalRaw, contextRaw] = splitTopLevelGeneral(formalAndContext, ",");
441
+ if (!formalRaw)
442
+ return null;
443
+ const context = contextRaw ? parseContextTerm(contextRaw) : null;
444
+ const parsed = parseFormalTerm(formalRaw);
445
+ if (!parsed)
446
+ return null;
447
+ switch (parsed.kind) {
448
+ case "invalid_relationship": {
449
+ const formatted = contextRaw ? parseInvalidRelationshipContext(contextRaw) : null;
450
+ const relationship = formatted !== null ? substituteRelationshipArgs(formatted.formatTemplate, formatted.formatArgs) : null;
451
+ if (relationship === null) {
452
+ return {
453
+ code: "invalid_relationship",
454
+ message: "Invalid relationship type or direction"
455
+ };
456
+ }
457
+ return {
458
+ code: "invalid_relationship",
459
+ relationship,
460
+ message: `Invalid relationship: ${relationship.relType} from ${relationship.fromType} to ${relationship.toType}`
461
+ };
462
+ }
463
+ case "stale_snapshot":
464
+ return {
465
+ code: "stale_snapshot",
466
+ message: "KB snapshot is stale; reattach or refresh the runtime before retrying (stale_snapshot)"
467
+ };
468
+ case "audit_locked":
469
+ return {
470
+ code: "audit_locked",
471
+ message: "Audit journal is locked by another Kibi runtime; restart the stale MCP/CLI session before retrying"
472
+ };
473
+ case "store_locked": {
474
+ const detail = parseStoreLockedContext(contextRaw);
475
+ const holder = detail.owner;
476
+ const holderSummary = holder === null ? "an unidentified holder" : `pid ${holder.pid ?? "?"}${holder.workspaceRoot !== undefined ? ` for ${holder.workspaceRoot}` : ""}${holder.startedAt !== undefined ? ` since ${holder.startedAt}` : ""}`;
477
+ return {
478
+ code: "permission_denied",
479
+ storeLocked: detail,
480
+ 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)`
481
+ };
482
+ }
483
+ case "permission_denied":
484
+ return {
485
+ code: "permission_denied",
486
+ message: "Access denied or KB locked"
487
+ };
488
+ case "entity_not_found": {
489
+ const entityId = unquoteAtom(parsed.entityId);
490
+ const role = parseEntityRole(context?.message);
491
+ const verb = role === "source" ? "Source entity" : role === "target" ? "Target entity" : "Entity";
492
+ const record = {
493
+ code: "entity_not_found",
494
+ entityId,
495
+ message: `${verb} does not exist: ${entityId}`
496
+ };
497
+ return role === undefined ? record : { ...record, role };
498
+ }
499
+ case "contradiction": {
500
+ const conflicts = parsed.pairs.map((pair) => ({
501
+ reason: pair[0],
502
+ otherId: pair[1]
503
+ }));
504
+ return {
505
+ code: "contradiction",
506
+ conflicts,
507
+ message: context?.message ?? "Contradiction detected: conflicting requirements without a supersedes relationship"
508
+ };
509
+ }
510
+ case "validation_error":
511
+ return {
512
+ code: "validation_error",
513
+ message: parsed.message
514
+ };
515
+ }
516
+ }
517
+ function parseContextTerm(contextRaw) {
518
+ const contextMatch = contextRaw.trim().match(/^context\((.*)\)$/s);
519
+ if (contextMatch?.[1] === undefined) {
520
+ const message2 = unquoteAtom(contextRaw.trim());
521
+ return message2 === "" ? null : { message: message2 };
522
+ }
523
+ const parts = splitTopLevelGeneral(contextMatch[1], ",");
524
+ if (parts.length === 0)
525
+ return null;
526
+ const predicate = parts[0]?.trim();
527
+ const message = parts[1] ? unquoteAtom(parts[1].trim()) : "";
528
+ return {
529
+ ...predicate ? { predicate } : {},
530
+ ...message ? { message } : {}
531
+ };
532
+ }
533
+ function parseStoreLockedContext(contextRaw) {
534
+ const empty = {
535
+ owner: null,
536
+ lockDirectory: ""
537
+ };
538
+ if (!contextRaw)
539
+ return empty;
540
+ const inner = contextRaw.trim().match(/^kb_store_locked\((.*)\)$/s)?.[1];
541
+ if (inner === undefined)
542
+ return empty;
543
+ const parts = splitTopLevelGeneral(inner, ",");
544
+ const ownerRaw = parts[0]?.trim();
545
+ const lockRaw = parts[1]?.trim();
546
+ const lockDirectory = lockRaw ? unquoteAtom(lockRaw) : "";
547
+ const originalRaw = parts[2]?.trim();
548
+ const originalError = originalRaw ? unquoteAtom(originalRaw) : "";
549
+ if (!ownerRaw || ownerRaw === '""' || ownerRaw === "''") {
550
+ return {
551
+ owner: null,
552
+ lockDirectory,
553
+ ...originalError !== "" ? { originalError } : {}
554
+ };
555
+ }
556
+ try {
557
+ const parsed = JSON.parse(unquoteAtom(ownerRaw));
558
+ if (parsed === null || typeof parsed !== "object") {
559
+ return {
560
+ owner: null,
561
+ lockDirectory,
562
+ ...originalError !== "" ? { originalError } : {}
563
+ };
564
+ }
565
+ return {
566
+ owner: {
567
+ ...typeof parsed.pid === "number" ? { pid: parsed.pid } : {},
568
+ ...typeof parsed.workspaceRoot === "string" ? { workspaceRoot: parsed.workspaceRoot } : {},
569
+ ...typeof parsed.bootId === "string" ? { bootId: parsed.bootId } : {},
570
+ ...typeof parsed.startedAt === "string" ? { startedAt: parsed.startedAt } : {}
571
+ },
572
+ lockDirectory,
573
+ ...originalError !== "" ? { originalError } : {}
574
+ };
575
+ } catch {
576
+ return {
577
+ owner: null,
578
+ lockDirectory,
579
+ ...originalError !== "" ? { originalError } : {}
580
+ };
581
+ }
582
+ }
583
+ function parseFormalTerm(formalRaw) {
584
+ const formal = formalRaw.trim();
585
+ const functorMatch = formal.match(/^([a-z][A-Za-z0-9_]*)\((.*)\)$/s);
586
+ if (functorMatch?.[1] === undefined || functorMatch[2] === undefined) {
587
+ return null;
588
+ }
589
+ const functor = functorMatch[1];
590
+ const args = functorMatch[2].trim() ? splitTopLevelGeneral(functorMatch[2], ",") : [];
591
+ if (functor === "permission_error") {
592
+ const operation = args[0]?.trim();
593
+ const resource = args[1]?.trim();
594
+ const detail = args[2]?.trim();
595
+ if (detail === "stale_snapshot")
596
+ return { kind: "stale_snapshot" };
597
+ if (operation === "lock" && resource === "audit_log") {
598
+ return { kind: "audit_locked" };
599
+ }
600
+ if (operation === "attach" && resource === "kb_store") {
601
+ return { kind: "store_locked" };
602
+ }
603
+ return { kind: "permission_denied" };
604
+ }
605
+ if (functor === "existence_error" && args[0]?.trim() === "entity") {
606
+ const entityId = args[1]?.trim();
607
+ if (entityId === undefined)
608
+ return null;
609
+ return { kind: "entity_not_found", entityId: unquoteAtom(entityId) };
610
+ }
611
+ if (functor === "type_error" && args[0]?.trim() === "relationship") {
612
+ return { kind: "invalid_relationship" };
613
+ }
614
+ if (functor === "kb_contradiction") {
615
+ const listText = args[0]?.trim() ?? "[]";
616
+ const pairs = parseContradictionPairs(listText);
617
+ return { kind: "contradiction", pairs };
618
+ }
619
+ if (functor === "validation_error") {
620
+ const message = args[0]?.trim();
621
+ if (message === undefined)
622
+ return null;
623
+ return { kind: "validation_error", message: unquoteAtom(message) };
624
+ }
625
+ return null;
626
+ }
627
+ function parseInvalidRelationshipContext(contextRaw) {
628
+ const contextMatch = contextRaw.trim().match(/^context\((.*)\)$/s);
629
+ const inner = contextMatch?.[1];
630
+ if (inner === undefined)
631
+ return null;
632
+ const parts = splitTopLevelGeneral(inner, ",");
633
+ const formatted = parts[1]?.trim();
634
+ if (formatted === undefined)
635
+ return null;
636
+ const formatMatch = formatted.match(/^(.*)-\[(.*)\]$/s);
637
+ const template = formatMatch?.[1];
638
+ const argsText = formatMatch?.[2];
639
+ if (template === undefined || argsText === undefined)
640
+ return null;
641
+ const formatArgs = argsText.trim() ? splitTopLevelGeneral(argsText, ",").map((arg) => unquoteAtom(arg.trim())) : [];
642
+ return { formatTemplate: unquoteAtom(template.trim()), formatArgs };
643
+ }
644
+ function substituteRelationshipArgs(template, args) {
645
+ if (template === null || args === null || args.length < 3)
646
+ return null;
647
+ const substituted = template.split("~w").map((chunk, index) => index === 0 ? chunk : `${args[index - 1] ?? ""}${chunk}`).join("");
648
+ const match = substituted.match(/^Invalid relationship:\s*(\S+)\s+from\s+(\S+)\s+to\s+(\S+)\s*$/);
649
+ if (!match?.[1] || !match[2] || !match[3])
650
+ return null;
651
+ return {
652
+ relType: match[1],
653
+ fromType: match[2],
654
+ toType: match[3]
655
+ };
656
+ }
657
+ function parseContradictionPairs(listText) {
658
+ const trimmed = listText.trim();
659
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]"))
660
+ return [];
661
+ const inner = trimmed.slice(1, -1).trim();
662
+ if (inner === "")
663
+ return [];
664
+ return splitTopLevelGeneral(inner, ",").map((item) => splitTopLevelGeneral(item.trim(), "-")).map((pair) => {
665
+ const reason = pair[0] ?? "";
666
+ const otherId = pair[1] ?? "";
667
+ return [unquoteAtom(reason.trim()), unquoteAtom(otherId.trim())];
668
+ }).filter((pair) => pair[0] !== "" && pair[1] !== "");
669
+ }
670
+ function parseEntityRole(message) {
671
+ if (message?.includes("Source entity does not exist"))
672
+ return "source";
673
+ if (message?.includes("Target entity does not exist"))
674
+ return "target";
675
+ return;
676
+ }
677
+ function unquoteAtom(value) {
678
+ const trimmed = value.trim();
679
+ if (trimmed.length >= 2 && trimmed.startsWith("'") && trimmed.endsWith("'")) {
680
+ const inner = trimmed.slice(1, -1);
681
+ return inner.replace(/\\'/g, "'").replace(/\\\\/g, "\\").replace(/\\n/g, `
682
+ `).replace(/\\t/g, "\t");
683
+ }
684
+ return trimmed;
685
+ }
686
+
49
687
  // ../cli/src/prolog.ts
50
688
  var importMetaDir = path.dirname(fileURLToPath(import.meta.url));
51
689
  var PROLOG_OUTPUT_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
52
690
  var PROLOG_OUTPUT_OVERFLOW_ERROR = "Query exceeded bounded Prolog output capacity (ENOBUFS); narrow the operation or reduce stored entity size";
53
691
  var INTERACTIVE_QUERY_FRAME_END = "__KIBI_QUERY_FRAME_END__";
54
- var require2 = createRequire(import.meta.url);
692
+ var require2 = createRequire2(import.meta.url);
55
693
  function packageVersion(packagePath) {
56
694
  try {
57
695
  const packageJson = require2(packagePath);
@@ -89,6 +727,17 @@ function resolveKbPlPath() {
89
727
  }
90
728
  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
729
  }
730
+ function registerProcessExitOnce(current, handler, on = (event, listener) => process.on(event, listener)) {
731
+ if (current)
732
+ return current;
733
+ on("exit", handler);
734
+ return handler;
735
+ }
736
+ function bindProcessExitHandler(current, terminate, on = (event, listener) => process.on(event, listener)) {
737
+ return registerProcessExitOnce(current, () => {
738
+ terminate();
739
+ }, on);
740
+ }
92
741
 
93
742
  class PrologProcess {
94
743
  process = null;
@@ -111,6 +760,9 @@ class PrologProcess {
111
760
  this.timeout = options.timeout || 30000;
112
761
  this.useOneShotMode = options.oneShot ?? false;
113
762
  }
763
+ attachProcessExitHandler(on = (event, listener) => process.on(event, listener)) {
764
+ this.onProcessExit = bindProcessExitHandler(this.onProcessExit, () => this.terminate(), on);
765
+ }
114
766
  async start() {
115
767
  if (!existsSync(this.swiplPath) && this.swiplPath !== "swipl") {
116
768
  throw new Error(`SWI-Prolog not found at ${this.swiplPath}. Please install SWI-Prolog or check your PATH.`);
@@ -141,19 +793,14 @@ class PrologProcess {
141
793
  });
142
794
  this.process.stdin.write(`true.
143
795
  `);
144
- if (!this.onProcessExit) {
145
- this.onProcessExit = () => {
146
- this.terminate();
147
- };
148
- process.on("exit", this.onProcessExit);
149
- }
796
+ this.attachProcessExitHandler();
150
797
  await this.waitForReady();
151
798
  }
152
799
  async waitForReady() {
153
800
  const start = Date.now();
154
801
  const maxStartWait = 2000;
155
802
  while (Date.now() - start < maxStartWait) {
156
- if (!this.process || this.process.killed) {
803
+ if (!this.process || this.process.killed || this.process.exitCode !== null || this.process.signalCode !== null) {
157
804
  throw new Error(`Prolog process terminated unexpectedly during startup: ${this.translateError(this.errorBuffer)}`);
158
805
  }
159
806
  if (this.errorBuffer.includes("ERROR")) {
@@ -209,7 +856,8 @@ class PrologProcess {
209
856
  const goalLabel = this.goalLabel(normalizedGoal);
210
857
  const shouldPersist = this.isMutatingGoal(normalizedGoal) && !normalizedGoal.includes("kb_save") && !normalizedGoal.includes("kb_commit_upsert");
211
858
  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))`;
859
+ 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)`;
860
+ const wrappedGoal = /^once\s*\(/.test(goalWithPersistence) ? `catch(${goalWithPersistence}, _E, ${errorReport})` : `catch(once((${goalWithPersistence})), _E, ${errorReport})`;
213
861
  const start = Date.now();
214
862
  if (debug) {
215
863
  console.error(`[prolog debug] start query: ${goalLabel}`);
@@ -221,7 +869,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
221
869
  let settled = false;
222
870
  const timeoutId = setTimeout(() => {
223
871
  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})`;
872
+ 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
873
  if (debug) {
226
874
  console.error(`[prolog debug] timeout: ${msg}`);
227
875
  const runtime = this.errorBuffer.split(`
@@ -266,10 +914,12 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
266
914
  console.error(`[prolog debug] query error: ${goalLabel} error=${this.errorBuffer.split(`
267
915
  `)[0]}`);
268
916
  }
917
+ const classified = this.classifyErrorOutput(this.errorBuffer);
269
918
  resolve({
270
919
  success: false,
271
920
  bindings: {},
272
- error: this.addDiagnosticStage(this.translateError(this.errorBuffer), normalizedGoal, this.errorBuffer)
921
+ error: this.addDiagnosticStage(classified.message, normalizedGoal, this.errorBuffer),
922
+ ...classified.record ? { errorRecord: classified.record } : {}
273
923
  });
274
924
  return;
275
925
  }
@@ -392,7 +1042,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
392
1042
  "read_term_from_atom(GoalAtom, Goal, [variable_names(Vars)])",
393
1043
  kbPath ? "getenv('KIBI_KB_PATH', KBPath), kb_attach(KBPath)" : "true",
394
1044
  isBatch ? "WrappedGoal = rdf_transaction(Goal)" : "WrappedGoal = Goal",
395
- "(catch(call(WrappedGoal), E, (print_message(error, E), fail)) -> QuerySucceeded = true ; QuerySucceeded = false)",
1045
+ "(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
1046
  kbPath && (explicitSaveRequested || goalList.some((item) => this.isMutatingGoal(item))) ? "(QuerySucceeded == true -> kb_save ; true)" : "true",
397
1047
  kbPath ? "kb_detach" : "true",
398
1048
  "(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 +1155,12 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
505
1155
  };
506
1156
  }
507
1157
  if (stderr.includes("ERROR")) {
1158
+ const classified = this.classifyErrorOutput(stderr);
508
1159
  return {
509
1160
  success: false,
510
1161
  bindings: {},
511
- error: this.addDiagnosticStage(this.translateError(stderr), combinedGoal ?? "true", stderr)
1162
+ error: this.addDiagnosticStage(classified.message, combinedGoal ?? "true", stderr),
1163
+ ...classified.record ? { errorRecord: classified.record } : {}
512
1164
  };
513
1165
  }
514
1166
  if (stdout.includes("__KIBI_TRUE__")) {
@@ -622,7 +1274,7 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
622
1274
  }
623
1275
  }
624
1276
  isProcessUsable() {
625
- return Boolean(this.process?.stdin && !this.process?.killed && this.process?.exitCode === null);
1277
+ return Boolean(this.process?.stdin && !this.process?.killed && this.process?.exitCode === null && this.process?.signalCode === null);
626
1278
  }
627
1279
  extractBindings(output) {
628
1280
  const bindings = {};
@@ -639,315 +1291,204 @@ writeln('${INTERACTIVE_QUERY_FRAME_END}').
639
1291
  for (const line of output.split(`
640
1292
  `)) {
641
1293
  const match = line.match(/^([A-Z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
642
- if (match?.[1] !== undefined && match[2] !== undefined) {
643
- flush();
644
- variableName = match[1];
645
- valueLines = [match[2]];
646
- continue;
647
- }
648
- if (variableName !== undefined)
649
- valueLines.push(line);
650
- }
651
- flush();
652
- return bindings;
653
- }
654
- translateError(errorText) {
655
- const cleanError = errorText.replace(/^__KIBI_(?:STAGE|RUNTIME)__:[^\r\n]*\r?\n?/gm, "");
656
- if (cleanError.includes("stale_snapshot")) {
657
- return "KB snapshot is stale; reattach or refresh the runtime before retrying (stale_snapshot)";
658
- }
659
- if ((cleanError.includes("audit_log") || cleanError.includes("audit.log") || cleanError.includes("Resource temporarily unavailable")) && (cleanError.includes("lock") || cleanError.includes("permission"))) {
660
- return "Audit journal is locked by another Kibi runtime; restart the stale MCP/CLI session before retrying";
661
- }
662
- if (cleanError.includes("does not exist") && /entity [`'"].+?[`'"]/.test(cleanError)) {
663
- const entityIdMatch = cleanError.match(/entity [`'"]+(.+?)[`'"]+ does not exist/);
664
- const matchedEntityId = entityIdMatch?.[1];
665
- const entityId = matchedEntityId !== undefined ? matchedEntityId.replace(/^`?'+|'+`?$/g, "") : "unknown";
666
- if (cleanError.includes("Target entity does not exist")) {
667
- return `Target entity does not exist: ${entityId}`;
668
- }
669
- if (cleanError.includes("Source entity does not exist")) {
670
- return `Source entity does not exist: ${entityId}`;
671
- }
672
- return `Entity does not exist: ${entityId}`;
673
- }
674
- if (cleanError.includes("Type error: `relationship' expected")) {
675
- const relMatch = cleanError.match(/\(Invalid relationship: ([^)]+)\)/);
676
- const invalidRelationship = relMatch?.[1];
677
- if (invalidRelationship !== undefined) {
678
- return `Invalid relationship: ${invalidRelationship.trim()}`;
679
- }
680
- return "Invalid relationship type or direction";
681
- }
682
- if (cleanError.includes("existence_error") || cleanError.includes("Unknown procedure")) {
683
- return "Predicate or file not found";
684
- }
685
- if (cleanError.includes("permission_error")) {
686
- return "Access denied or KB locked";
687
- }
688
- if (cleanError.includes("syntax_error") || cleanError.includes("Operator expected")) {
689
- return "Invalid query syntax";
690
- }
691
- if (cleanError.includes("timeout_error")) {
692
- return `Operation exceeded ${this.timeout / 1000}s timeout`;
693
- }
694
- const simpleError = (cleanError.replace(/ERROR:\s*/g, "").replace(/^\*\*.*\*\*$/gm, "").replace(/^\s+/gm, "").split(`
695
- `)[0] ?? "").trim();
696
- return simpleError || "Unknown error";
697
- }
698
- isRunning() {
699
- return Boolean(this.process && !this.process.killed && this.process?.exitCode === null);
700
- }
701
- getPid() {
702
- return this.process?.pid || 0;
703
- }
704
- async terminate() {
705
- if (this.terminationPromise) {
706
- await this.terminationPromise;
707
- return;
708
- }
709
- if (this.onProcessExit) {
710
- process.off("exit", this.onProcessExit);
711
- this.onProcessExit = null;
712
- }
713
- const current = this.process;
714
- const oneShots = [...this.oneShotProcesses];
715
- this.process = null;
716
- this.clearQueryBuffers();
717
- if (current || oneShots.length > 0) {
718
- this.terminationPromise = (async () => {
719
- if (current) {
720
- current.stdin?.end();
721
- await this.terminateProcessTree(current);
722
- }
723
- await Promise.all(oneShots.map((child) => this.terminateProcessTree(child)));
724
- })();
725
- try {
726
- await this.terminationPromise;
727
- } finally {
728
- this.terminationPromise = null;
729
- }
730
- }
731
- }
732
- }
733
-
734
- // ../cli/src/prolog/codec.ts
735
- function parseListOfLists(listStr) {
736
- const cleaned = listStr.trim().replace(/^\[/, "").replace(/\]$/, "");
737
- if (cleaned === "") {
738
- return [];
739
- }
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
- }
772
- }
773
- return results;
774
- }
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 {};
782
- }
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
- }
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
- }
1294
+ if (match?.[1] !== undefined && match[2] !== undefined) {
1295
+ flush();
1296
+ variableName = match[1];
1297
+ valueLines = [match[2]];
1298
+ continue;
838
1299
  }
1300
+ if (variableName !== undefined)
1301
+ valueLines.push(line);
839
1302
  }
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);
1303
+ flush();
1304
+ return bindings;
1305
+ }
1306
+ classifyErrorOutput(errorText) {
1307
+ const record = extractPrologErrorRecord(errorText);
1308
+ if (record) {
1309
+ return { message: record.message, record };
1310
+ }
1311
+ return { message: this.translateError(errorText) };
1312
+ }
1313
+ translateError(errorText) {
1314
+ const cleanError = errorText.replace(/^__KIBI_(?:STAGE|RUNTIME|ERROR)__:[^\r\n]*\r?\n?/gm, "");
1315
+ if (cleanError.includes("stale_snapshot")) {
1316
+ return "KB snapshot is stale; reattach or refresh the runtime before retrying (stale_snapshot)";
1317
+ }
1318
+ if ((cleanError.includes("audit_log") || cleanError.includes("audit.log") || cleanError.includes("Resource temporarily unavailable")) && (cleanError.includes("lock") || cleanError.includes("permission"))) {
1319
+ return "Audit journal is locked by another Kibi runtime; restart the stale MCP/CLI session before retrying";
1320
+ }
1321
+ if (cleanError.includes("does not exist") && /entity [`'"].+?[`'"]/.test(cleanError)) {
1322
+ const entityIdMatch = cleanError.match(/entity [`'"]+(.+?)[`'"]+ does not exist/);
1323
+ const matchedEntityId = entityIdMatch?.[1];
1324
+ const entityId = matchedEntityId !== undefined ? matchedEntityId.replace(/^`?'+|'+`?$/g, "") : "unknown";
1325
+ if (cleanError.includes("Target entity does not exist")) {
1326
+ return `Target entity does not exist: ${entityId}`;
858
1327
  }
859
- if (datatype.includes("#boolean")) {
860
- return literalValue === "true";
1328
+ if (cleanError.includes("Source entity does not exist")) {
1329
+ return `Source entity does not exist: ${entityId}`;
861
1330
  }
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());
1331
+ return `Entity does not exist: ${entityId}`;
1332
+ }
1333
+ if (cleanError.includes("Type error: `relationship' expected")) {
1334
+ const relMatch = cleanError.match(/\(Invalid relationship: ([^)]+)\)/);
1335
+ const invalidRelationship = relMatch?.[1];
1336
+ if (invalidRelationship !== undefined) {
1337
+ return `Invalid relationship: ${invalidRelationship.trim()}`;
868
1338
  }
869
- return literalValue;
1339
+ return "Invalid relationship type or direction";
870
1340
  }
871
- }
872
- if (value.startsWith("file:///")) {
873
- const lastSlash = value.lastIndexOf("/");
874
- if (lastSlash !== -1) {
875
- return value.substring(lastSlash + 1);
1341
+ if (cleanError.includes("existence_error") || cleanError.includes("Unknown procedure")) {
1342
+ return "Predicate or file not found";
876
1343
  }
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);
1344
+ if (cleanError.includes("permission_error")) {
1345
+ return "Access denied or KB locked";
1346
+ }
1347
+ if (cleanError.includes("syntax_error") || cleanError.includes("Operator expected")) {
1348
+ return "Invalid query syntax";
884
1349
  }
1350
+ if (cleanError.includes("timeout_error")) {
1351
+ return `Operation exceeded ${this.timeout / 1000}s timeout`;
1352
+ }
1353
+ const simpleError = (cleanError.replace(/ERROR:\s*/g, "").replace(/^\*\*.*\*\*$/gm, "").replace(/^\s+/gm, "").split(`
1354
+ `)[0] ?? "").trim();
1355
+ return simpleError || "Unknown error";
885
1356
  }
886
- if (value.startsWith("'") && value.endsWith("'")) {
887
- return value.substring(1, value.length - 1);
1357
+ isRunning() {
1358
+ return Boolean(this.process && !this.process.killed && this.process?.exitCode === null && this.process?.signalCode === null);
888
1359
  }
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;
1360
+ getPid() {
1361
+ return this.process?.pid || 0;
898
1362
  }
899
- return value;
900
- }
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 = "";
1363
+ async terminate() {
1364
+ if (this.terminationPromise) {
1365
+ await this.terminationPromise;
1366
+ return;
1367
+ }
1368
+ if (this.onProcessExit) {
1369
+ process.off("exit", this.onProcessExit);
1370
+ this.onProcessExit = null;
1371
+ }
1372
+ const current = this.process;
1373
+ const oneShots = [...this.oneShotProcesses];
1374
+ this.process = null;
1375
+ this.clearQueryBuffers();
1376
+ if (current || oneShots.length > 0) {
1377
+ this.terminationPromise = (async () => {
1378
+ if (current) {
1379
+ current.stdin?.end();
1380
+ await this.terminateProcessTree(current);
1381
+ }
1382
+ await Promise.all(oneShots.map((child) => this.terminateProcessTree(child)));
1383
+ })();
1384
+ try {
1385
+ await this.terminationPromise;
1386
+ } finally {
1387
+ this.terminationPromise = null;
926
1388
  }
927
- } else {
928
- current += char;
929
1389
  }
930
1390
  }
931
- if (current) {
932
- results.push(current);
1391
+ }
1392
+
1393
+ // ../cli/src/prolog/store-lock.ts
1394
+ import { existsSync as existsSync2, rmSync } from "node:fs";
1395
+ import { readFileSync } from "node:fs";
1396
+ import { dirname, join } from "node:path";
1397
+ var cachedBootId;
1398
+ function currentBootId() {
1399
+ if (cachedBootId !== undefined)
1400
+ return cachedBootId;
1401
+ if (process.platform !== "linux") {
1402
+ cachedBootId = null;
1403
+ return cachedBootId;
933
1404
  }
934
- return results;
1405
+ try {
1406
+ const raw = readFileSync("/proc/sys/kernel/random/boot_id", "utf8");
1407
+ cachedBootId = raw.trim();
1408
+ } catch {
1409
+ cachedBootId = null;
1410
+ }
1411
+ return cachedBootId;
935
1412
  }
936
- function stripOuterQuotes(value) {
937
- if (value.startsWith("'") && value.endsWith("'")) {
938
- return value.slice(1, -1);
1413
+ function isProcessAlive(pid) {
1414
+ if (!Number.isInteger(pid) || pid <= 0)
1415
+ return false;
1416
+ try {
1417
+ process.kill(pid, 0);
1418
+ return true;
1419
+ } catch (error) {
1420
+ const code = error?.code;
1421
+ return code === "EPERM";
939
1422
  }
940
- if (value.startsWith('"') && value.endsWith('"')) {
941
- return value.slice(1, -1);
1423
+ }
1424
+ function classifyStoreLockHolder(owner) {
1425
+ if (owner === null || typeof owner.pid !== "number")
1426
+ return "unknown";
1427
+ if (owner.pid === process.pid)
1428
+ return "self";
1429
+ const alive = isProcessAlive(owner.pid);
1430
+ if (!alive)
1431
+ return "dead";
1432
+ const bootId = currentBootId();
1433
+ if (bootId !== null && owner.bootId !== undefined && owner.bootId !== "") {
1434
+ return owner.bootId === bootId ? "alive" : "dead";
1435
+ }
1436
+ return "alive";
1437
+ }
1438
+ function breakStoreLock(rdfDirectory) {
1439
+ const candidates = [
1440
+ join(rdfDirectory, "lock"),
1441
+ join(rdfDirectory, ".kibi-lock-owner.json"),
1442
+ join(dirname(rdfDirectory), ".kibi-lock-owner.json")
1443
+ ];
1444
+ for (const candidate of candidates) {
1445
+ if (existsSync2(candidate)) {
1446
+ rmSync(candidate, { force: true });
1447
+ }
942
1448
  }
943
- return value;
944
1449
  }
945
- function normalizeEntityId(value) {
946
- if (!value.startsWith("file:///")) {
947
- return value;
1450
+ function decideStoreLockTakeover(owner) {
1451
+ const state = classifyStoreLockHolder(owner);
1452
+ switch (state) {
1453
+ case "dead":
1454
+ return {
1455
+ action: "break",
1456
+ reason: `recorded holder pid ${owner?.pid ?? "?"} is no longer running`
1457
+ };
1458
+ case "unknown":
1459
+ return {
1460
+ action: "surface",
1461
+ reason: "no ownership journal; the holder cannot be verified"
1462
+ };
1463
+ case "self":
1464
+ case "alive":
1465
+ return {
1466
+ action: "surface",
1467
+ reason: `holder pid ${owner?.pid ?? "?"} is still running${owner?.workspaceRoot !== undefined ? ` for ${owner.workspaceRoot}` : ""}`
1468
+ };
948
1469
  }
949
- const idx = value.lastIndexOf("/");
950
- return idx === -1 ? value : value.slice(idx + 1);
1470
+ }
1471
+ async function retryAttachAfterBreakingStaleLock(prolog, kbPath, failed) {
1472
+ const record = failed.errorRecord;
1473
+ const storeLocked = record?.storeLocked;
1474
+ if (storeLocked === undefined)
1475
+ return failed;
1476
+ const decision = decideStoreLockTakeover(storeLocked.owner);
1477
+ if (decision.action !== "break") {
1478
+ const holderSummary = storeLocked.owner === null ? decision.reason : `pid ${storeLocked.owner.pid ?? "?"} (${decision.reason})`;
1479
+ return {
1480
+ ...failed,
1481
+ error: `${failed.error}
1482
+ Store lock holder: ${holderSummary}. Close that session, or run 'kibi engine stop' for its workspace, then retry.`
1483
+ };
1484
+ }
1485
+ const lockDirectory = storeLocked.lockDirectory !== "" ? storeLocked.lockDirectory : join(kbPath, "rdf");
1486
+ breakStoreLock(lockDirectory);
1487
+ const retried = await prolog.query(`kb_attach('${escapeAtom(kbPath)}')`);
1488
+ if (retried.success) {
1489
+ console.warn(`[KIBI] Broke a stale branch-store lock (${decision.reason}) and reattached.`);
1490
+ }
1491
+ return retried;
951
1492
  }
952
1493
 
953
1494
  // ../cli/src/utils/branch-resolver.ts
@@ -960,9 +1501,9 @@ import * as path3 from "node:path";
960
1501
  // ../cli/src/utils/branch-store-locator.ts
961
1502
  import { createHash } from "node:crypto";
962
1503
  import {
963
- existsSync as existsSync2,
1504
+ existsSync as existsSync3,
964
1505
  mkdirSync,
965
- readFileSync,
1506
+ readFileSync as readFileSync2,
966
1507
  readdirSync,
967
1508
  statSync,
968
1509
  writeFileSync
@@ -990,10 +1531,10 @@ function expectedBranchStoreManifest(branch) {
990
1531
  }
991
1532
  function readBranchStoreManifest(storePath) {
992
1533
  const manifestPath = branchStoreManifestPath(storePath);
993
- if (!existsSync2(manifestPath))
1534
+ if (!existsSync3(manifestPath))
994
1535
  return null;
995
1536
  try {
996
- const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
1537
+ const parsed = JSON.parse(readFileSync2(manifestPath, "utf8"));
997
1538
  if (parsed.version !== BRANCH_STORE_MANIFEST_VERSION || typeof parsed.branch !== "string" || typeof parsed.key !== "string") {
998
1539
  return null;
999
1540
  }
@@ -1020,7 +1561,7 @@ function ensureBranchStoreManifest(workspaceRoot, branch) {
1020
1561
  throw new Error(`Branch store identity mismatch at ${storePath}; refusing to attach it to '${branch}'.`);
1021
1562
  }
1022
1563
  if (existing === null) {
1023
- if (existsSync2(manifestPath)) {
1564
+ if (existsSync3(manifestPath)) {
1024
1565
  throw new Error(`Branch store manifest is invalid at ${manifestPath}; refusing to overwrite it.`);
1025
1566
  }
1026
1567
  const entries = readdirSync(storePath);
@@ -1083,10 +1624,78 @@ var ENGINE_QUERY_CACHE_MAX_ENTRIES = 128;
1083
1624
  var ENGINE_QUERY_CACHE_MAX_RESULT_BYTES = 8 * 1024 * 1024;
1084
1625
  var ENGINE_FRESHNESS_CACHE_MS = 100;
1085
1626
  var ENGINE_PUBLICATION_LOCK_STALE_MS = 5000;
1627
+ var ENGINE_WORKSPACE_WATCHDOG_MS = 30000;
1628
+ function requestEngineSignalShutdown(shutdown) {
1629
+ return () => {
1630
+ shutdown();
1631
+ };
1632
+ }
1633
+ function readJournalGeneration(branchPath) {
1634
+ const currentPath = path4.join(branchPath, "CURRENT");
1635
+ if (!existsSync4(currentPath))
1636
+ return "";
1637
+ return readFileSync3(currentPath, "utf8").trim();
1638
+ }
1639
+ function readEngineAttachmentIdentity(branchPath) {
1640
+ if (!existsSync4(branchPath))
1641
+ return null;
1642
+ const stats = statSync2(branchPath);
1643
+ let resolved = path4.resolve(branchPath);
1644
+ try {
1645
+ resolved = realpathSync(branchPath);
1646
+ } catch {}
1647
+ return {
1648
+ path: resolved,
1649
+ generation: readJournalGeneration(branchPath),
1650
+ dev: stats.dev,
1651
+ ino: stats.ino
1652
+ };
1653
+ }
1654
+ function parseEngineAttachmentIdentity(statusJson) {
1655
+ if (statusJson === undefined)
1656
+ return null;
1657
+ try {
1658
+ const decoded = JSON.parse(JSON.parse(statusJson));
1659
+ if (typeof decoded.attachedPath !== "string" || typeof decoded.attachedGeneration !== "string" || typeof decoded.attachedDev !== "number" || typeof decoded.attachedIno !== "number") {
1660
+ return null;
1661
+ }
1662
+ return {
1663
+ path: decoded.attachedPath,
1664
+ generation: decoded.attachedGeneration,
1665
+ dev: decoded.attachedDev,
1666
+ ino: decoded.attachedIno
1667
+ };
1668
+ } catch {
1669
+ return null;
1670
+ }
1671
+ }
1672
+ function engineAttachmentsMatch(expected, attached) {
1673
+ if (expected === null || attached === null)
1674
+ return false;
1675
+ if (expected.path !== attached.path)
1676
+ return false;
1677
+ if (expected.ino !== 0 && attached.ino !== 0) {
1678
+ return expected.dev === attached.dev && expected.ino === attached.ino;
1679
+ }
1680
+ return expected.generation.length > 0 && expected.generation === attached.generation;
1681
+ }
1682
+ function formatEngineAttachmentMismatch(operation, expected, attached) {
1683
+ return [
1684
+ `operation=${operation}`,
1685
+ `branchStore=${expected?.path ?? "missing"}`,
1686
+ `expectedGeneration=${expected?.generation || "missing"}`,
1687
+ `attachedPath=${attached?.path ?? "missing"}`,
1688
+ `attachedGeneration=${attached?.generation || "missing"}`
1689
+ ].join(" ");
1690
+ }
1086
1691
  function engineIdleTimeoutMs() {
1087
1692
  const configured = Number.parseInt(process.env.KIBI_ENGINE_IDLE_TIMEOUT_MS ?? "", 10);
1088
1693
  return Number.isFinite(configured) && configured >= 100 ? configured : ENGINE_IDLE_TIMEOUT_MS;
1089
1694
  }
1695
+ function engineWorkspaceWatchdogMs() {
1696
+ const configured = Number.parseInt(process.env.KIBI_ENGINE_WORKSPACE_WATCHDOG_MS ?? "", 10);
1697
+ return Number.isFinite(configured) && configured >= 100 ? configured : ENGINE_WORKSPACE_WATCHDOG_MS;
1698
+ }
1090
1699
  function quoteProlog(value) {
1091
1700
  return value.replaceAll("'", "''");
1092
1701
  }
@@ -1215,7 +1824,7 @@ function runtimeDirectory() {
1215
1824
  throw new Error("Unable to create a writable Kibi engine runtime directory");
1216
1825
  }
1217
1826
  function fsyncPath(filePath) {
1218
- if (!existsSync3(filePath))
1827
+ if (!existsSync4(filePath))
1219
1828
  return;
1220
1829
  let descriptor = null;
1221
1830
  try {
@@ -1229,7 +1838,7 @@ function fsyncPath(filePath) {
1229
1838
  function fsyncJournaledBranchStore(branchPath) {
1230
1839
  const rdfRoot = path4.join(branchPath, "rdf");
1231
1840
  const directories = [];
1232
- const pending = existsSync3(rdfRoot) ? [rdfRoot] : [];
1841
+ const pending = existsSync4(rdfRoot) ? [rdfRoot] : [];
1233
1842
  while (pending.length > 0) {
1234
1843
  const directory = pending.pop();
1235
1844
  if (directory === undefined)
@@ -1284,7 +1893,7 @@ function enginePublicationLockPath(workspaceRoot, branch) {
1284
1893
  }
1285
1894
  function publicationLockOwner(lockPath) {
1286
1895
  try {
1287
- const [pidText, createdText] = readFileSync2(lockPath, "utf8").trim().split(":");
1896
+ const [pidText, createdText] = readFileSync3(lockPath, "utf8").trim().split(":");
1288
1897
  const pid = Number.parseInt(pidText ?? "", 10);
1289
1898
  const createdAt = Number.parseInt(createdText ?? "", 10);
1290
1899
  if (!Number.isInteger(pid) || pid <= 1 || !Number.isFinite(createdAt)) {
@@ -1304,7 +1913,7 @@ function processIsAlive(pid) {
1304
1913
  }
1305
1914
  }
1306
1915
  function clearStalePublicationLock(lockPath) {
1307
- if (!existsSync3(lockPath))
1916
+ if (!existsSync4(lockPath))
1308
1917
  return true;
1309
1918
  const owner = publicationLockOwner(lockPath);
1310
1919
  let age = 0;
@@ -1324,11 +1933,11 @@ function clearStalePublicationLock(lockPath) {
1324
1933
  return false;
1325
1934
  }
1326
1935
  function publicationLockIsActive(lockPath) {
1327
- if (!existsSync3(lockPath))
1936
+ if (!existsSync4(lockPath))
1328
1937
  return false;
1329
1938
  if (clearStalePublicationLock(lockPath))
1330
1939
  return false;
1331
- return existsSync3(lockPath);
1940
+ return existsSync4(lockPath);
1332
1941
  }
1333
1942
  function tryAcquireStartLock(lockPath) {
1334
1943
  try {
@@ -1338,7 +1947,7 @@ function tryAcquireStartLock(lockPath) {
1338
1947
  return descriptor;
1339
1948
  } catch {
1340
1949
  try {
1341
- const stamp = readFileSync2(lockPath, "utf8").trim().split(":");
1950
+ const stamp = readFileSync3(lockPath, "utf8").trim().split(":");
1342
1951
  const pid = Number.parseInt(stamp[0] ?? "", 10);
1343
1952
  const created = Number.parseInt(stamp[1] ?? "", 10);
1344
1953
  let age = 0;
@@ -1377,11 +1986,11 @@ function releaseStartLock(lockPath, descriptor) {
1377
1986
  }
1378
1987
  function recoverStaleJournalLock(branchPath) {
1379
1988
  const lockPath = path4.join(branchPath, "rdf", "lock");
1380
- if (!existsSync3(lockPath))
1989
+ if (!existsSync4(lockPath))
1381
1990
  return;
1382
1991
  let contents = "";
1383
1992
  try {
1384
- contents = readFileSync2(lockPath, "utf8");
1993
+ contents = readFileSync3(lockPath, "utf8");
1385
1994
  } catch {
1386
1995
  return;
1387
1996
  }
@@ -1396,7 +2005,7 @@ function recoverStaleJournalLock(branchPath) {
1396
2005
  return;
1397
2006
  } catch {
1398
2007
  try {
1399
- rmSync(lockPath, { force: true });
2008
+ rmSync2(lockPath, { force: true });
1400
2009
  } catch {}
1401
2010
  }
1402
2011
  }
@@ -1408,7 +2017,7 @@ function daemonEntry() {
1408
2017
  path4.join(moduleDirectory, "..", "node_modules", "kibi-cli", "dist", "engine-daemon.js")
1409
2018
  ];
1410
2019
  for (const candidate of candidates) {
1411
- if (existsSync3(candidate))
2020
+ if (existsSync4(candidate))
1412
2021
  return candidate;
1413
2022
  }
1414
2023
  throw new Error("The Kibi engine is not built. Run `npm run build:cli` before using the CLI engine.");
@@ -1445,16 +2054,16 @@ async function connectWithRetry(socketPath, timeoutMs) {
1445
2054
  let lastError;
1446
2055
  while (Date.now() < deadline) {
1447
2056
  const errorPath = `${socketPath}.error`;
1448
- if (existsSync3(errorPath)) {
2057
+ if (existsSync4(errorPath)) {
1449
2058
  try {
1450
- const diagnostic = readFileSync2(errorPath, "utf8").trim();
2059
+ const diagnostic = readFileSync3(errorPath, "utf8").trim();
1451
2060
  if (diagnostic)
1452
2061
  throw new Error(diagnostic);
1453
2062
  } catch (error) {
1454
2063
  throw new Error(`Kibi engine failed to start: ${error instanceof Error ? error.message : String(error)}`);
1455
2064
  }
1456
2065
  }
1457
- if (process.platform !== "win32" && !existsSync3(socketPath)) {
2066
+ if (process.platform !== "win32" && !existsSync4(socketPath)) {
1458
2067
  await new Promise((resolve3) => setTimeout(resolve3, 25));
1459
2068
  continue;
1460
2069
  }
@@ -1480,6 +2089,7 @@ class EngineClient {
1480
2089
  pending = new Map;
1481
2090
  requestTail = Promise.resolve();
1482
2091
  lastResult = null;
2092
+ replacingStaleDaemon = false;
1483
2093
  constructor(options) {
1484
2094
  if (!isValidBranchName(options.branch)) {
1485
2095
  throw new Error(`Invalid Kibi engine branch name: ${options.branch}`);
@@ -1495,6 +2105,13 @@ class EngineClient {
1495
2105
  this.allowPublicationLock = options.allowPublicationLock ?? false;
1496
2106
  }
1497
2107
  async start(allowSpawn = true) {
2108
+ const wasConnected = this.socket !== null && !this.socket.destroyed;
2109
+ await this.connect(allowSpawn);
2110
+ if (!wasConnected && this.socket !== null) {
2111
+ await this.reconcileAttachment();
2112
+ }
2113
+ }
2114
+ async connect(allowSpawn = true) {
1498
2115
  if (this.socket !== null && !this.socket.destroyed)
1499
2116
  return;
1500
2117
  const socketPath = engineSocketPath(this.workspaceRoot, this.branch);
@@ -1503,7 +2120,7 @@ class EngineClient {
1503
2120
  throw new Error(`Kibi engine publication is in progress: ${publicationLockPath}`);
1504
2121
  }
1505
2122
  try {
1506
- if (existsSync3(socketPath)) {
2123
+ if (existsSync4(socketPath)) {
1507
2124
  this.socket = await connectWithRetry(socketPath, 150);
1508
2125
  } else {
1509
2126
  throw new Error("engine socket is absent");
@@ -1517,7 +2134,7 @@ class EngineClient {
1517
2134
  const startDeadline = Date.now() + this.timeout;
1518
2135
  let lock = null;
1519
2136
  while (lock === null && Date.now() < startDeadline) {
1520
- if (existsSync3(socketPath)) {
2137
+ if (existsSync4(socketPath)) {
1521
2138
  try {
1522
2139
  this.socket = await connectWithRetry(socketPath, 100);
1523
2140
  break;
@@ -1535,7 +2152,7 @@ class EngineClient {
1535
2152
  }
1536
2153
  if (lock !== null) {
1537
2154
  try {
1538
- rmSync(`${socketPath}.error`, { force: true });
2155
+ rmSync2(`${socketPath}.error`, { force: true });
1539
2156
  try {
1540
2157
  const daemon = spawn2(nodePath, [
1541
2158
  entry,
@@ -1760,27 +2377,37 @@ class EngineClient {
1760
2377
  isRunning() {
1761
2378
  return this.socket !== null && !this.socket.destroyed;
1762
2379
  }
1763
- getPid() {
1764
- const pidPath = enginePidPath(this.workspaceRoot, this.branch);
2380
+ async readAttachedIdentity() {
2381
+ const status = await this.queryStatusJson();
2382
+ if (!status.success)
2383
+ return null;
2384
+ return parseEngineAttachmentIdentity(status.bindings.JsonString);
2385
+ }
2386
+ async reconcileAttachment() {
2387
+ if (this.replacingStaleDaemon || this.socket === null)
2388
+ return;
2389
+ const livePath = branchStorePath(this.workspaceRoot, this.branch);
2390
+ const expected = readEngineAttachmentIdentity(livePath);
2391
+ const attached = await this.readAttachedIdentity();
2392
+ if (engineAttachmentsMatch(expected, attached))
2393
+ return;
2394
+ this.replacingStaleDaemon = true;
1765
2395
  try {
1766
- return Number.parseInt(readFileSync2(pidPath, "utf8").trim(), 10) || 0;
1767
- } catch {
1768
- return 0;
2396
+ await this.shutdownConnectedDaemon();
2397
+ await this.connect(true);
2398
+ const live = readEngineAttachmentIdentity(livePath);
2399
+ const retried = await this.readAttachedIdentity();
2400
+ if (!engineAttachmentsMatch(live, retried)) {
2401
+ throw new Error(`Kibi engine is attached to a stale branch generation: ${formatEngineAttachmentMismatch("attachment-handshake", live, retried)}`);
2402
+ }
2403
+ } finally {
2404
+ this.replacingStaleDaemon = false;
1769
2405
  }
1770
2406
  }
1771
- async stop(startIfMissing = true) {
2407
+ async shutdownConnectedDaemon() {
1772
2408
  const socketPath = engineSocketPath(this.workspaceRoot, this.branch);
1773
2409
  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)) {
2410
+ if (existsSync4(socketPath)) {
1784
2411
  throw new Error(`Kibi engine socket remains present but is not reachable: ${socketPath}`);
1785
2412
  }
1786
2413
  return;
@@ -1794,14 +2421,41 @@ class EngineClient {
1794
2421
  this.socket?.destroy();
1795
2422
  this.socket = null;
1796
2423
  const deadline = Date.now() + 5000;
1797
- while (existsSync3(socketPath) && Date.now() < deadline) {
2424
+ while (existsSync4(socketPath) && Date.now() < deadline) {
1798
2425
  await new Promise((resolve3) => setTimeout(resolve3, 25));
1799
2426
  }
1800
- if (existsSync3(socketPath)) {
2427
+ if (existsSync4(socketPath)) {
1801
2428
  const detail = requestError instanceof Error ? `: ${requestError.message}` : "";
1802
2429
  throw new Error(`Kibi engine did not stop within 5 seconds${detail}`);
1803
2430
  }
1804
2431
  }
2432
+ getPid() {
2433
+ const pidPath = enginePidPath(this.workspaceRoot, this.branch);
2434
+ try {
2435
+ return Number.parseInt(readFileSync3(pidPath, "utf8").trim(), 10) || 0;
2436
+ } catch {
2437
+ return 0;
2438
+ }
2439
+ }
2440
+ async stop(startIfMissing = true) {
2441
+ const socketPath = engineSocketPath(this.workspaceRoot, this.branch);
2442
+ if (!this.isRunning()) {
2443
+ try {
2444
+ await this.connect(startIfMissing);
2445
+ } catch {
2446
+ if (!startIfMissing)
2447
+ return;
2448
+ throw new Error("Unable to start Kibi engine for stop request");
2449
+ }
2450
+ }
2451
+ if (!this.isRunning()) {
2452
+ if (existsSync4(socketPath)) {
2453
+ throw new Error(`Kibi engine socket remains present but is not reachable: ${socketPath}`);
2454
+ }
2455
+ return;
2456
+ }
2457
+ await this.shutdownConnectedDaemon();
2458
+ }
1805
2459
  async terminate() {
1806
2460
  this.socket?.end();
1807
2461
  this.socket?.destroy();
@@ -1813,17 +2467,17 @@ class EngineClient {
1813
2467
  async function ensureJournaledBranchStoreAsync(branchPath) {
1814
2468
  mkdirSync2(branchPath, { recursive: true });
1815
2469
  const markerPath = path4.join(branchPath, "storage.json");
1816
- const marker = existsSync3(markerPath) ? readFileSync2(markerPath, "utf8") : "";
2470
+ const marker = existsSync4(markerPath) ? readFileSync3(markerPath, "utf8") : "";
1817
2471
  if (marker.includes("kibi.rdf-journal.v1")) {
1818
2472
  const rdfDirectory = path4.join(branchPath, "rdf");
1819
2473
  const currentPath = path4.join(branchPath, "CURRENT");
1820
2474
  const sentinelPath = path4.join(branchPath, "kb.rdf");
1821
- if (!existsSync3(rdfDirectory) || !existsSync3(currentPath)) {
2475
+ if (!existsSync4(rdfDirectory) || !existsSync4(currentPath)) {
1822
2476
  recoverInterruptedGeneration(branchPath);
1823
- if (existsSync3(rdfDirectory) && existsSync3(currentPath)) {
2477
+ if (existsSync4(rdfDirectory) && existsSync4(currentPath)) {
1824
2478
  recoverStaleJournalLock(branchPath);
1825
2479
  archiveLegacyFiles(branchPath);
1826
- if (!existsSync3(sentinelPath)) {
2480
+ if (!existsSync4(sentinelPath)) {
1827
2481
  writeFileSync2(sentinelPath, `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
1828
2482
  `, { mode: 384 });
1829
2483
  }
@@ -1831,18 +2485,18 @@ async function ensureJournaledBranchStoreAsync(branchPath) {
1831
2485
  }
1832
2486
  const legacyRdf2 = path4.join(branchPath, "kb.rdf");
1833
2487
  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 });
2488
+ const legacyContents = existsSync4(legacyRdf2) ? readFileSync3(legacyRdf2, "utf8") : "";
2489
+ if (existsSync4(legacyAudit2) || existsSync4(legacyRdf2) && !legacyContents.includes("kibi.rdf-journal.v1")) {
2490
+ rmSync2(rdfDirectory, { recursive: true, force: true });
2491
+ rmSync2(currentPath, { force: true });
2492
+ rmSync2(markerPath, { force: true });
1839
2493
  } else {
1840
2494
  throw new Error(`Journaled branch store is incomplete at ${branchPath}; restore the immutable legacy/ backup or remove the interrupted generation before retrying`);
1841
2495
  }
1842
2496
  } else {
1843
2497
  recoverStaleJournalLock(branchPath);
1844
2498
  archiveLegacyFiles(branchPath);
1845
- if (!existsSync3(sentinelPath)) {
2499
+ if (!existsSync4(sentinelPath)) {
1846
2500
  writeFileSync2(sentinelPath, `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
1847
2501
  `, { mode: 384 });
1848
2502
  }
@@ -1851,7 +2505,7 @@ async function ensureJournaledBranchStoreAsync(branchPath) {
1851
2505
  }
1852
2506
  const legacyRdf = path4.join(branchPath, "kb.rdf");
1853
2507
  const legacyAudit = path4.join(branchPath, "audit.log");
1854
- if (!existsSync3(legacyRdf) && !existsSync3(legacyAudit)) {
2508
+ if (!existsSync4(legacyRdf) && !existsSync4(legacyAudit)) {
1855
2509
  mkdirSync2(path4.join(branchPath, "rdf"), { recursive: true });
1856
2510
  writeFileSync2(markerPath, `{"format":"kibi.rdf-journal.v1","schemaVersion":1}
1857
2511
  `, { mode: 384 });
@@ -1863,8 +2517,8 @@ async function ensureJournaledBranchStoreAsync(branchPath) {
1863
2517
  `, { mode: 384 });
1864
2518
  return;
1865
2519
  }
1866
- if (existsSync3(legacyRdf)) {
1867
- const legacyContents = readFileSync2(legacyRdf, "utf8");
2520
+ if (existsSync4(legacyRdf)) {
2521
+ const legacyContents = readFileSync3(legacyRdf, "utf8");
1868
2522
  if (!/<rdf:RDF(?:\s|>)/.test(legacyContents)) {
1869
2523
  throw new Error(`Legacy Kibi store is corrupt at ${legacyRdf}: expected RDF/XML root`);
1870
2524
  }
@@ -1893,40 +2547,40 @@ async function ensureJournaledBranchStoreAsync(branchPath) {
1893
2547
  throw new Error(migrated.error ?? "Legacy KB migration failed");
1894
2548
  fsyncJournaledBranchStore(staging);
1895
2549
  } catch (error) {
1896
- rmSync(staging, { recursive: true, force: true });
2550
+ rmSync2(staging, { recursive: true, force: true });
1897
2551
  throw error;
1898
2552
  } finally {
1899
2553
  await migrator.terminate();
1900
2554
  }
1901
2555
  for (const file of ["rdf", "storage.json", "CURRENT"]) {
1902
2556
  const source = path4.join(staging, file);
1903
- if (!existsSync3(source))
2557
+ if (!existsSync4(source))
1904
2558
  continue;
1905
2559
  const target = path4.join(branchPath, file);
1906
- if (existsSync3(target))
1907
- rmSync(target, { recursive: true, force: true });
2560
+ if (existsSync4(target))
2561
+ rmSync2(target, { recursive: true, force: true });
1908
2562
  renameSync(source, target);
1909
2563
  }
1910
2564
  archiveLegacyFiles(branchPath);
1911
2565
  writeFileSync2(path4.join(branchPath, "kb.rdf"), `KIBI_STORAGE_FORMAT=kibi.rdf-journal.v1
1912
2566
  `, { mode: 384 });
1913
2567
  fsyncJournaledBranchStore(branchPath);
1914
- rmSync(staging, { recursive: true, force: true });
2568
+ rmSync2(staging, { recursive: true, force: true });
1915
2569
  }
1916
2570
  function archiveLegacyFiles(branchPath) {
1917
2571
  const legacyDir = path4.join(branchPath, "legacy");
1918
2572
  mkdirSync2(legacyDir, { recursive: true, mode: 448 });
1919
2573
  for (const file of ["kb.rdf", "audit.log"]) {
1920
2574
  const source = path4.join(branchPath, file);
1921
- if (!existsSync3(source))
2575
+ if (!existsSync4(source))
1922
2576
  continue;
1923
2577
  if (file === "kb.rdf") {
1924
- const contents = readFileSync2(source, "utf8");
2578
+ const contents = readFileSync3(source, "utf8");
1925
2579
  if (contents.includes("kibi.rdf-journal.v1"))
1926
2580
  continue;
1927
2581
  }
1928
2582
  const destination = path4.join(legacyDir, file);
1929
- if (existsSync3(destination)) {
2583
+ if (existsSync4(destination)) {
1930
2584
  renameSync(source, path4.join(legacyDir, `${file}.recovery.${Date.now()}`));
1931
2585
  } else {
1932
2586
  renameSync(source, destination);
@@ -1949,23 +2603,23 @@ function recoverInterruptedGeneration(branchPath) {
1949
2603
  const oldCurrent = newest("CURRENT.old.");
1950
2604
  const rdfPath = path4.join(branchPath, "rdf");
1951
2605
  const currentPath = path4.join(branchPath, "CURRENT");
1952
- if ((!existsSync3(rdfPath) || !existsSync3(currentPath)) && oldRdf && oldCurrent) {
2606
+ if ((!existsSync4(rdfPath) || !existsSync4(currentPath)) && oldRdf && oldCurrent) {
1953
2607
  try {
1954
- if (existsSync3(rdfPath))
1955
- rmSync(rdfPath, { recursive: true, force: true });
1956
- if (existsSync3(currentPath))
1957
- rmSync(currentPath, { force: true });
2608
+ if (existsSync4(rdfPath))
2609
+ rmSync2(rdfPath, { recursive: true, force: true });
2610
+ if (existsSync4(currentPath))
2611
+ rmSync2(currentPath, { force: true });
1958
2612
  renameSync(oldRdf, rdfPath);
1959
2613
  renameSync(oldCurrent, currentPath);
1960
2614
  return;
1961
2615
  } catch {}
1962
2616
  }
1963
- if (!existsSync3(rdfPath) && oldRdf) {
2617
+ if (!existsSync4(rdfPath) && oldRdf) {
1964
2618
  try {
1965
2619
  renameSync(oldRdf, rdfPath);
1966
2620
  } catch {}
1967
2621
  }
1968
- if (!existsSync3(currentPath) && oldCurrent) {
2622
+ if (!existsSync4(currentPath) && oldCurrent) {
1969
2623
  try {
1970
2624
  renameSync(oldCurrent, currentPath);
1971
2625
  } catch {}
@@ -1980,15 +2634,25 @@ async function runEngineDaemon(options) {
1980
2634
  const prolog = new PrologProcess({ timeout: 120000, oneShot: false });
1981
2635
  await prolog.start();
1982
2636
  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");
2637
+ if (!attached.success) {
2638
+ const recovered = await retryAttachAfterBreakingStaleLock(prolog, branchPath, attached);
2639
+ if (!recovered.success)
2640
+ throw new Error(recovered.error ?? "Failed to attach branch KB");
2641
+ }
2642
+ const attachedIdentity = readEngineAttachmentIdentity(branchPath);
2643
+ const coreModuleDir = path4.dirname(resolveKbPlPath());
2644
+ for (const [fileName, label] of [
2645
+ ["status.pl", "status"],
2646
+ ["discovery.pl", "discovery"]
2647
+ ]) {
2648
+ const modulePath = path4.join(coreModuleDir, fileName);
2649
+ const loaded = await prolog.query(`use_module('${quoteProlog(modulePath.replaceAll("\\", "/"))}')`);
2650
+ if (!loaded.success) {
2651
+ throw new Error(loaded.error ?? `Failed to load Kibi ${label} module`);
2652
+ }
1989
2653
  }
1990
2654
  mkdirSync2(path4.dirname(options.socketPath), { recursive: true, mode: 448 });
1991
- if (existsSync3(options.socketPath)) {
2655
+ if (existsSync4(options.socketPath)) {
1992
2656
  let live = false;
1993
2657
  try {
1994
2658
  const existing = await connectSocket(options.socketPath, 100);
@@ -1999,7 +2663,7 @@ async function runEngineDaemon(options) {
1999
2663
  throw new Error(`A Kibi engine is already listening at ${options.socketPath}`);
2000
2664
  }
2001
2665
  try {
2002
- rmSync(options.socketPath, { force: true });
2666
+ rmSync2(options.socketPath, { force: true });
2003
2667
  } catch {}
2004
2668
  }
2005
2669
  writeFileSync2(enginePidPath(options.workspaceRoot, options.branch), `${process.pid}
@@ -2021,6 +2685,12 @@ async function runEngineDaemon(options) {
2021
2685
  try {
2022
2686
  const decoded = JSON.parse(JSON.parse(result.bindings.JsonString));
2023
2687
  decoded.branch = options.branch;
2688
+ if (attachedIdentity !== null) {
2689
+ decoded.attachedPath = attachedIdentity.path;
2690
+ decoded.attachedGeneration = attachedIdentity.generation;
2691
+ decoded.attachedDev = attachedIdentity.dev;
2692
+ decoded.attachedIno = attachedIdentity.ino;
2693
+ }
2024
2694
  return {
2025
2695
  ...result,
2026
2696
  bindings: {
@@ -2032,6 +2702,21 @@ async function runEngineDaemon(options) {
2032
2702
  return result;
2033
2703
  }
2034
2704
  };
2705
+ let workspaceMissingStreak = 0;
2706
+ const workspaceWatchdog = setInterval(() => {
2707
+ if (shuttingDown)
2708
+ return;
2709
+ if (!existsSync4(options.workspaceRoot)) {
2710
+ workspaceMissingStreak += 1;
2711
+ if (workspaceMissingStreak < 2)
2712
+ return;
2713
+ console.error(`[KIBI] workspace ${options.workspaceRoot} no longer exists; stopping engine`);
2714
+ shutdown();
2715
+ return;
2716
+ }
2717
+ workspaceMissingStreak = 0;
2718
+ }, engineWorkspaceWatchdogMs());
2719
+ workspaceWatchdog.unref();
2035
2720
  const scheduleIdleExit = () => {
2036
2721
  if (activeClients > 0)
2037
2722
  return;
@@ -2276,6 +2961,7 @@ async function runEngineDaemon(options) {
2276
2961
  process.off("SIGINT", requestSignalShutdown);
2277
2962
  if (idleTimer)
2278
2963
  clearTimeout(idleTimer);
2964
+ clearInterval(workspaceWatchdog);
2279
2965
  const saved = await prolog.query("kb_save").catch(() => null);
2280
2966
  if (saved?.success) {
2281
2967
  try {
@@ -2290,18 +2976,16 @@ async function runEngineDaemon(options) {
2290
2976
  server.close();
2291
2977
  server.unref();
2292
2978
  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), {
2979
+ if (existsSync4(options.socketPath))
2980
+ rmSync2(options.socketPath, { force: true });
2981
+ if (existsSync4(enginePidPath(options.workspaceRoot, options.branch)))
2982
+ rmSync2(enginePidPath(options.workspaceRoot, options.branch), {
2297
2983
  force: true
2298
2984
  });
2299
2985
  } catch {}
2300
2986
  setTimeout(() => process.exit(0), 50).unref();
2301
2987
  };
2302
- const requestSignalShutdown = () => {
2303
- shutdown();
2304
- };
2988
+ const requestSignalShutdown = requestEngineSignalShutdown(shutdown);
2305
2989
  process.once("SIGTERM", requestSignalShutdown);
2306
2990
  process.once("SIGINT", requestSignalShutdown);
2307
2991
  server.on("connection", (socket) => {
@@ -2372,6 +3056,19 @@ async function runEngineDaemon(options) {
2372
3056
  }
2373
3057
 
2374
3058
  // ../cli/src/engine-daemon.ts
3059
+ function isEngineDaemonEntrypoint(argv = process.argv, moduleUrl = import.meta.url, isMain = __require.main == __require.module) {
3060
+ if (isMain === true) {
3061
+ return moduleUrl === import.meta.url;
3062
+ }
3063
+ const entry = argv[1];
3064
+ if (typeof entry !== "string" || entry.length === 0)
3065
+ return false;
3066
+ try {
3067
+ return moduleUrl === pathToFileURL(path5.resolve(entry)).href;
3068
+ } catch {
3069
+ return false;
3070
+ }
3071
+ }
2375
3072
  function requiredArg(args, name) {
2376
3073
  const index = args.indexOf(name);
2377
3074
  const value = index >= 0 ? args[index + 1] : undefined;
@@ -2380,23 +3077,35 @@ function requiredArg(args, name) {
2380
3077
  }
2381
3078
  return value;
2382
3079
  }
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}
3080
+ async function runEngineDaemonCli(args = process.argv.slice(2)) {
3081
+ try {
3082
+ await runEngineDaemon({
3083
+ workspaceRoot: requiredArg(args, "--workspace"),
3084
+ branch: requiredArg(args, "--branch"),
3085
+ socketPath: requiredArg(args, "--socket")
3086
+ });
3087
+ } catch (error) {
3088
+ const message = error instanceof Error ? error.message : String(error);
3089
+ console.error(message);
3090
+ const socketIndex = args.indexOf("--socket");
3091
+ const socketPath = socketIndex >= 0 ? args[socketIndex + 1] : undefined;
3092
+ if (socketPath) {
3093
+ try {
3094
+ writeFileSync3(`${socketPath}.error`, `${message}
2398
3095
  `, { mode: 384 });
2399
- } catch {}
3096
+ } catch {}
3097
+ }
3098
+ process.exitCode = 1;
2400
3099
  }
2401
- process.exitCode = 1;
2402
3100
  }
3101
+ async function runEngineDaemonIfEntrypoint(isEntrypoint = isEngineDaemonEntrypoint()) {
3102
+ if (!isEntrypoint)
3103
+ return;
3104
+ await runEngineDaemonCli();
3105
+ }
3106
+ await runEngineDaemonIfEntrypoint();
3107
+ export {
3108
+ runEngineDaemonIfEntrypoint,
3109
+ runEngineDaemonCli,
3110
+ isEngineDaemonEntrypoint
3111
+ };