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