@gmickel/gno 1.22.0 → 1.24.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/README.md +33 -12
- package/assets/skill/SKILL.md +41 -19
- package/package.json +1 -1
- package/spec/cli.md +127 -20
- package/spec/evals-agentic.md +35 -0
- package/spec/evals.md +6 -0
- package/spec/mcp.md +18 -0
- package/spec/output-schemas/query-diagnose-v1.schema.json +123 -0
- package/spec/output-schemas/query-diagnose.schema.json +89 -2
- package/spec/output-schemas/setup-activation-result.schema.json +456 -0
- package/spec/output-schemas/setup-command-result.schema.json +93 -0
- package/spec/output-schemas/setup-receipt.schema.json +258 -0
- package/spec/output-schemas/setup-semantic-receipt.schema.json +195 -0
- package/src/app/context-runtime-types.ts +3 -0
- package/src/app/context-runtime.ts +1 -0
- package/src/app/context-surface.ts +4 -2
- package/src/cli/commands/ask.ts +31 -20
- package/src/cli/commands/completion/scripts.ts +2 -0
- package/src/cli/commands/context-build.ts +17 -7
- package/src/cli/commands/embed.ts +7 -2
- package/src/cli/commands/query.ts +58 -37
- package/src/cli/commands/search.ts +29 -19
- package/src/cli/commands/setup-activation.ts +324 -0
- package/src/cli/commands/setup-semantic.ts +591 -0
- package/src/cli/commands/setup.ts +410 -0
- package/src/cli/commands/vsearch.ts +31 -22
- package/src/cli/options.ts +39 -0
- package/src/cli/program.ts +112 -0
- package/src/cli/setup-semantic-worker.ts +177 -0
- package/src/config/defaults.ts +10 -1
- package/src/config/types.ts +71 -0
- package/src/core/config-mutation.ts +94 -64
- package/src/core/file-lock.ts +70 -31
- package/src/core/folder-setup-planning.ts +453 -0
- package/src/core/folder-setup.ts +490 -0
- package/src/core/project-affinity-surface.ts +114 -0
- package/src/core/project-affinity.ts +330 -0
- package/src/core/setup-activation.ts +309 -0
- package/src/core/setup-receipt.ts +321 -0
- package/src/core/validation.ts +20 -1
- package/src/mcp/tools/ask.ts +10 -1
- package/src/mcp/tools/context.ts +18 -0
- package/src/mcp/tools/index.ts +13 -2
- package/src/mcp/tools/query.ts +12 -0
- package/src/mcp/tools/search.ts +7 -0
- package/src/mcp/tools/vsearch.ts +7 -0
- package/src/pipeline/diagnose.ts +48 -3
- package/src/pipeline/explain.ts +54 -13
- package/src/pipeline/hybrid.ts +100 -59
- package/src/pipeline/project-affinity.ts +162 -0
- package/src/pipeline/search.ts +76 -10
- package/src/pipeline/types.ts +9 -0
- package/src/pipeline/vsearch.ts +117 -91
- package/src/sdk/client.ts +80 -20
- package/src/sdk/index.ts +2 -0
- package/src/sdk/types.ts +20 -7
- package/src/serve/connectors.ts +29 -2
- package/src/serve/context-capsule.ts +18 -1
- package/src/serve/routes/api.ts +69 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable, one-shot semantic handoff for `gno setup`.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately independent of the resident runtime. The setup parent
|
|
5
|
+
* records a local job and starts one detached Bun process; that process embeds
|
|
6
|
+
* the selected collection and exits.
|
|
7
|
+
*
|
|
8
|
+
* @module src/cli/commands/setup-semantic
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// node:fs provides append-only descriptors for detached child stdio; Bun.spawn
|
|
12
|
+
// accepts numeric descriptors but Bun has no append-open equivalent.
|
|
13
|
+
import { closeSync, openSync } from "node:fs";
|
|
14
|
+
// node:fs/promises provides private atomic file replacement without a Bun equivalent.
|
|
15
|
+
import { chmod, mkdir, open, rename, unlink } from "node:fs/promises";
|
|
16
|
+
// node:path has no Bun path utilities.
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
|
|
19
|
+
import type { FolderSetupReceipt } from "../../core/setup-receipt";
|
|
20
|
+
|
|
21
|
+
import { VERSION } from "../../app/constants";
|
|
22
|
+
import { canonicalizeIndexName } from "../../app/index-name";
|
|
23
|
+
import { withWriteLock } from "../../core/file-lock";
|
|
24
|
+
import {
|
|
25
|
+
setupFingerprint,
|
|
26
|
+
setupRootFingerprint,
|
|
27
|
+
} from "../../core/setup-receipt";
|
|
28
|
+
import { isProcessAlive } from "../detach";
|
|
29
|
+
|
|
30
|
+
export const SETUP_SEMANTIC_SCHEMA_VERSION = "1.0" as const;
|
|
31
|
+
export const SETUP_SEMANTIC_STATUSES = [
|
|
32
|
+
"scheduled",
|
|
33
|
+
"running",
|
|
34
|
+
"completed",
|
|
35
|
+
"failed",
|
|
36
|
+
"pending",
|
|
37
|
+
"skipped",
|
|
38
|
+
] as const;
|
|
39
|
+
|
|
40
|
+
export type SetupSemanticStatus = (typeof SETUP_SEMANTIC_STATUSES)[number];
|
|
41
|
+
|
|
42
|
+
export interface SetupSemanticReceipt {
|
|
43
|
+
schemaVersion: typeof SETUP_SEMANTIC_SCHEMA_VERSION;
|
|
44
|
+
status: SetupSemanticStatus;
|
|
45
|
+
generatedAt: string;
|
|
46
|
+
startedAt: string | null;
|
|
47
|
+
completedAt: string | null;
|
|
48
|
+
jobId: string;
|
|
49
|
+
collection: string;
|
|
50
|
+
indexName: string;
|
|
51
|
+
folderFingerprint: string;
|
|
52
|
+
pid: number | null;
|
|
53
|
+
offline: boolean;
|
|
54
|
+
setupReceiptFingerprint: string;
|
|
55
|
+
setupReceiptPath: string;
|
|
56
|
+
receiptPath: string;
|
|
57
|
+
logPath: string;
|
|
58
|
+
resumeCommand: string;
|
|
59
|
+
counts: {
|
|
60
|
+
embedded: number;
|
|
61
|
+
errors: number;
|
|
62
|
+
} | null;
|
|
63
|
+
error: {
|
|
64
|
+
message: string;
|
|
65
|
+
remediation: string;
|
|
66
|
+
} | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface SpawnedSemanticWorker {
|
|
70
|
+
pid: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ScheduleSetupSemanticOptions {
|
|
74
|
+
setupReceipt: FolderSetupReceipt;
|
|
75
|
+
dataDir: string;
|
|
76
|
+
configPath: string;
|
|
77
|
+
indexName: string;
|
|
78
|
+
offline: boolean;
|
|
79
|
+
disabled?: boolean;
|
|
80
|
+
now?: () => Date;
|
|
81
|
+
spawnWorker?: (
|
|
82
|
+
receipt: SetupSemanticReceipt
|
|
83
|
+
) => Promise<SpawnedSemanticWorker>;
|
|
84
|
+
processIsAlive?: (pid: number) => boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const MAX_ERROR_LENGTH = 500;
|
|
88
|
+
const MAX_REMEDIATION_LENGTH = 8192;
|
|
89
|
+
const LOCK_TIMEOUT_MS = 5000;
|
|
90
|
+
const FINGERPRINT_PATTERN = /^[a-f0-9]{64}$/;
|
|
91
|
+
const COLLECTION_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
92
|
+
const SEMANTIC_RECEIPT_KEYS = [
|
|
93
|
+
"schemaVersion",
|
|
94
|
+
"status",
|
|
95
|
+
"generatedAt",
|
|
96
|
+
"startedAt",
|
|
97
|
+
"completedAt",
|
|
98
|
+
"jobId",
|
|
99
|
+
"collection",
|
|
100
|
+
"indexName",
|
|
101
|
+
"folderFingerprint",
|
|
102
|
+
"pid",
|
|
103
|
+
"offline",
|
|
104
|
+
"setupReceiptFingerprint",
|
|
105
|
+
"setupReceiptPath",
|
|
106
|
+
"receiptPath",
|
|
107
|
+
"logPath",
|
|
108
|
+
"resumeCommand",
|
|
109
|
+
"counts",
|
|
110
|
+
"error",
|
|
111
|
+
] as const;
|
|
112
|
+
|
|
113
|
+
function nowIso(now?: () => Date): string {
|
|
114
|
+
return (now ?? (() => new Date()))().toISOString();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function boundedMessage(value: unknown): string {
|
|
118
|
+
const message = value instanceof Error ? value.message : String(value);
|
|
119
|
+
return message.slice(0, MAX_ERROR_LENGTH) || "Unknown semantic setup error";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function quoteShellArg(value: string): string {
|
|
123
|
+
if (/^[a-zA-Z0-9_./:@+-]+$/.test(value)) {
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function buildSetupSemanticResumeCommand(input: {
|
|
130
|
+
indexName: string;
|
|
131
|
+
configPath: string;
|
|
132
|
+
offline: boolean;
|
|
133
|
+
collection: string;
|
|
134
|
+
}): string {
|
|
135
|
+
const flags = [
|
|
136
|
+
"--index",
|
|
137
|
+
quoteShellArg(input.indexName),
|
|
138
|
+
"--config",
|
|
139
|
+
quoteShellArg(input.configPath),
|
|
140
|
+
];
|
|
141
|
+
if (input.offline) {
|
|
142
|
+
flags.push("--offline");
|
|
143
|
+
}
|
|
144
|
+
return `gno ${flags.join(" ")} embed ${quoteShellArg(input.collection)}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function getSetupSemanticReceiptPath(input: {
|
|
148
|
+
dataDir: string;
|
|
149
|
+
indexName: string;
|
|
150
|
+
folderRealpath: string;
|
|
151
|
+
}): string {
|
|
152
|
+
return join(
|
|
153
|
+
input.dataDir,
|
|
154
|
+
"setup-semantic",
|
|
155
|
+
canonicalizeIndexName(input.indexName),
|
|
156
|
+
`${setupRootFingerprint(input.folderRealpath)}.json`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function serializeSetupSemanticReceipt(
|
|
161
|
+
receipt: SetupSemanticReceipt
|
|
162
|
+
): string {
|
|
163
|
+
return `${JSON.stringify(receipt, null, 2)}\n`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function persistSetupSemanticReceipt(
|
|
167
|
+
receipt: SetupSemanticReceipt
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
if (!isSetupSemanticReceipt(receipt, receipt.receiptPath)) {
|
|
170
|
+
throw new Error("Refusing to persist an invalid semantic setup receipt");
|
|
171
|
+
}
|
|
172
|
+
const receiptDir = dirname(receipt.receiptPath);
|
|
173
|
+
await mkdir(receiptDir, { recursive: true, mode: 0o700 });
|
|
174
|
+
await chmod(receiptDir, 0o700);
|
|
175
|
+
await mkdir(dirname(receipt.logPath), { recursive: true, mode: 0o700 });
|
|
176
|
+
await chmod(dirname(receipt.logPath), 0o700);
|
|
177
|
+
|
|
178
|
+
const temporaryPath = `${receipt.receiptPath}.tmp.${crypto.randomUUID()}`;
|
|
179
|
+
let temporaryFile: Awaited<ReturnType<typeof open>> | null = null;
|
|
180
|
+
try {
|
|
181
|
+
temporaryFile = await open(temporaryPath, "wx", 0o600);
|
|
182
|
+
await temporaryFile.writeFile(
|
|
183
|
+
serializeSetupSemanticReceipt(receipt),
|
|
184
|
+
"utf8"
|
|
185
|
+
);
|
|
186
|
+
await temporaryFile.sync();
|
|
187
|
+
await temporaryFile.close();
|
|
188
|
+
temporaryFile = null;
|
|
189
|
+
await rename(temporaryPath, receipt.receiptPath);
|
|
190
|
+
await chmod(receipt.receiptPath, 0o600);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
await temporaryFile?.close().catch(() => undefined);
|
|
193
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function loadSetupSemanticReceipt(
|
|
199
|
+
path: string
|
|
200
|
+
): Promise<SetupSemanticReceipt | null> {
|
|
201
|
+
const file = Bun.file(path);
|
|
202
|
+
if (!(await file.exists())) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const value: unknown = await file.json();
|
|
207
|
+
if (!isSetupSemanticReceipt(value, path)) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return value;
|
|
211
|
+
} catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isNullableString(value: unknown): value is string | null {
|
|
217
|
+
return value === null || typeof value === "string";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isIsoDate(value: string | null): boolean {
|
|
221
|
+
return value === null || Number.isFinite(Date.parse(value));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function hasExactReceiptKeys(receipt: Record<string, unknown>): boolean {
|
|
225
|
+
const expected = new Set<string>(SEMANTIC_RECEIPT_KEYS);
|
|
226
|
+
const keys = Object.keys(receipt);
|
|
227
|
+
return (
|
|
228
|
+
keys.length === SEMANTIC_RECEIPT_KEYS.length &&
|
|
229
|
+
keys.every((key) => expected.has(key))
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function hasValidStatusState(receipt: SetupSemanticReceipt): boolean {
|
|
234
|
+
switch (receipt.status) {
|
|
235
|
+
case "scheduled":
|
|
236
|
+
return (
|
|
237
|
+
receipt.startedAt === null &&
|
|
238
|
+
receipt.completedAt === null &&
|
|
239
|
+
receipt.counts === null &&
|
|
240
|
+
receipt.error === null
|
|
241
|
+
);
|
|
242
|
+
case "running":
|
|
243
|
+
return (
|
|
244
|
+
receipt.pid !== null &&
|
|
245
|
+
receipt.startedAt !== null &&
|
|
246
|
+
receipt.completedAt === null &&
|
|
247
|
+
receipt.counts === null &&
|
|
248
|
+
receipt.error === null
|
|
249
|
+
);
|
|
250
|
+
case "completed":
|
|
251
|
+
return (
|
|
252
|
+
receipt.pid === null &&
|
|
253
|
+
receipt.completedAt !== null &&
|
|
254
|
+
receipt.counts !== null &&
|
|
255
|
+
receipt.error === null
|
|
256
|
+
);
|
|
257
|
+
case "failed":
|
|
258
|
+
return (
|
|
259
|
+
receipt.pid === null &&
|
|
260
|
+
receipt.completedAt !== null &&
|
|
261
|
+
receipt.error !== null
|
|
262
|
+
);
|
|
263
|
+
case "pending":
|
|
264
|
+
return (
|
|
265
|
+
receipt.completedAt === null &&
|
|
266
|
+
receipt.counts === null &&
|
|
267
|
+
receipt.error !== null
|
|
268
|
+
);
|
|
269
|
+
case "skipped":
|
|
270
|
+
return (
|
|
271
|
+
receipt.completedAt !== null &&
|
|
272
|
+
receipt.counts === null &&
|
|
273
|
+
receipt.error === null
|
|
274
|
+
);
|
|
275
|
+
default:
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isSetupSemanticReceipt(
|
|
281
|
+
value: unknown,
|
|
282
|
+
expectedPath: string
|
|
283
|
+
): value is SetupSemanticReceipt {
|
|
284
|
+
if (typeof value !== "object" || value === null) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
const receipt = value as Record<string, unknown>;
|
|
288
|
+
const error = receipt.error;
|
|
289
|
+
const counts = receipt.counts;
|
|
290
|
+
return (
|
|
291
|
+
hasExactReceiptKeys(receipt) &&
|
|
292
|
+
receipt.schemaVersion === SETUP_SEMANTIC_SCHEMA_VERSION &&
|
|
293
|
+
SETUP_SEMANTIC_STATUSES.includes(receipt.status as SetupSemanticStatus) &&
|
|
294
|
+
typeof receipt.generatedAt === "string" &&
|
|
295
|
+
isIsoDate(receipt.generatedAt) &&
|
|
296
|
+
isNullableString(receipt.startedAt) &&
|
|
297
|
+
isIsoDate(receipt.startedAt) &&
|
|
298
|
+
isNullableString(receipt.completedAt) &&
|
|
299
|
+
isIsoDate(receipt.completedAt) &&
|
|
300
|
+
typeof receipt.jobId === "string" &&
|
|
301
|
+
FINGERPRINT_PATTERN.test(receipt.jobId) &&
|
|
302
|
+
typeof receipt.collection === "string" &&
|
|
303
|
+
COLLECTION_PATTERN.test(receipt.collection) &&
|
|
304
|
+
typeof receipt.indexName === "string" &&
|
|
305
|
+
receipt.indexName.length > 0 &&
|
|
306
|
+
receipt.indexName.length <= 64 &&
|
|
307
|
+
typeof receipt.folderFingerprint === "string" &&
|
|
308
|
+
FINGERPRINT_PATTERN.test(receipt.folderFingerprint) &&
|
|
309
|
+
(receipt.pid === null ||
|
|
310
|
+
(typeof receipt.pid === "number" &&
|
|
311
|
+
Number.isInteger(receipt.pid) &&
|
|
312
|
+
receipt.pid > 0)) &&
|
|
313
|
+
typeof receipt.offline === "boolean" &&
|
|
314
|
+
typeof receipt.setupReceiptFingerprint === "string" &&
|
|
315
|
+
FINGERPRINT_PATTERN.test(receipt.setupReceiptFingerprint) &&
|
|
316
|
+
typeof receipt.setupReceiptPath === "string" &&
|
|
317
|
+
receipt.setupReceiptPath.length > 0 &&
|
|
318
|
+
receipt.receiptPath === expectedPath &&
|
|
319
|
+
typeof receipt.logPath === "string" &&
|
|
320
|
+
receipt.logPath.length > 0 &&
|
|
321
|
+
typeof receipt.resumeCommand === "string" &&
|
|
322
|
+
receipt.resumeCommand.startsWith("gno ") &&
|
|
323
|
+
receipt.resumeCommand.includes(" embed ") &&
|
|
324
|
+
(counts === null ||
|
|
325
|
+
(typeof counts === "object" &&
|
|
326
|
+
counts !== null &&
|
|
327
|
+
Object.keys(counts).length === 2 &&
|
|
328
|
+
Number.isInteger((counts as Record<string, unknown>).embedded) &&
|
|
329
|
+
Number((counts as Record<string, unknown>).embedded) >= 0 &&
|
|
330
|
+
Number.isInteger((counts as Record<string, unknown>).errors) &&
|
|
331
|
+
Number((counts as Record<string, unknown>).errors) >= 0)) &&
|
|
332
|
+
(error === null ||
|
|
333
|
+
(typeof error === "object" &&
|
|
334
|
+
error !== null &&
|
|
335
|
+
Object.keys(error).length === 2 &&
|
|
336
|
+
typeof (error as Record<string, unknown>).message === "string" &&
|
|
337
|
+
((error as Record<string, unknown>).message as string).length > 0 &&
|
|
338
|
+
((error as Record<string, unknown>).message as string).length <=
|
|
339
|
+
MAX_ERROR_LENGTH &&
|
|
340
|
+
typeof (error as Record<string, unknown>).remediation === "string" &&
|
|
341
|
+
((error as Record<string, unknown>).remediation as string).length > 0 &&
|
|
342
|
+
((error as Record<string, unknown>).remediation as string).length <=
|
|
343
|
+
MAX_REMEDIATION_LENGTH)) &&
|
|
344
|
+
hasValidStatusState(receipt as unknown as SetupSemanticReceipt)
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function setupSemanticSourceFingerprint(
|
|
349
|
+
receipt: FolderSetupReceipt
|
|
350
|
+
): string {
|
|
351
|
+
return setupFingerprint({
|
|
352
|
+
schemaVersion: receipt.schemaVersion,
|
|
353
|
+
status: receipt.status,
|
|
354
|
+
input: receipt.input,
|
|
355
|
+
fingerprints: receipt.fingerprints,
|
|
356
|
+
collection: {
|
|
357
|
+
name: receipt.collection.name,
|
|
358
|
+
path: receipt.collection.path,
|
|
359
|
+
},
|
|
360
|
+
paths: receipt.paths,
|
|
361
|
+
activation: receipt.activation
|
|
362
|
+
? {
|
|
363
|
+
collection: receipt.activation.collection,
|
|
364
|
+
fingerprint: receipt.activation.fingerprint,
|
|
365
|
+
ready: receipt.activation.ready,
|
|
366
|
+
evidence: receipt.activation.evidence,
|
|
367
|
+
}
|
|
368
|
+
: null,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function createSemanticReceipt(
|
|
373
|
+
options: ScheduleSetupSemanticOptions,
|
|
374
|
+
status: SetupSemanticStatus
|
|
375
|
+
): SetupSemanticReceipt {
|
|
376
|
+
const setupReceipt = options.setupReceipt;
|
|
377
|
+
const collection = setupReceipt.collection.name;
|
|
378
|
+
if (!collection) {
|
|
379
|
+
throw new Error("Completed setup receipt has no collection");
|
|
380
|
+
}
|
|
381
|
+
const indexName = canonicalizeIndexName(options.indexName);
|
|
382
|
+
const receiptPath = getSetupSemanticReceiptPath({
|
|
383
|
+
dataDir: options.dataDir,
|
|
384
|
+
indexName,
|
|
385
|
+
folderRealpath: setupReceipt.input.folder,
|
|
386
|
+
});
|
|
387
|
+
const generatedAt = nowIso(options.now);
|
|
388
|
+
const setupReceiptFingerprint = setupSemanticSourceFingerprint(setupReceipt);
|
|
389
|
+
return {
|
|
390
|
+
schemaVersion: SETUP_SEMANTIC_SCHEMA_VERSION,
|
|
391
|
+
status,
|
|
392
|
+
generatedAt,
|
|
393
|
+
startedAt: null,
|
|
394
|
+
completedAt: status === "skipped" ? generatedAt : null,
|
|
395
|
+
jobId: setupFingerprint({
|
|
396
|
+
setupReceiptFingerprint,
|
|
397
|
+
packageVersion: VERSION,
|
|
398
|
+
indexName,
|
|
399
|
+
configPath: options.configPath,
|
|
400
|
+
offline: options.offline,
|
|
401
|
+
}),
|
|
402
|
+
collection,
|
|
403
|
+
indexName,
|
|
404
|
+
folderFingerprint: setupReceipt.input.folderFingerprint,
|
|
405
|
+
pid: null,
|
|
406
|
+
offline: options.offline,
|
|
407
|
+
setupReceiptFingerprint,
|
|
408
|
+
setupReceiptPath: setupReceipt.paths.receipt,
|
|
409
|
+
receiptPath,
|
|
410
|
+
logPath: join(
|
|
411
|
+
options.dataDir,
|
|
412
|
+
"setup-semantic",
|
|
413
|
+
indexName,
|
|
414
|
+
`${setupReceipt.input.folderFingerprint}.log`
|
|
415
|
+
),
|
|
416
|
+
resumeCommand: buildSetupSemanticResumeCommand({
|
|
417
|
+
indexName,
|
|
418
|
+
configPath: options.configPath,
|
|
419
|
+
offline: options.offline,
|
|
420
|
+
collection,
|
|
421
|
+
}),
|
|
422
|
+
counts: null,
|
|
423
|
+
error: null,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function defaultSpawnWorker(
|
|
428
|
+
receipt: SetupSemanticReceipt
|
|
429
|
+
): Promise<SpawnedSemanticWorker> {
|
|
430
|
+
const workerPath = join(import.meta.dir, "..", "setup-semantic-worker.ts");
|
|
431
|
+
const descriptor = openSync(receipt.logPath, "a", 0o600);
|
|
432
|
+
try {
|
|
433
|
+
const child = Bun.spawn({
|
|
434
|
+
cmd: [process.execPath, workerPath, receipt.receiptPath, receipt.jobId],
|
|
435
|
+
stdio: ["ignore", descriptor, descriptor],
|
|
436
|
+
detached: true,
|
|
437
|
+
env: process.env,
|
|
438
|
+
});
|
|
439
|
+
child.unref();
|
|
440
|
+
return { pid: child.pid };
|
|
441
|
+
} finally {
|
|
442
|
+
closeSync(descriptor);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function existingReceiptMatches(
|
|
447
|
+
existing: SetupSemanticReceipt,
|
|
448
|
+
expected: SetupSemanticReceipt
|
|
449
|
+
): boolean {
|
|
450
|
+
return (
|
|
451
|
+
existing.jobId === expected.jobId &&
|
|
452
|
+
existing.collection === expected.collection &&
|
|
453
|
+
existing.indexName === expected.indexName &&
|
|
454
|
+
existing.setupReceiptFingerprint === expected.setupReceiptFingerprint &&
|
|
455
|
+
existing.setupReceiptPath === expected.setupReceiptPath &&
|
|
456
|
+
existing.offline === expected.offline
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Schedule one collection-scoped semantic worker without waiting for model
|
|
462
|
+
* download or embedding.
|
|
463
|
+
*/
|
|
464
|
+
export async function scheduleSetupSemantic(
|
|
465
|
+
options: ScheduleSetupSemanticOptions
|
|
466
|
+
): Promise<SetupSemanticReceipt> {
|
|
467
|
+
const initial = createSemanticReceipt(
|
|
468
|
+
options,
|
|
469
|
+
options.disabled ? "skipped" : "scheduled"
|
|
470
|
+
);
|
|
471
|
+
const lockPath = `${initial.receiptPath}.lock`;
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
return await withWriteLock(
|
|
475
|
+
lockPath,
|
|
476
|
+
async () => {
|
|
477
|
+
const existing = await loadSetupSemanticReceipt(initial.receiptPath);
|
|
478
|
+
const processAlive = options.processIsAlive ?? isProcessAlive;
|
|
479
|
+
if (options.disabled) {
|
|
480
|
+
const existingIsLive =
|
|
481
|
+
existing !== null &&
|
|
482
|
+
(existing.status === "scheduled" ||
|
|
483
|
+
existing.status === "running" ||
|
|
484
|
+
existing.status === "pending" ||
|
|
485
|
+
existing.status === "skipped") &&
|
|
486
|
+
existing.pid !== null &&
|
|
487
|
+
processAlive(existing.pid);
|
|
488
|
+
const skippedBase = existingIsLive ? existing : initial;
|
|
489
|
+
const generatedAt = nowIso(options.now);
|
|
490
|
+
const skipped: SetupSemanticReceipt = {
|
|
491
|
+
...skippedBase,
|
|
492
|
+
status: "skipped",
|
|
493
|
+
generatedAt,
|
|
494
|
+
completedAt: generatedAt,
|
|
495
|
+
pid: existingIsLive ? existing.pid : null,
|
|
496
|
+
counts: null,
|
|
497
|
+
error: null,
|
|
498
|
+
};
|
|
499
|
+
await persistSetupSemanticReceipt(skipped);
|
|
500
|
+
return skipped;
|
|
501
|
+
}
|
|
502
|
+
if (existing && existingReceiptMatches(existing, initial)) {
|
|
503
|
+
if (existing.status === "completed") {
|
|
504
|
+
return existing;
|
|
505
|
+
}
|
|
506
|
+
if (
|
|
507
|
+
(existing.status === "scheduled" ||
|
|
508
|
+
existing.status === "running" ||
|
|
509
|
+
existing.status === "pending" ||
|
|
510
|
+
existing.status === "skipped") &&
|
|
511
|
+
existing.pid !== null &&
|
|
512
|
+
processAlive(existing.pid)
|
|
513
|
+
) {
|
|
514
|
+
return existing;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (
|
|
518
|
+
existing &&
|
|
519
|
+
!existingReceiptMatches(existing, initial) &&
|
|
520
|
+
(existing.status === "scheduled" ||
|
|
521
|
+
existing.status === "running" ||
|
|
522
|
+
existing.status === "pending" ||
|
|
523
|
+
existing.status === "skipped") &&
|
|
524
|
+
existing.pid !== null &&
|
|
525
|
+
processAlive(existing.pid)
|
|
526
|
+
) {
|
|
527
|
+
// The active worker owns this receipt identity. Replacing it would
|
|
528
|
+
// make its final update fail the jobId check and strand the durable
|
|
529
|
+
// state. Preserve it; a later setup rerun can schedule the new
|
|
530
|
+
// identity after this process exits.
|
|
531
|
+
return existing;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
await persistSetupSemanticReceipt(initial);
|
|
535
|
+
try {
|
|
536
|
+
const spawned = await (options.spawnWorker ?? defaultSpawnWorker)(
|
|
537
|
+
initial
|
|
538
|
+
);
|
|
539
|
+
const scheduled: SetupSemanticReceipt = {
|
|
540
|
+
...initial,
|
|
541
|
+
pid: spawned.pid,
|
|
542
|
+
generatedAt: nowIso(options.now),
|
|
543
|
+
};
|
|
544
|
+
await persistSetupSemanticReceipt(scheduled);
|
|
545
|
+
return scheduled;
|
|
546
|
+
} catch (error) {
|
|
547
|
+
const pending: SetupSemanticReceipt = {
|
|
548
|
+
...initial,
|
|
549
|
+
status: "pending",
|
|
550
|
+
generatedAt: nowIso(options.now),
|
|
551
|
+
error: {
|
|
552
|
+
message: boundedMessage(error),
|
|
553
|
+
remediation: `Run: ${initial.resumeCommand}`,
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
await persistSetupSemanticReceipt(pending);
|
|
557
|
+
return pending;
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
LOCK_TIMEOUT_MS
|
|
561
|
+
);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
return {
|
|
564
|
+
...initial,
|
|
565
|
+
status: "pending",
|
|
566
|
+
generatedAt: nowIso(options.now),
|
|
567
|
+
error: {
|
|
568
|
+
message: boundedMessage(error),
|
|
569
|
+
remediation: `Run: ${initial.resumeCommand}`,
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export async function updateSetupSemanticReceipt(
|
|
576
|
+
receiptPath: string,
|
|
577
|
+
jobId: string,
|
|
578
|
+
update: (
|
|
579
|
+
receipt: SetupSemanticReceipt
|
|
580
|
+
) => SetupSemanticReceipt | Promise<SetupSemanticReceipt>
|
|
581
|
+
): Promise<SetupSemanticReceipt> {
|
|
582
|
+
return withWriteLock(`${receiptPath}.lock`, async () => {
|
|
583
|
+
const receipt = await loadSetupSemanticReceipt(receiptPath);
|
|
584
|
+
if (!receipt || receipt.jobId !== jobId) {
|
|
585
|
+
throw new Error("Semantic setup receipt identity changed");
|
|
586
|
+
}
|
|
587
|
+
const next = await update(receipt);
|
|
588
|
+
await persistSetupSemanticReceipt(next);
|
|
589
|
+
return next;
|
|
590
|
+
});
|
|
591
|
+
}
|