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.
- package/dist/engine-daemon.js +1116 -407
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6314 -2446
- package/dist/skill-system/validation.js +8 -8
- package/dist/skills/kibi-freshness/SKILL.md +1 -1
- package/dist/skills/kibi-usage/SKILL.md +41 -12
- package/dist/skills/kibi-usage/resources/operation-access.md +1 -1
- package/dist/skills/kibi-usage/resources/proof.md +105 -0
- package/dist/skills/kibi-usage/resources/workflows.md +6 -6
- package/package.json +2 -2
package/dist/engine-daemon.js
CHANGED
|
@@ -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
|
|
16
|
+
existsSync as existsSync4,
|
|
13
17
|
fsyncSync,
|
|
14
18
|
mkdirSync as mkdirSync2,
|
|
15
19
|
openSync,
|
|
16
|
-
readFileSync as
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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 (
|
|
860
|
-
return
|
|
1328
|
+
if (cleanError.includes("Source entity does not exist")) {
|
|
1329
|
+
return `Source entity does not exist: ${entityId}`;
|
|
861
1330
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
|
1339
|
+
return "Invalid relationship type or direction";
|
|
870
1340
|
}
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
return
|
|
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
|
-
|
|
887
|
-
return
|
|
1357
|
+
isRunning() {
|
|
1358
|
+
return Boolean(this.process && !this.process.killed && this.process?.exitCode === null && this.process?.signalCode === null);
|
|
888
1359
|
}
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
const
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
|
|
932
|
-
|
|
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
|
-
|
|
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
|
|
937
|
-
if (
|
|
938
|
-
return
|
|
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
|
-
|
|
941
|
-
|
|
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
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
950
|
-
|
|
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
|
|
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 (!
|
|
1534
|
+
if (!existsSync3(manifestPath))
|
|
994
1535
|
return null;
|
|
995
1536
|
try {
|
|
996
|
-
const parsed = JSON.parse(
|
|
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 (
|
|
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 (!
|
|
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 =
|
|
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] =
|
|
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 (!
|
|
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 (!
|
|
1936
|
+
if (!existsSync4(lockPath))
|
|
1328
1937
|
return false;
|
|
1329
1938
|
if (clearStalePublicationLock(lockPath))
|
|
1330
1939
|
return false;
|
|
1331
|
-
return
|
|
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 =
|
|
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 (!
|
|
1989
|
+
if (!existsSync4(lockPath))
|
|
1381
1990
|
return;
|
|
1382
1991
|
let contents = "";
|
|
1383
1992
|
try {
|
|
1384
|
-
contents =
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
2057
|
+
if (existsSync4(errorPath)) {
|
|
1449
2058
|
try {
|
|
1450
|
-
const diagnostic =
|
|
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" && !
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
1764
|
-
const
|
|
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
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
|
2407
|
+
async shutdownConnectedDaemon() {
|
|
1772
2408
|
const socketPath = engineSocketPath(this.workspaceRoot, this.branch);
|
|
1773
2409
|
if (!this.isRunning()) {
|
|
1774
|
-
|
|
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 (
|
|
2424
|
+
while (existsSync4(socketPath) && Date.now() < deadline) {
|
|
1798
2425
|
await new Promise((resolve3) => setTimeout(resolve3, 25));
|
|
1799
2426
|
}
|
|
1800
|
-
if (
|
|
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 =
|
|
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 (!
|
|
2475
|
+
if (!existsSync4(rdfDirectory) || !existsSync4(currentPath)) {
|
|
1822
2476
|
recoverInterruptedGeneration(branchPath);
|
|
1823
|
-
if (
|
|
2477
|
+
if (existsSync4(rdfDirectory) && existsSync4(currentPath)) {
|
|
1824
2478
|
recoverStaleJournalLock(branchPath);
|
|
1825
2479
|
archiveLegacyFiles(branchPath);
|
|
1826
|
-
if (!
|
|
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 =
|
|
1835
|
-
if (
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
1867
|
-
const legacyContents =
|
|
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
|
-
|
|
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 (!
|
|
2557
|
+
if (!existsSync4(source))
|
|
1904
2558
|
continue;
|
|
1905
2559
|
const target = path4.join(branchPath, file);
|
|
1906
|
-
if (
|
|
1907
|
-
|
|
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
|
-
|
|
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 (!
|
|
2575
|
+
if (!existsSync4(source))
|
|
1922
2576
|
continue;
|
|
1923
2577
|
if (file === "kb.rdf") {
|
|
1924
|
-
const contents =
|
|
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 (
|
|
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 ((!
|
|
2606
|
+
if ((!existsSync4(rdfPath) || !existsSync4(currentPath)) && oldRdf && oldCurrent) {
|
|
1953
2607
|
try {
|
|
1954
|
-
if (
|
|
1955
|
-
|
|
1956
|
-
if (
|
|
1957
|
-
|
|
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 (!
|
|
2617
|
+
if (!existsSync4(rdfPath) && oldRdf) {
|
|
1964
2618
|
try {
|
|
1965
2619
|
renameSync(oldRdf, rdfPath);
|
|
1966
2620
|
} catch {}
|
|
1967
2621
|
}
|
|
1968
|
-
if (!
|
|
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
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
2294
|
-
|
|
2295
|
-
if (
|
|
2296
|
-
|
|
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
|
-
|
|
2384
|
-
try {
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
} catch (error) {
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
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
|
-
|
|
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
|
+
};
|