@remnic/core 9.3.573 → 9.3.574
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/chunk-KDUFBSBF.js +532 -0
- package/dist/chunk-KDUFBSBF.js.map +1 -0
- package/dist/{chunk-2FHLI4U6.js → chunk-QNXFFUWA.js} +2 -2
- package/dist/cli.js +2 -2
- package/dist/index.js +2 -2
- package/dist/schemas.d.ts +22 -22
- package/dist/semantic-rule-promotion.d.ts +12 -1
- package/dist/semantic-rule-promotion.js +5 -3
- package/dist/transfer/types.d.ts +12 -12
- package/package.json +1 -1
- package/src/semantic-rule-promotion.ts +502 -37
- package/dist/chunk-74VA26CT.js +0 -135
- package/dist/chunk-74VA26CT.js.map +0 -1
- /package/dist/{chunk-2FHLI4U6.js.map → chunk-QNXFFUWA.js.map} +0 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { link, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
1
5
|
import { StorageManager } from "./storage.js";
|
|
2
6
|
import type { MemoryFile, MemoryLink } from "./types.js";
|
|
3
7
|
|
|
@@ -30,6 +34,465 @@ export interface SemanticRulePromotionReport {
|
|
|
30
34
|
skipped: SemanticRulePromotionSkip[];
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
const PROMOTION_LOCK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
38
|
+
const PROMOTION_LOCK_RETRY_MS = 25;
|
|
39
|
+
const PROMOTION_LOCK_STALE_MS = 5 * 60 * 1000;
|
|
40
|
+
const PROMOTION_LOCK_OWNERLESS_GRACE_MS = 1000;
|
|
41
|
+
const PROMOTION_LOCK_HEARTBEAT_MS = 30 * 1000;
|
|
42
|
+
|
|
43
|
+
type SemanticRulePromotionTestHooks = {
|
|
44
|
+
beforeLockOwnerWrite?: (lockDir: string, ownerToken: string) => Promise<void> | void;
|
|
45
|
+
beforeLockRelease?: (lockDir: string, ownerToken: string) => Promise<void> | void;
|
|
46
|
+
beforePromotedRuleWrite?: (lockDir: string, ownerToken: string) => Promise<void> | void;
|
|
47
|
+
lockTimeoutMs?: number;
|
|
48
|
+
lockRetryMs?: number;
|
|
49
|
+
staleLockMs?: number;
|
|
50
|
+
ownerlessGraceMs?: number;
|
|
51
|
+
heartbeatMs?: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let testHooks: SemanticRulePromotionTestHooks | null = null;
|
|
55
|
+
|
|
56
|
+
export function setSemanticRulePromotionTestHooks(hooks: SemanticRulePromotionTestHooks | null): void {
|
|
57
|
+
testHooks = hooks;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function promotionLockTimeoutMs(): number {
|
|
61
|
+
return testHooks?.lockTimeoutMs ?? PROMOTION_LOCK_TIMEOUT_MS;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function promotionLockRetryMs(): number {
|
|
65
|
+
return testHooks?.lockRetryMs ?? PROMOTION_LOCK_RETRY_MS;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function promotionLockStaleMs(): number {
|
|
69
|
+
return testHooks?.staleLockMs ?? PROMOTION_LOCK_STALE_MS;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function promotionLockOwnerlessGraceMs(): number {
|
|
73
|
+
return testHooks?.ownerlessGraceMs ?? PROMOTION_LOCK_OWNERLESS_GRACE_MS;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function promotionLockHeartbeatMs(): number {
|
|
77
|
+
return testHooks?.heartbeatMs ?? PROMOTION_LOCK_HEARTBEAT_MS;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isErrnoCode(err: unknown, code: string): boolean {
|
|
81
|
+
return typeof err === "object" && err !== null && "code" in err && (err as { code?: unknown }).code === code;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function promotionLockDir(memoryDir: string, ruleKey: string): string {
|
|
85
|
+
const digest = createHash("sha256").update(ruleKey).digest("hex");
|
|
86
|
+
return path.join(path.resolve(memoryDir), "state", "semantic-rule-promotion-locks", `${digest}.lock`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function promotionHeartbeatPath(lockDir: string, token: string): string {
|
|
90
|
+
return path.join(lockDir, `heartbeat.${token}.json`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function promotionReapDir(lockDir: string): string {
|
|
94
|
+
return `${lockDir}.reap`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function promotionReleaseDir(lockDir: string): string {
|
|
98
|
+
return `${lockDir}.release`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function pathExists(targetPath: string): Promise<boolean> {
|
|
102
|
+
try {
|
|
103
|
+
await stat(targetPath);
|
|
104
|
+
return true;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (isErrnoCode(err, "ENOENT")) return false;
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function readPromotionLockOwner(lockDir: string): Promise<{
|
|
112
|
+
acquiredAtMs: number | null;
|
|
113
|
+
heartbeatAtMs: number | null;
|
|
114
|
+
token: string | null;
|
|
115
|
+
}> {
|
|
116
|
+
try {
|
|
117
|
+
const owner = JSON.parse(await readFile(path.join(lockDir, "owner.json"), "utf8")) as {
|
|
118
|
+
acquiredAt?: unknown;
|
|
119
|
+
heartbeatAt?: unknown;
|
|
120
|
+
token?: unknown;
|
|
121
|
+
};
|
|
122
|
+
const acquiredAtMs = typeof owner.acquiredAt === "string" ? Date.parse(owner.acquiredAt) : Number.NaN;
|
|
123
|
+
const heartbeatAtMs = typeof owner.heartbeatAt === "string" ? Date.parse(owner.heartbeatAt) : Number.NaN;
|
|
124
|
+
return {
|
|
125
|
+
acquiredAtMs: Number.isFinite(acquiredAtMs) ? acquiredAtMs : null,
|
|
126
|
+
heartbeatAtMs: Number.isFinite(heartbeatAtMs) ? heartbeatAtMs : null,
|
|
127
|
+
token: typeof owner.token === "string" ? owner.token : null,
|
|
128
|
+
};
|
|
129
|
+
} catch {
|
|
130
|
+
return {
|
|
131
|
+
acquiredAtMs: null,
|
|
132
|
+
heartbeatAtMs: null,
|
|
133
|
+
token: null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function tryWritePromotionLockOwner(
|
|
139
|
+
lockDir: string,
|
|
140
|
+
owner: { token: string; acquiredAt: string }
|
|
141
|
+
): Promise<boolean> {
|
|
142
|
+
const ownerPath = path.join(lockDir, "owner.json");
|
|
143
|
+
const tempPath = path.join(lockDir, `owner.${process.pid}.${randomUUID()}.tmp`);
|
|
144
|
+
await testHooks?.beforeLockOwnerWrite?.(lockDir, owner.token);
|
|
145
|
+
try {
|
|
146
|
+
await writeFile(
|
|
147
|
+
tempPath,
|
|
148
|
+
`${JSON.stringify(
|
|
149
|
+
{
|
|
150
|
+
pid: process.pid,
|
|
151
|
+
token: owner.token,
|
|
152
|
+
acquiredAt: owner.acquiredAt,
|
|
153
|
+
},
|
|
154
|
+
null,
|
|
155
|
+
2
|
|
156
|
+
)}\n`
|
|
157
|
+
);
|
|
158
|
+
await link(tempPath, ownerPath);
|
|
159
|
+
return true;
|
|
160
|
+
} catch (err) {
|
|
161
|
+
if (isErrnoCode(err, "EEXIST") || isErrnoCode(err, "ENOENT")) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
throw err;
|
|
165
|
+
} finally {
|
|
166
|
+
await rm(tempPath, { force: true });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function writePromotionLockHeartbeat(lockDir: string, token: string): Promise<void> {
|
|
171
|
+
const heartbeatPath = promotionHeartbeatPath(lockDir, token);
|
|
172
|
+
const tempPath = path.join(lockDir, `heartbeat.${process.pid}.${randomUUID()}.tmp`);
|
|
173
|
+
try {
|
|
174
|
+
await writeFile(
|
|
175
|
+
tempPath,
|
|
176
|
+
`${JSON.stringify(
|
|
177
|
+
{
|
|
178
|
+
pid: process.pid,
|
|
179
|
+
token,
|
|
180
|
+
heartbeatAt: new Date().toISOString(),
|
|
181
|
+
},
|
|
182
|
+
null,
|
|
183
|
+
2
|
|
184
|
+
)}\n`
|
|
185
|
+
);
|
|
186
|
+
await rename(tempPath, heartbeatPath);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
await rm(tempPath, { force: true });
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function refreshPromotionLockHeartbeat(lockDir: string, token: string): Promise<boolean> {
|
|
194
|
+
const owner = await readPromotionLockOwner(lockDir);
|
|
195
|
+
if (owner.token !== token) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
await writePromotionLockHeartbeat(lockDir, token);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function readPromotionLockHeartbeatMs(lockDir: string, token: string | null): Promise<number | null> {
|
|
203
|
+
if (!token) return null;
|
|
204
|
+
try {
|
|
205
|
+
return (await stat(promotionHeartbeatPath(lockDir, token))).mtimeMs;
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (isErrnoCode(err, "ENOENT")) return null;
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function reapAbandonedPromotionGuard(guardDir: string): Promise<boolean> {
|
|
213
|
+
let reapStat: { mtimeMs: number };
|
|
214
|
+
try {
|
|
215
|
+
reapStat = await stat(guardDir);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
if (isErrnoCode(err, "ENOENT")) return false;
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
if (Date.now() - reapStat.mtimeMs < promotionLockStaleMs()) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const staleReapDir = `${guardDir}.stale-${process.pid}-${randomUUID()}`;
|
|
225
|
+
try {
|
|
226
|
+
await rename(guardDir, staleReapDir);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
if (isErrnoCode(err, "ENOENT")) return false;
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
await rm(staleReapDir, { recursive: true, force: true });
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function reapAbandonedPromotionGuards(lockDir: string): Promise<boolean> {
|
|
236
|
+
const reapedReapGuard = await reapAbandonedPromotionGuard(promotionReapDir(lockDir));
|
|
237
|
+
const reapedReleaseGuard = await reapAbandonedPromotionGuard(promotionReleaseDir(lockDir));
|
|
238
|
+
return reapedReapGuard || reapedReleaseGuard;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function hasPromotionGuard(lockDir: string): Promise<boolean> {
|
|
242
|
+
return (await pathExists(promotionReapDir(lockDir))) || (await pathExists(promotionReleaseDir(lockDir)));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function reapStalePromotionLock(lockDir: string): Promise<boolean> {
|
|
246
|
+
if (await hasPromotionGuard(lockDir)) {
|
|
247
|
+
await reapAbandonedPromotionGuards(lockDir);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const reapDir = promotionReapDir(lockDir);
|
|
252
|
+
try {
|
|
253
|
+
await mkdir(reapDir);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if (isErrnoCode(err, "EEXIST")) return false;
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let staleDir: string | null = null;
|
|
260
|
+
try {
|
|
261
|
+
return await reapStalePromotionLockWithGuard(lockDir, (value) => {
|
|
262
|
+
staleDir = value;
|
|
263
|
+
});
|
|
264
|
+
} finally {
|
|
265
|
+
if (staleDir) {
|
|
266
|
+
await rm(staleDir, { recursive: true, force: true });
|
|
267
|
+
}
|
|
268
|
+
await rm(reapDir, { recursive: true, force: true });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function reapStalePromotionLockWithGuard(
|
|
273
|
+
lockDir: string,
|
|
274
|
+
setStaleDir: (staleDir: string) => void
|
|
275
|
+
): Promise<boolean> {
|
|
276
|
+
let lockStat: { mtimeMs: number };
|
|
277
|
+
try {
|
|
278
|
+
lockStat = await stat(lockDir);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
if (isErrnoCode(err, "ENOENT")) return false;
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const owner = await readPromotionLockOwner(lockDir);
|
|
285
|
+
const heartbeatMs = await readPromotionLockHeartbeatMs(lockDir, owner.token);
|
|
286
|
+
const lockedAtMs = heartbeatMs ?? owner.heartbeatAtMs ?? owner.acquiredAtMs ?? lockStat.mtimeMs;
|
|
287
|
+
const ownerlessLock = owner.token === null && owner.heartbeatAtMs === null && owner.acquiredAtMs === null;
|
|
288
|
+
const staleAfterMs = ownerlessLock ? promotionLockOwnerlessGraceMs() : promotionLockStaleMs();
|
|
289
|
+
if (Date.now() - lockedAtMs < staleAfterMs) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const staleDir = `${lockDir}.stale-${process.pid}-${randomUUID()}`;
|
|
294
|
+
try {
|
|
295
|
+
await rename(lockDir, staleDir);
|
|
296
|
+
setStaleDir(staleDir);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
if (isErrnoCode(err, "ENOENT")) return false;
|
|
299
|
+
throw err;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const staleOwner = await readPromotionLockOwner(staleDir);
|
|
303
|
+
const staleHeartbeatMs = await readPromotionLockHeartbeatMs(staleDir, staleOwner.token);
|
|
304
|
+
const staleLockedAtMs = staleHeartbeatMs ?? staleOwner.heartbeatAtMs ?? staleOwner.acquiredAtMs ?? lockStat.mtimeMs;
|
|
305
|
+
const staleOwnerlessLock =
|
|
306
|
+
staleOwner.token === null && staleOwner.heartbeatAtMs === null && staleOwner.acquiredAtMs === null;
|
|
307
|
+
const staleAfterRecheckMs = staleOwnerlessLock ? promotionLockOwnerlessGraceMs() : promotionLockStaleMs();
|
|
308
|
+
if (Date.now() - staleLockedAtMs < staleAfterRecheckMs) {
|
|
309
|
+
await rename(staleDir, lockDir);
|
|
310
|
+
setStaleDir("");
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
await rm(staleDir, { recursive: true, force: true });
|
|
315
|
+
setStaleDir("");
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function releasePromotionLock(lockDir: string, token: string): Promise<void> {
|
|
320
|
+
const releaseDir = promotionReleaseDir(lockDir);
|
|
321
|
+
const deadline = Date.now() + promotionLockTimeoutMs();
|
|
322
|
+
await testHooks?.beforeLockRelease?.(lockDir, token);
|
|
323
|
+
for (;;) {
|
|
324
|
+
try {
|
|
325
|
+
await mkdir(releaseDir);
|
|
326
|
+
break;
|
|
327
|
+
} catch (err) {
|
|
328
|
+
if (!isErrnoCode(err, "EEXIST")) {
|
|
329
|
+
throw err;
|
|
330
|
+
}
|
|
331
|
+
const owner = await readPromotionLockOwner(lockDir);
|
|
332
|
+
if (owner.token !== token) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
await reapAbandonedPromotionGuard(releaseDir);
|
|
336
|
+
if (Date.now() >= deadline) {
|
|
337
|
+
throw new Error("Timed out releasing semantic rule promotion lock");
|
|
338
|
+
}
|
|
339
|
+
await sleep(promotionLockRetryMs());
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const owner = await readPromotionLockOwner(lockDir);
|
|
345
|
+
if (owner.token !== token) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
349
|
+
} finally {
|
|
350
|
+
await rm(releaseDir, { recursive: true, force: true });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
type PromotionLockLease = {
|
|
355
|
+
assertHeld: () => Promise<void>;
|
|
356
|
+
beforePromotedRuleWrite: () => Promise<void>;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
async function withPromotionLock<T>(
|
|
360
|
+
memoryDir: string,
|
|
361
|
+
ruleKey: string,
|
|
362
|
+
fn: (lease: PromotionLockLease) => Promise<T>
|
|
363
|
+
): Promise<T> {
|
|
364
|
+
const lockDir = promotionLockDir(memoryDir, ruleKey);
|
|
365
|
+
const lockRoot = path.dirname(lockDir);
|
|
366
|
+
const deadline = Date.now() + promotionLockTimeoutMs();
|
|
367
|
+
let lockToken: string | null = null;
|
|
368
|
+
let heartbeat: NodeJS.Timeout | null = null;
|
|
369
|
+
|
|
370
|
+
await mkdir(lockRoot, { recursive: true });
|
|
371
|
+
for (;;) {
|
|
372
|
+
try {
|
|
373
|
+
if (await hasPromotionGuard(lockDir)) {
|
|
374
|
+
await reapAbandonedPromotionGuards(lockDir);
|
|
375
|
+
if (Date.now() >= deadline) {
|
|
376
|
+
throw new Error("Timed out acquiring semantic rule promotion lock");
|
|
377
|
+
}
|
|
378
|
+
await sleep(promotionLockRetryMs());
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
await mkdir(lockDir);
|
|
382
|
+
if (await hasPromotionGuard(lockDir)) {
|
|
383
|
+
await reapAbandonedPromotionGuards(lockDir);
|
|
384
|
+
if (Date.now() >= deadline) {
|
|
385
|
+
throw new Error("Timed out acquiring semantic rule promotion lock");
|
|
386
|
+
}
|
|
387
|
+
await sleep(promotionLockRetryMs());
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
lockToken = randomUUID();
|
|
392
|
+
const lockOwner = {
|
|
393
|
+
token: lockToken,
|
|
394
|
+
acquiredAt: new Date().toISOString(),
|
|
395
|
+
};
|
|
396
|
+
const wroteOwner = await tryWritePromotionLockOwner(lockDir, lockOwner);
|
|
397
|
+
if (!wroteOwner) {
|
|
398
|
+
lockToken = null;
|
|
399
|
+
if (Date.now() >= deadline) {
|
|
400
|
+
throw new Error("Timed out acquiring semantic rule promotion lock");
|
|
401
|
+
}
|
|
402
|
+
await sleep(promotionLockRetryMs());
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
await writePromotionLockHeartbeat(lockDir, lockToken);
|
|
406
|
+
const heartbeatToken = lockToken;
|
|
407
|
+
const heartbeatMs = promotionLockHeartbeatMs();
|
|
408
|
+
if (heartbeatMs > 0) {
|
|
409
|
+
heartbeat = setInterval(() => {
|
|
410
|
+
void refreshPromotionLockHeartbeat(lockDir, heartbeatToken)
|
|
411
|
+
.then((ownsLock) => {
|
|
412
|
+
if (!ownsLock && heartbeat) {
|
|
413
|
+
clearInterval(heartbeat);
|
|
414
|
+
heartbeat = null;
|
|
415
|
+
}
|
|
416
|
+
})
|
|
417
|
+
.catch(() => undefined);
|
|
418
|
+
}, heartbeatMs);
|
|
419
|
+
}
|
|
420
|
+
} catch (err) {
|
|
421
|
+
lockToken = null;
|
|
422
|
+
if (heartbeat) {
|
|
423
|
+
clearInterval(heartbeat);
|
|
424
|
+
heartbeat = null;
|
|
425
|
+
}
|
|
426
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
427
|
+
throw err;
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
} catch (err) {
|
|
431
|
+
if (!isErrnoCode(err, "EEXIST")) {
|
|
432
|
+
throw err;
|
|
433
|
+
}
|
|
434
|
+
if (await reapStalePromotionLock(lockDir)) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (Date.now() >= deadline) {
|
|
438
|
+
throw new Error("Timed out acquiring semantic rule promotion lock");
|
|
439
|
+
}
|
|
440
|
+
await sleep(promotionLockRetryMs());
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const assertHeld = async () => {
|
|
445
|
+
if (!lockToken) {
|
|
446
|
+
throw new Error("Semantic rule promotion lock is no longer held");
|
|
447
|
+
}
|
|
448
|
+
const ownsLock = await refreshPromotionLockHeartbeat(lockDir, lockToken);
|
|
449
|
+
if (!ownsLock) {
|
|
450
|
+
throw new Error("Semantic rule promotion lock is no longer held");
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
let hasResult = false;
|
|
455
|
+
let result: T | undefined;
|
|
456
|
+
let callbackError: unknown;
|
|
457
|
+
let releaseError: unknown;
|
|
458
|
+
try {
|
|
459
|
+
await assertHeld();
|
|
460
|
+
result = await fn({
|
|
461
|
+
assertHeld,
|
|
462
|
+
beforePromotedRuleWrite: async () => {
|
|
463
|
+
if (!lockToken) {
|
|
464
|
+
throw new Error("Semantic rule promotion lock is no longer held");
|
|
465
|
+
}
|
|
466
|
+
await testHooks?.beforePromotedRuleWrite?.(lockDir, lockToken);
|
|
467
|
+
await assertHeld();
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
hasResult = true;
|
|
471
|
+
} catch (err) {
|
|
472
|
+
callbackError = err;
|
|
473
|
+
} finally {
|
|
474
|
+
if (heartbeat) {
|
|
475
|
+
clearInterval(heartbeat);
|
|
476
|
+
}
|
|
477
|
+
if (lockToken) {
|
|
478
|
+
try {
|
|
479
|
+
await releasePromotionLock(lockDir, lockToken);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
if (!hasResult && !callbackError) {
|
|
482
|
+
releaseError = err;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (releaseError) {
|
|
488
|
+
throw releaseError;
|
|
489
|
+
}
|
|
490
|
+
if (callbackError) {
|
|
491
|
+
throw callbackError;
|
|
492
|
+
}
|
|
493
|
+
return result as T;
|
|
494
|
+
}
|
|
495
|
+
|
|
33
496
|
function normalizeRuleWhitespace(value: string): string {
|
|
34
497
|
return value.replace(/\s+/g, " ").trim();
|
|
35
498
|
}
|
|
@@ -111,10 +574,7 @@ export async function promoteSemanticRuleFromMemory(options: {
|
|
|
111
574
|
});
|
|
112
575
|
return report;
|
|
113
576
|
}
|
|
114
|
-
if (
|
|
115
|
-
sourceMemory.frontmatter.status === "archived" ||
|
|
116
|
-
sourceMemory.frontmatter.memoryKind !== "episode"
|
|
117
|
-
) {
|
|
577
|
+
if (sourceMemory.frontmatter.status === "archived" || sourceMemory.frontmatter.memoryKind !== "episode") {
|
|
118
578
|
report.skipped.push({
|
|
119
579
|
sourceMemoryId: options.sourceMemoryId,
|
|
120
580
|
reason: "source-memory-not-episode",
|
|
@@ -132,22 +592,6 @@ export async function promoteSemanticRuleFromMemory(options: {
|
|
|
132
592
|
}
|
|
133
593
|
|
|
134
594
|
const ruleKey = canonicalizeRuleKey(content);
|
|
135
|
-
const existingRule = (await storage.readAllMemories()).find(
|
|
136
|
-
(memory) =>
|
|
137
|
-
memory.frontmatter.category === "rule" &&
|
|
138
|
-
memory.frontmatter.status !== "archived" &&
|
|
139
|
-
memory.frontmatter.status !== "forgotten" &&
|
|
140
|
-
canonicalizeRuleKey(memory.content) === ruleKey,
|
|
141
|
-
);
|
|
142
|
-
if (existingRule) {
|
|
143
|
-
report.skipped.push({
|
|
144
|
-
sourceMemoryId: options.sourceMemoryId,
|
|
145
|
-
reason: "duplicate-rule",
|
|
146
|
-
existingRuleId: existingRule.frontmatter.id,
|
|
147
|
-
});
|
|
148
|
-
return report;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
595
|
const confidence = promotionConfidence(sourceMemory);
|
|
152
596
|
const candidateBase = {
|
|
153
597
|
sourceMemoryId: options.sourceMemoryId,
|
|
@@ -158,26 +602,47 @@ export async function promoteSemanticRuleFromMemory(options: {
|
|
|
158
602
|
lineage: [options.sourceMemoryId],
|
|
159
603
|
};
|
|
160
604
|
|
|
161
|
-
|
|
605
|
+
return withPromotionLock(options.memoryDir, ruleKey, async (lock) => {
|
|
606
|
+
storage.invalidateAllMemoriesCacheForDir();
|
|
607
|
+
const existingRule = (await storage.readAllMemories()).find(
|
|
608
|
+
(memory) =>
|
|
609
|
+
memory.frontmatter.category === "rule" &&
|
|
610
|
+
memory.frontmatter.status !== "archived" &&
|
|
611
|
+
memory.frontmatter.status !== "forgotten" &&
|
|
612
|
+
canonicalizeRuleKey(memory.content) === ruleKey
|
|
613
|
+
);
|
|
614
|
+
if (existingRule) {
|
|
615
|
+
report.skipped.push({
|
|
616
|
+
sourceMemoryId: options.sourceMemoryId,
|
|
617
|
+
reason: "duplicate-rule",
|
|
618
|
+
existingRuleId: existingRule.frontmatter.id,
|
|
619
|
+
});
|
|
620
|
+
return report;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (options.dryRun === true) {
|
|
624
|
+
await lock.assertHeld();
|
|
625
|
+
report.promoted.push({
|
|
626
|
+
id: `dry-run:${options.sourceMemoryId}`,
|
|
627
|
+
...candidateBase,
|
|
628
|
+
});
|
|
629
|
+
return report;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
await lock.beforePromotedRuleWrite();
|
|
633
|
+
const id = await storage.writeMemory("rule", content, {
|
|
634
|
+
confidence,
|
|
635
|
+
tags: candidateBase.tags,
|
|
636
|
+
source: "semantic-rule-promotion",
|
|
637
|
+
lineage: candidateBase.lineage,
|
|
638
|
+
sourceMemoryId: options.sourceMemoryId,
|
|
639
|
+
memoryKind: "note",
|
|
640
|
+
links: buildSupportLinks(options.sourceMemoryId, confidence),
|
|
641
|
+
});
|
|
162
642
|
report.promoted.push({
|
|
163
|
-
id
|
|
643
|
+
id,
|
|
164
644
|
...candidateBase,
|
|
165
645
|
});
|
|
166
646
|
return report;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const id = await storage.writeMemory("rule", content, {
|
|
170
|
-
confidence,
|
|
171
|
-
tags: candidateBase.tags,
|
|
172
|
-
source: "semantic-rule-promotion",
|
|
173
|
-
lineage: candidateBase.lineage,
|
|
174
|
-
sourceMemoryId: options.sourceMemoryId,
|
|
175
|
-
memoryKind: "note",
|
|
176
|
-
links: buildSupportLinks(options.sourceMemoryId, confidence),
|
|
177
|
-
});
|
|
178
|
-
report.promoted.push({
|
|
179
|
-
id,
|
|
180
|
-
...candidateBase,
|
|
181
647
|
});
|
|
182
|
-
return report;
|
|
183
648
|
}
|
package/dist/chunk-74VA26CT.js
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
StorageManager
|
|
3
|
-
} from "./chunk-IRFF6LSF.js";
|
|
4
|
-
|
|
5
|
-
// src/semantic-rule-promotion.ts
|
|
6
|
-
function normalizeRuleWhitespace(value) {
|
|
7
|
-
return value.replace(/\s+/g, " ").trim();
|
|
8
|
-
}
|
|
9
|
-
function stripTrailingClausePunctuation(value) {
|
|
10
|
-
return value.replace(/[,:;]+$/g, "").trim();
|
|
11
|
-
}
|
|
12
|
-
function canonicalizeRuleContent(value) {
|
|
13
|
-
return extractExplicitIfThenRule(value) ?? normalizeRuleWhitespace(value);
|
|
14
|
-
}
|
|
15
|
-
function canonicalizeRuleKey(value) {
|
|
16
|
-
return canonicalizeRuleContent(value).toLowerCase();
|
|
17
|
-
}
|
|
18
|
-
function extractExplicitIfThenRule(content) {
|
|
19
|
-
const match = content.match(/\bif\b([\s\S]+?)\bthen\b([\s\S]+?)(?:[.!?](?:\s|$)|$)/i);
|
|
20
|
-
if (!match) return null;
|
|
21
|
-
const condition = stripTrailingClausePunctuation(normalizeRuleWhitespace(match[1] ?? ""));
|
|
22
|
-
const outcome = stripTrailingClausePunctuation(normalizeRuleWhitespace(match[2] ?? ""));
|
|
23
|
-
if (condition.length === 0 || outcome.length === 0) return null;
|
|
24
|
-
return `IF ${condition} THEN ${outcome}.`;
|
|
25
|
-
}
|
|
26
|
-
function promotionConfidence(memory) {
|
|
27
|
-
const base = Number.isFinite(memory.frontmatter.confidence) ? memory.frontmatter.confidence : 0.8;
|
|
28
|
-
return Math.max(0.6, Math.min(0.98, base));
|
|
29
|
-
}
|
|
30
|
-
function promotionTags(memory) {
|
|
31
|
-
return Array.from(/* @__PURE__ */ new Set([...memory.frontmatter.tags ?? [], "semantic-rule", "promoted-rule"]));
|
|
32
|
-
}
|
|
33
|
-
function buildSupportLinks(sourceMemoryId, confidence) {
|
|
34
|
-
return [
|
|
35
|
-
{
|
|
36
|
-
targetId: sourceMemoryId,
|
|
37
|
-
linkType: "supports",
|
|
38
|
-
strength: confidence,
|
|
39
|
-
reason: "Promoted from verified episodic memory"
|
|
40
|
-
}
|
|
41
|
-
];
|
|
42
|
-
}
|
|
43
|
-
async function promoteSemanticRuleFromMemory(options) {
|
|
44
|
-
const report = {
|
|
45
|
-
enabled: options.enabled,
|
|
46
|
-
dryRun: options.dryRun === true,
|
|
47
|
-
promoted: [],
|
|
48
|
-
skipped: []
|
|
49
|
-
};
|
|
50
|
-
if (!options.enabled) {
|
|
51
|
-
report.skipped.push({
|
|
52
|
-
sourceMemoryId: options.sourceMemoryId,
|
|
53
|
-
reason: "disabled"
|
|
54
|
-
});
|
|
55
|
-
return report;
|
|
56
|
-
}
|
|
57
|
-
const storage = new StorageManager(options.memoryDir);
|
|
58
|
-
const sourceMemory = await storage.getMemoryById(options.sourceMemoryId);
|
|
59
|
-
if (!sourceMemory) {
|
|
60
|
-
report.skipped.push({
|
|
61
|
-
sourceMemoryId: options.sourceMemoryId,
|
|
62
|
-
reason: "source-memory-missing"
|
|
63
|
-
});
|
|
64
|
-
return report;
|
|
65
|
-
}
|
|
66
|
-
if (sourceMemory.frontmatter.status === "forgotten") {
|
|
67
|
-
report.skipped.push({
|
|
68
|
-
sourceMemoryId: options.sourceMemoryId,
|
|
69
|
-
reason: "source-memory-forgotten"
|
|
70
|
-
});
|
|
71
|
-
return report;
|
|
72
|
-
}
|
|
73
|
-
if (sourceMemory.frontmatter.status === "archived" || sourceMemory.frontmatter.memoryKind !== "episode") {
|
|
74
|
-
report.skipped.push({
|
|
75
|
-
sourceMemoryId: options.sourceMemoryId,
|
|
76
|
-
reason: "source-memory-not-episode"
|
|
77
|
-
});
|
|
78
|
-
return report;
|
|
79
|
-
}
|
|
80
|
-
const content = extractExplicitIfThenRule(sourceMemory.content);
|
|
81
|
-
if (!content) {
|
|
82
|
-
report.skipped.push({
|
|
83
|
-
sourceMemoryId: options.sourceMemoryId,
|
|
84
|
-
reason: "no-explicit-rule"
|
|
85
|
-
});
|
|
86
|
-
return report;
|
|
87
|
-
}
|
|
88
|
-
const ruleKey = canonicalizeRuleKey(content);
|
|
89
|
-
const existingRule = (await storage.readAllMemories()).find(
|
|
90
|
-
(memory) => memory.frontmatter.category === "rule" && memory.frontmatter.status !== "archived" && memory.frontmatter.status !== "forgotten" && canonicalizeRuleKey(memory.content) === ruleKey
|
|
91
|
-
);
|
|
92
|
-
if (existingRule) {
|
|
93
|
-
report.skipped.push({
|
|
94
|
-
sourceMemoryId: options.sourceMemoryId,
|
|
95
|
-
reason: "duplicate-rule",
|
|
96
|
-
existingRuleId: existingRule.frontmatter.id
|
|
97
|
-
});
|
|
98
|
-
return report;
|
|
99
|
-
}
|
|
100
|
-
const confidence = promotionConfidence(sourceMemory);
|
|
101
|
-
const candidateBase = {
|
|
102
|
-
sourceMemoryId: options.sourceMemoryId,
|
|
103
|
-
content,
|
|
104
|
-
confidence,
|
|
105
|
-
tags: promotionTags(sourceMemory),
|
|
106
|
-
memoryKind: "note",
|
|
107
|
-
lineage: [options.sourceMemoryId]
|
|
108
|
-
};
|
|
109
|
-
if (options.dryRun === true) {
|
|
110
|
-
report.promoted.push({
|
|
111
|
-
id: `dry-run:${options.sourceMemoryId}`,
|
|
112
|
-
...candidateBase
|
|
113
|
-
});
|
|
114
|
-
return report;
|
|
115
|
-
}
|
|
116
|
-
const id = await storage.writeMemory("rule", content, {
|
|
117
|
-
confidence,
|
|
118
|
-
tags: candidateBase.tags,
|
|
119
|
-
source: "semantic-rule-promotion",
|
|
120
|
-
lineage: candidateBase.lineage,
|
|
121
|
-
sourceMemoryId: options.sourceMemoryId,
|
|
122
|
-
memoryKind: "note",
|
|
123
|
-
links: buildSupportLinks(options.sourceMemoryId, confidence)
|
|
124
|
-
});
|
|
125
|
-
report.promoted.push({
|
|
126
|
-
id,
|
|
127
|
-
...candidateBase
|
|
128
|
-
});
|
|
129
|
-
return report;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export {
|
|
133
|
-
promoteSemanticRuleFromMemory
|
|
134
|
-
};
|
|
135
|
-
//# sourceMappingURL=chunk-74VA26CT.js.map
|