@avantf/dsh-mem 0.8.1 → 0.10.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 +42 -43
- package/lib/client.js +1181 -327
- package/lib/engine/graph-recall/index.d.ts +2 -2
- package/lib/engine/graph-recall/traverse.d.ts +2 -2
- package/lib/engine/graph-recall/types.d.ts +1 -1
- package/lib/engine/mem/config/config_files.d.ts +1 -1
- package/lib/engine/mem/db/dao/facts.d.ts +80 -82
- package/lib/engine/mem/dbdrivers/sqlite/facts.d.ts +6 -13
- package/lib/engine/mem/defaults.d.ts +4 -1
- package/lib/engine/mem/entities/extract.d.ts +34 -14
- package/lib/engine/mem/facts_io.d.ts +4 -0
- package/lib/engine/mem/index.d.ts +1 -0
- package/lib/engine/mem/interfaces/repositories.d.ts +5 -19
- package/lib/engine/mem/lifecycle/maintenance.d.ts +8 -4
- package/lib/engine/mem/lifecycle/tick.d.ts +11 -10
- package/lib/engine/mem/lifecycle/trust.d.ts +24 -37
- package/lib/engine/mem/modelBootstrap.d.ts +29 -0
- package/lib/engine/mem/runtime.d.ts +4 -8
- package/lib/engine/mem/store/facts_restore.d.ts +8 -6
- package/lib/engine/mem/store/graph_reader.d.ts +2 -2
- package/lib/engine/mem/store/memory.d.ts +98 -37
- package/lib/engine/mem/store/recall_policy.d.ts +10 -0
- package/lib/engine/mem/store/time_filter.d.ts +1 -1
- package/lib/engine/mem/trustHeartbeat.d.ts +1 -1
- package/lib/engine/mem-contract/config.d.ts +144 -9
- package/lib/engine/mem-contract/index.d.ts +2 -0
- package/lib/engine/mem-contract/time.d.ts +75 -0
- package/lib/engine/mem-contract/tools.d.ts +9 -15
- package/lib/engine/mem-contract/trust_curve.d.ts +119 -0
- package/lib/engine/mem-contract/types.d.ts +73 -12
- package/lib/engine/mem-retrieval/adapters/auto_vstore.d.ts +10 -0
- package/lib/engine/mem-retrieval/adapters/hnswlib.d.ts +28 -0
- package/lib/engine/mem-retrieval/adapters/local_bge.d.ts +48 -6
- package/lib/engine/mem-retrieval/interfaces.d.ts +22 -2
- package/lib/engine/mem-retrieval/text_budget.d.ts +31 -0
- package/lib/index.js +2349 -665
- package/lib/interface-version.json +2 -2
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/paging.d.ts +58 -8
- package/lib/types/client/paging.d.ts.map +1 -1
- package/lib/types/client/time_filter.d.ts +54 -0
- package/lib/types/client/time_filter.d.ts.map +1 -0
- package/lib/types/command.d.ts.map +1 -1
- package/lib/types/deps_audit.d.ts +88 -0
- package/lib/types/deps_audit.d.ts.map +1 -0
- package/lib/types/envinit-bootstrap.d.ts +1 -1
- package/lib/types/envinit.d.ts +224 -4
- package/lib/types/envinit.d.ts.map +1 -1
- package/lib/types/hints.d.ts +58 -0
- package/lib/types/hints.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/remote.d.ts.map +1 -1
- package/lib/types/render.d.ts.map +1 -1
- package/lib/types/runtime_deps_resource.d.ts +29 -0
- package/lib/types/runtime_deps_resource.d.ts.map +1 -0
- package/package.json +26 -24
- package/res/runtime-deps.yaml +99 -0
package/lib/index.js
CHANGED
|
@@ -12,9 +12,9 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
12
12
|
import { Readable } from "node:stream";
|
|
13
13
|
import { pipeline } from "node:stream/promises";
|
|
14
14
|
import { brotliCompressSync, brotliDecompressSync, constants, gunzipSync, gzipSync } from "node:zlib";
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
16
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
16
17
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
17
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
18
|
//#region ../contract/lib/types.js
|
|
19
19
|
/** Shared domain types for avantf-mem. Single source of truth in @avantf/mem-contract. */
|
|
20
20
|
/**
|
|
@@ -167,6 +167,146 @@ const DEGRADED_WEIGHTS = {
|
|
|
167
167
|
*/
|
|
168
168
|
const DEGRADED_LEG_NOTE = "本次仅词法腿,语义腿未就绪(语义后端不可用,结果只来自 FTS 与实体腿)。";
|
|
169
169
|
//#endregion
|
|
170
|
+
//#region ../contract/lib/trust_curve.js
|
|
171
|
+
/**
|
|
172
|
+
* The trust decay curve — PURE functions, no IO, no config object of its own.
|
|
173
|
+
*
|
|
174
|
+
* This file is the ONE implementation of "how much trust is left after N active days" and of every
|
|
175
|
+
* inverse of it. `core/lifecycle/trust.ts` (read path, recall reinforcement), the tick's settle
|
|
176
|
+
* statement (`db/dao/facts.ts`) and the config's load-time validation (`config.ts`) all delegate
|
|
177
|
+
* here, so a change of shape cannot leave one of them behind (docs/TRUST_MODEL.md §2.2, DESIGN §18).
|
|
178
|
+
*
|
|
179
|
+
* It lives in the CONTRACT package because the load-time validation needs the same math, and the
|
|
180
|
+
* dependency direction is core → contract, never the reverse.
|
|
181
|
+
*
|
|
182
|
+
* ## The two shapes
|
|
183
|
+
*
|
|
184
|
+
* `linear` (TRUST_MODEL D7, the P3 equivalent tier): trust falls by a constant amount per active
|
|
185
|
+
* day, `slope = BASE_TRUST / base_lifetime_days` ⇒ a fact that starts at `BASE_TRUST` reaches 0
|
|
186
|
+
* after exactly `base_lifetime_days` active days.
|
|
187
|
+
*
|
|
188
|
+
* `accelerating` (the v2 target shape): `dT/dt = −λ·(a + 1 − T)` — the closer to 1, the slower the
|
|
189
|
+
* decay, so "almost permanent" facts really linger. Closed form (plan §3.1):
|
|
190
|
+
*
|
|
191
|
+
* T(t) = (1 + a) − ((1 + a) − T₀)·e^(λt) λ = ln((1 + a)/(1 + a − BASE_TRUST)) / B
|
|
192
|
+
*
|
|
193
|
+
* Note the POSITIVE exponent: `u = a + 1 − T` grows, so `e^(λt)` is the decay direction here (the
|
|
194
|
+
* plan writes it this way, and the closed-form test pins it). `a = decay_curvature`.
|
|
195
|
+
*
|
|
196
|
+
* ## Units
|
|
197
|
+
*
|
|
198
|
+
* `days` is ALWAYS the ACTIVE-DAY counter (`avantf_stats.trust_clock`), never wall time.
|
|
199
|
+
*/
|
|
200
|
+
/**
|
|
201
|
+
* The frozen anchor (TRUST_MODEL D20): a new fact's initial trust, the trust value the lifetime
|
|
202
|
+
* anchor is defined at (`daysToZero(BASE_TRUST) === base_lifetime_days`), and the divider between
|
|
203
|
+
* the two recall-reinforcement segments. One constant on purpose — the three uses must never be
|
|
204
|
+
* configurable apart, or "a new fact lives 90 active days" stops being true.
|
|
205
|
+
*/
|
|
206
|
+
const BASE_TRUST = .5;
|
|
207
|
+
/** The decay shapes `decay_shape` accepts. */
|
|
208
|
+
const DECAY_SHAPES = ["linear", "accelerating"];
|
|
209
|
+
/** Read the curve out of the `trust` section (the single mapping from keys to math). */
|
|
210
|
+
function curveOf(trust) {
|
|
211
|
+
return {
|
|
212
|
+
shape: trust.decay_shape,
|
|
213
|
+
curvature: trust.decay_curvature,
|
|
214
|
+
baseDays: trust.base_lifetime_days
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Trust units lost per active day — the LINEAR slope, `BASE_TRUST / base_lifetime_days`.
|
|
219
|
+
*
|
|
220
|
+
* `linear` is the only shape with a constant slope; for `accelerating` this returns the LOCAL slope
|
|
221
|
+
* at the anchor `T = BASE_TRUST` (`λ·(BASE_TRUST + a)`). It exists as the building block of the
|
|
222
|
+
* `linear` branch of {@link trustAfterDays} / {@link ceilingForDays} and for the tests that pin the
|
|
223
|
+
* anchor; it is deliberately NOT a scalar stand-in for the curve — anything that needs "how much
|
|
224
|
+
* trust is left after N days" uses {@link trustAfterDays}, and anything that needs "N days from a
|
|
225
|
+
* threshold" uses {@link ceilingForDays}. (An earlier caller — `trust_diagnose.forgetting_soon` —
|
|
226
|
+
* used it as a linear rate and was wrong once `accelerating` became the default.)
|
|
227
|
+
*/
|
|
228
|
+
function slopeOf(shape, curvature, baseDays) {
|
|
229
|
+
if (shape === "linear") return BASE_TRUST / baseDays;
|
|
230
|
+
return lambdaFor(curvature, baseDays) * (BASE_TRUST + curvature);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* `λ = ln((1 + a) / (1 + a − BASE_TRUST)) / baseDays` — pinned so the anchor holds:
|
|
234
|
+
* `λ·baseDays = ln((1 + a)/(0.5 + a))`, i.e. `daysToZero(BASE_TRUST) === baseDays` for ANY `a`.
|
|
235
|
+
*/
|
|
236
|
+
function lambdaFor(curvature, baseDays) {
|
|
237
|
+
return Math.log((1 + curvature) / (1 + curvature - BASE_TRUST)) / baseDays;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* `T − days·slope` (linear) or the closed form `(1 + a) − ((1 + a) − T)·e^(λ·days)`.
|
|
241
|
+
*
|
|
242
|
+
* UNCLAMPED on purpose: callers clamp to `[0, 1]` themselves (`effectiveTrust`'s read path and the
|
|
243
|
+
* settle write both must clamp, and a shared clamp here would hide a caller that forgot).
|
|
244
|
+
*/
|
|
245
|
+
function trustAfterDays(T, days, cfg) {
|
|
246
|
+
if (cfg.shape === "linear") return T - days * slopeOf(cfg.shape, cfg.curvature, cfg.baseDays);
|
|
247
|
+
const a = cfg.curvature;
|
|
248
|
+
return 1 + a - (1 + a - T) * Math.exp(lambdaFor(a, cfg.baseDays) * days);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Active days between two trust values, `t(T0 → T1)`.
|
|
252
|
+
*
|
|
253
|
+
* `linear`: `(T0 − T1) / slope`; `accelerating`: `ln((1 + a − T1)/(1 + a − T0)) / λ`. Both are
|
|
254
|
+
* signed the natural way (a RISING target yields a negative number of days), so the formula is
|
|
255
|
+
* usable as an inverse without special cases. Returns `NaN` only for an out-of-domain input
|
|
256
|
+
* (`T > 1 + a` on the accelerating branch), which the config validation makes unreachable.
|
|
257
|
+
*/
|
|
258
|
+
function daysBetween(T0, T1, cfg) {
|
|
259
|
+
if (cfg.shape === "linear") return (T0 - T1) / slopeOf(cfg.shape, cfg.curvature, cfg.baseDays);
|
|
260
|
+
const a = cfg.curvature;
|
|
261
|
+
return Math.log((1 + a - T1) / (1 + a - T0)) / lambdaFor(a, cfg.baseDays);
|
|
262
|
+
}
|
|
263
|
+
/** Active days until the value reaches 0 — `remainingDays`'s whole body. */
|
|
264
|
+
function daysToZero(T, cfg) {
|
|
265
|
+
return daysBetween(T, 0, cfg);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* The trust ceiling a `max_retention_days` promises: after being reinforced up to `ceiling`, a fact
|
|
269
|
+
* survives `days` more active days.
|
|
270
|
+
*
|
|
271
|
+
* `linear`: `days · slope` (= `days / (2·baseDays)`); `accelerating`: `(1 + a)(1 − e^(−λ·days))`.
|
|
272
|
+
* This is an INVERSION (`daysToZero(ceilingForDays(R)) === R`), not a configuration value — the
|
|
273
|
+
* config has no `recall_ceiling` key any more (D16).
|
|
274
|
+
*/
|
|
275
|
+
function ceilingForDays(days, cfg) {
|
|
276
|
+
if (cfg.shape === "linear") return days * slopeOf(cfg.shape, cfg.curvature, cfg.baseDays);
|
|
277
|
+
const a = cfg.curvature;
|
|
278
|
+
return (1 + a) * (1 - Math.exp(-lambdaFor(a, cfg.baseDays) * days));
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The inverse of {@link ceilingForDays}: how many active days a given ceiling is worth. Used by the
|
|
282
|
+
* deprecated-key warning (`config/loader.ts`) to translate an old `recall_ceiling` value, and by the
|
|
283
|
+
* validation messages.
|
|
284
|
+
*/
|
|
285
|
+
function retentionDaysForCeiling(ceiling, cfg) {
|
|
286
|
+
if (cfg.shape === "linear") return ceiling / slopeOf(cfg.shape, cfg.curvature, cfg.baseDays);
|
|
287
|
+
return daysToZero(ceiling, cfg);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* `R_max` of the accelerating shape — the days at which the ceiling reaches 1 (`ceiling → ∞`).
|
|
291
|
+
*
|
|
292
|
+
* R_max = baseDays · ln((1 + a)/a) / ln((1 + a)/(1 + a − BASE_TRUST))
|
|
293
|
+
*
|
|
294
|
+
* `max_retention_days` must stay strictly below it, or a reinforced fact would stop aging like
|
|
295
|
+
* `pinned` does (D14: permanent is set only by a person).
|
|
296
|
+
*/
|
|
297
|
+
function RMAX(curvature, baseDays) {
|
|
298
|
+
return baseDays * Math.log((1 + curvature) / curvature) / Math.log((1 + curvature) / (1 + curvature - BASE_TRUST));
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* The shape-aware upper bound on `max_retention_days`: the days at which the ceiling hits 1.
|
|
302
|
+
*
|
|
303
|
+
* `linear`: `2·baseDays` (the ceiling is `days/(2·baseDays)`); `accelerating`: {@link RMAX}. Both
|
|
304
|
+
* are STRICT bounds — at the bound the ceiling is exactly 1, which `superRefine` rejects.
|
|
305
|
+
*/
|
|
306
|
+
function retentionDaysLimit(shape, curvature, baseDays) {
|
|
307
|
+
return shape === "linear" ? 2 * baseDays : RMAX(curvature, baseDays);
|
|
308
|
+
}
|
|
309
|
+
//#endregion
|
|
170
310
|
//#region ../contract/lib/config.js
|
|
171
311
|
/**
|
|
172
312
|
* Upper bound on tokens handed to the embedder. 0 = auto (the loaded model's declared
|
|
@@ -174,6 +314,186 @@ const DEGRADED_LEG_NOTE = "本次仅词法腿,语义腿未就绪(语义后
|
|
|
174
314
|
* with no error, so the adapter bounds the text first and warns once (DESIGN §20).
|
|
175
315
|
*/
|
|
176
316
|
const maxInputTokens = z.number().int().nonnegative().default(0);
|
|
317
|
+
/**
|
|
318
|
+
* One registry/cache endpoint a self-heal section's attempts may name.
|
|
319
|
+
*
|
|
320
|
+
* SHARED by `semantic.self_heal` (transformers.js) and `semantic.native_self_heal` (the native
|
|
321
|
+
* packages): the base `runtime-deps` provider takes ONE source vocabulary, and two spellings of it
|
|
322
|
+
* would be two ways to get the same install wrong.
|
|
323
|
+
*
|
|
324
|
+
* Mirrors the provider's `DependencySource` (interface v5) without importing it: this package is
|
|
325
|
+
* also used by the CLI/MCP processes, which never load the base. An empty `cache` on a source with
|
|
326
|
+
* no registry means "the family root's npm cache" — the plugin fills in `<home>/cache/npm` when it
|
|
327
|
+
* builds the item, because this schema does not know the family root.
|
|
328
|
+
*/
|
|
329
|
+
const selfHealSourceSchema = z.object({
|
|
330
|
+
id: z.string().min(1),
|
|
331
|
+
registry: z.string().default(""),
|
|
332
|
+
cache: z.string().default("")
|
|
333
|
+
});
|
|
334
|
+
/**
|
|
335
|
+
* One install attempt, in the base provider's vocabulary. SHARED by both self-heal sections.
|
|
336
|
+
*
|
|
337
|
+
* `env` exists mainly for `ONNXRUNTIME_NODE_INSTALL=skip`: `onnxruntime-node`'s postinstall download
|
|
338
|
+
* returns 302 on some mirrors, and skipping that check is enough for transformers.js to load its
|
|
339
|
+
* WASM/CPU fallback.
|
|
340
|
+
*
|
|
341
|
+
* `allowScripts` is explicit in this plugin's defaults: the base provider defaults it to `false`
|
|
342
|
+
* (lifecycle scripts from a registry package are a caller decision), and `onnxruntime-node` needs its
|
|
343
|
+
* postinstall unless the attempt sets `ONNXRUNTIME_NODE_INSTALL=skip`. Leaving it unset here would
|
|
344
|
+
* turn every default attempt into an `--ignore-scripts` install and break the native binary. The same
|
|
345
|
+
* holds — more strongly — for the native section: `nodejieba` and `hnswlib-node` are node-gyp builds
|
|
346
|
+
* whose ONLY artefact is produced by the install script.
|
|
347
|
+
*/
|
|
348
|
+
const selfHealAttemptSchema = z.object({
|
|
349
|
+
source: z.string().min(1),
|
|
350
|
+
manager: z.enum(["npm", "pnpm"]).optional(),
|
|
351
|
+
env: z.record(z.string(), z.string()).default({}),
|
|
352
|
+
allowScripts: z.boolean().optional()
|
|
353
|
+
});
|
|
354
|
+
/**
|
|
355
|
+
* One entry of the STARTUP DEPENDENCY AUDIT (`audit:` in `res/runtime-deps.yaml`): what to observe,
|
|
356
|
+
* how loud to say it, and under which condition — a session notice, plus the probe that decides
|
|
357
|
+
* whether it is due.
|
|
358
|
+
*
|
|
359
|
+
* WHY IT IS DATA, NOT CODE. The interesting question at the end of a mount is "which of the optional
|
|
360
|
+
* things this build is made of can this process actually load?" — and the set differs per RELEASE (a
|
|
361
|
+
* native binding added or dropped, a mirror changed, a sentence reworded). Writing those names and
|
|
362
|
+
* sentences in code would make the implementation carry a deployment's vocabulary. So the plugin
|
|
363
|
+
* implements two GENERIC probe kinds, and the shipped declaration says which pieces exist and what a
|
|
364
|
+
* session is told about them. No package name, body, severity or condition is written in code.
|
|
365
|
+
*
|
|
366
|
+
* `kind` selects the probe:
|
|
367
|
+
*
|
|
368
|
+
* - `module` — resolve AND LOAD `specifier` (a `require`/`import`, so a native addon's binding is
|
|
369
|
+
* really loaded, not just found), from a ready self-heal root first and from this tree otherwise.
|
|
370
|
+
* States: `available` (loaded), `failed` (resolved, but loading it threw — the classic node-gyp
|
|
371
|
+
* build that exists and does not work), `missing` (nothing resolved).
|
|
372
|
+
* - `runtime-deps` — the STATE of the named `item` (`mem:semantic-deps`, `mem:native-deps`, or any
|
|
373
|
+
* other runtime-deps item): `available` (ready), `failed`, `missing`, `skipped`, `pending`.
|
|
374
|
+
*
|
|
375
|
+
* `when` is matched against that state, and the word means the same thing for both kinds:
|
|
376
|
+
* `failed` → `failed`; `missing` → `missing` or `skipped`; `degraded` → any of the three. An entry
|
|
377
|
+
* that is `pending` matches NOTHING: a line latched while a background install is still running would
|
|
378
|
+
* outlive the success it is waiting for. (`pending` only occurs for `runtime-deps`, whose item may be
|
|
379
|
+
* mid-install; a `module` probe is a single synchronous load and cannot be pending.)
|
|
380
|
+
*
|
|
381
|
+
* `min_vectors` (optional, `0`/omitted = every corpus) holds the line back until the live vector
|
|
382
|
+
* index reaches that size — the corpus-size knob an ANN entry needs (below the switch-over point the
|
|
383
|
+
* brute-force store is the intended backend, so a missing ANN binding is not a defect). It is a
|
|
384
|
+
* literal in the declaration, deliberately not derived from `vectorStore.auto_thresholds.hnswlib`:
|
|
385
|
+
* code that computed it would be branching on a package, which is what this section exists to avoid.
|
|
386
|
+
*
|
|
387
|
+
* `id` must be namespaced (`mem:…`) — it is the unit of the once-per-session latch, so an id shared
|
|
388
|
+
* with another plugin would silently suppress one of the two lines. The base registry warns and
|
|
389
|
+
* ignores a malformed one instead of throwing, and so this schema rejects it up front.
|
|
390
|
+
*/
|
|
391
|
+
const dependencyAuditFields = {
|
|
392
|
+
id: z.string().min(1),
|
|
393
|
+
severity: z.enum([
|
|
394
|
+
"info",
|
|
395
|
+
"warning",
|
|
396
|
+
"degraded"
|
|
397
|
+
]),
|
|
398
|
+
when: z.enum([
|
|
399
|
+
"failed",
|
|
400
|
+
"missing",
|
|
401
|
+
"degraded"
|
|
402
|
+
]),
|
|
403
|
+
min_vectors: z.number().int().nonnegative().optional(),
|
|
404
|
+
body: z.string().min(1)
|
|
405
|
+
};
|
|
406
|
+
/** One audit entry. `kind` decides which probe runs, and which of `specifier`/`item` is required. */
|
|
407
|
+
const DependencyAuditEntrySchema = z.discriminatedUnion("kind", [z.object({
|
|
408
|
+
kind: z.literal("module"),
|
|
409
|
+
/** The npm specifier to load (`nodejieba`, `hnswlib-node`, `@huggingface/transformers`, …). */
|
|
410
|
+
specifier: z.string().min(1),
|
|
411
|
+
...dependencyAuditFields
|
|
412
|
+
}), z.object({
|
|
413
|
+
kind: z.literal("runtime-deps"),
|
|
414
|
+
/** The runtime-deps item whose state is observed (`mem:semantic-deps`, `mem:native-deps`, …). */
|
|
415
|
+
item: z.string().min(1),
|
|
416
|
+
...dependencyAuditFields
|
|
417
|
+
})]);
|
|
418
|
+
/**
|
|
419
|
+
* One `runtime-deps` declaration as SHIPPED WITH THE PLUGIN (`packages/plugin/res/runtime-deps.yaml`).
|
|
420
|
+
*
|
|
421
|
+
* WHY IT IS A RESOURCE AND NOT USER CONFIG. The package set, the sources, the attempts, the verify
|
|
422
|
+
* list and the audit sentences are facts about THIS BUILD: which module the local backend imports,
|
|
423
|
+
* which native bindings this release looks for, what a session should be told when one of them is
|
|
424
|
+
* absent. Leaving them to `configs/common.yaml` made every deployment responsible for keeping that
|
|
425
|
+
* list correct — a stale copy would install a module no code loads, or silently drop the audit — and
|
|
426
|
+
* the shipped defaults were only ever the plugin's own declaration anyway. The operator keeps the one
|
|
427
|
+
* thing that really is a per-deployment decision (the two `enabled` switches); the DATA travels in the
|
|
428
|
+
* package, next to the code that reads it.
|
|
429
|
+
*
|
|
430
|
+
* Validation is strict on purpose: the file is read from the INSTALL directory at mount, and a
|
|
431
|
+
* malformed one is a packaging bug. The plugin logs it and degrades (no self-heal item, no audit)
|
|
432
|
+
* rather than throwing out of the mount — see `packages/plugin/src/runtime_deps_resource.ts`.
|
|
433
|
+
*
|
|
434
|
+
* The schema is the SINGLE definition of the file's shape: the loader validates with it, and the
|
|
435
|
+
* plugin maps it to the base provider's objects field by field (the base never reads YAML).
|
|
436
|
+
*/
|
|
437
|
+
const RuntimeDepsResourceSchema = z.object({
|
|
438
|
+
/** Format version, so a future shape can be recognized instead of misread. */
|
|
439
|
+
version: z.literal(1),
|
|
440
|
+
/** `@huggingface/transformers` — the semantic leg's optional module. */
|
|
441
|
+
semantic: z.object({
|
|
442
|
+
packages: z.record(z.string(), z.string()),
|
|
443
|
+
sources: z.array(selfHealSourceSchema),
|
|
444
|
+
attempts: z.array(selfHealAttemptSchema),
|
|
445
|
+
verify: z.array(z.string())
|
|
446
|
+
}),
|
|
447
|
+
/** The native optional dependencies (`nodejieba`, `hnswlib-node`). */
|
|
448
|
+
native: z.object({
|
|
449
|
+
packages: z.record(z.string(), z.string()),
|
|
450
|
+
sources: z.array(selfHealSourceSchema),
|
|
451
|
+
attempts: z.array(selfHealAttemptSchema),
|
|
452
|
+
verify: z.array(z.string())
|
|
453
|
+
}),
|
|
454
|
+
/**
|
|
455
|
+
* What the startup dependency audit observes, and what a session is told. Each entry is one probe
|
|
456
|
+
* (`kind`) plus its delivery decision — see {@link DependencyAuditEntrySchema}.
|
|
457
|
+
*/
|
|
458
|
+
audit: z.array(DependencyAuditEntrySchema)
|
|
459
|
+
});
|
|
460
|
+
/**
|
|
461
|
+
* Semantic dependency self-heal (`@huggingface/transformers` + transitive `onnxruntime-node`).
|
|
462
|
+
*
|
|
463
|
+
* **OFF by default** on purpose: enabling it means the plugin may run a package-manager install at
|
|
464
|
+
* startup, i.e. network access before anyone asked for it. A profile that wants the repair opts in
|
|
465
|
+
* (`semantic.self_heal.enabled: true`); without it the runtime keeps its existing optional-dependency
|
|
466
|
+
* resolution and degrades to FTS+entity when the package is absent.
|
|
467
|
+
*
|
|
468
|
+
* The SPEC itself (packages / sources / attempts / verify) is NOT here: it ships in
|
|
469
|
+
* `res/runtime-deps.yaml` (`RuntimeDepsResourceSchema`). This section is the operator's policy — the
|
|
470
|
+
* only per-deployment decision left, because it costs a startup install.
|
|
471
|
+
*/
|
|
472
|
+
const semanticSelfHealSchema = z.object({ enabled: z.boolean().default(false) });
|
|
473
|
+
/**
|
|
474
|
+
* NATIVE dependency self-heal (`nodejieba`, `hnswlib-node`) — the optional npm dependencies whose
|
|
475
|
+
* absence degrades a FEATURE rather than the plugin:
|
|
476
|
+
*
|
|
477
|
+
* - `nodejieba` → POS tagging; without it entity/relation extraction falls back to a regex pass
|
|
478
|
+
* (weaker entity + jaccard legs, still functional),
|
|
479
|
+
* - `hnswlib-node` → the ANN vector index; without it queries stay on the brute-force store (exact,
|
|
480
|
+
* slower once the corpus grows).
|
|
481
|
+
*
|
|
482
|
+
* Both are node-gyp packages, so their shipped `attempts` allow install scripts (see
|
|
483
|
+
* `res/runtime-deps.yaml`): an `--ignore-scripts` install of either produces a `require()` that fails
|
|
484
|
+
* at run time, which is the one outcome a "self-heal" must not produce.
|
|
485
|
+
*
|
|
486
|
+
* One item, two packages, ONE store root per spec: the base's `runtime-deps` item is
|
|
487
|
+
* content-addressed, so this spec (a different package set from the semantic one) lands in a
|
|
488
|
+
* DIFFERENT immutable root under `<home>/deps/runtime/<key>/` — never the transformers root, whose
|
|
489
|
+
* `node_modules` does not carry either native build.
|
|
490
|
+
*
|
|
491
|
+
* OFF by default, for the same reason as `self_heal`: enabling it means a package-manager install
|
|
492
|
+
* (and a native compile) at startup. What a session is told about a missing package is an `audit`
|
|
493
|
+
* entry in the same resource file, judged on the package itself and therefore working with or without
|
|
494
|
+
* a repair.
|
|
495
|
+
*/
|
|
496
|
+
const nativeSelfHealSchema = z.object({ enabled: z.boolean().default(false) });
|
|
177
497
|
const semanticSchema = z.object({
|
|
178
498
|
backend: z.string().default("local_bge"),
|
|
179
499
|
local_model: z.string().default("Xenova/bge-base-zh-v1.5"),
|
|
@@ -193,7 +513,14 @@ const semanticSchema = z.object({
|
|
|
193
513
|
* stay, but nothing re-encodes on its own. Set to `false` on a metered/embedded host that must not
|
|
194
514
|
* spend CPU on background re-encoding.
|
|
195
515
|
*/
|
|
196
|
-
auto_migrate: z.boolean().default(true)
|
|
516
|
+
auto_migrate: z.boolean().default(true),
|
|
517
|
+
/** Repair a missing `@huggingface/transformers` install through the base's shared runtime store. */
|
|
518
|
+
self_heal: semanticSelfHealSchema.prefault({}),
|
|
519
|
+
/**
|
|
520
|
+
* Repair a missing NATIVE optional dependency (`nodejieba` / `hnswlib-node`) through the same
|
|
521
|
+
* base mechanism, in its OWN store root (see {@link nativeSelfHealSchema}).
|
|
522
|
+
*/
|
|
523
|
+
native_self_heal: nativeSelfHealSchema.prefault({})
|
|
197
524
|
});
|
|
198
525
|
const vectorStoreSchema = z.object({
|
|
199
526
|
backend: z.string().default("auto"),
|
|
@@ -341,40 +668,83 @@ const presenceSchema = z.object({
|
|
|
341
668
|
/** Long-lived surfaces (plugin / MCP) tick this often; 0 = startup pass only. */
|
|
342
669
|
heartbeat_minutes: z.number().int().nonnegative().default(60)
|
|
343
670
|
});
|
|
671
|
+
/**
|
|
672
|
+
* Trust & forgetting (TRUST_MODEL.md §2 / §7).
|
|
673
|
+
*
|
|
674
|
+
* The knobs are DAYS and a SHAPE, never a slope: the operator's promise is "how long does a memory
|
|
675
|
+
* last", and the arithmetic that turns days into trust units lives in `trust_curve.ts` (one
|
|
676
|
+
* implementation, shared with the load-time validation below). `recall_ceiling` is gone — the
|
|
677
|
+
* reinforcement ceiling is an INVERSION of `max_retention_days` (D16).
|
|
678
|
+
*/
|
|
344
679
|
const trustSchema = z.object({
|
|
345
|
-
/** false = no presence, no decay, no reinforcement, no pinning (
|
|
680
|
+
/** false = no presence, no decay, no reinforcement, no pinning, no automatic archiving (purge keeps running). */
|
|
346
681
|
enabled: z.boolean().default(true),
|
|
347
|
-
/**
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
682
|
+
/**
|
|
683
|
+
* The anchor: active days a fact takes to fall from `BASE_TRUST` (0.5, frozen — D20) to 0, i.e.
|
|
684
|
+
* "how long a never-recalled memory lives". `start` is NOT configurable any more: it is the same
|
|
685
|
+
* 0.5 as the anchor and the reinforcement divider, so one knob could untie all three.
|
|
686
|
+
*/
|
|
687
|
+
base_lifetime_days: z.number().int().positive().default(90),
|
|
688
|
+
/**
|
|
689
|
+
* The longest stay recall reinforcement can buy, in ACTIVE days — the ceiling is inverted from
|
|
690
|
+
* this (`ceilingForDays`), never configured (D16).
|
|
691
|
+
*
|
|
692
|
+
* P4 ships **174** with `decay_shape: 'accelerating'` ⇒ ceiling ≈ **0.7492**. That pair is a
|
|
693
|
+
* deliberate PROMISE change, not a correction: the longest stay grows 153 → 174 active days and the
|
|
694
|
+
* reinforcement ceiling falls 0.85 → 0.7492 (CHANGELOG/unreleased.md). P3's equivalent tier —
|
|
695
|
+
* `linear` + 153 ⇒ exactly 0.85 — stays legal and is the documented rollback.
|
|
696
|
+
*/
|
|
697
|
+
max_retention_days: z.number().int().positive().default(174),
|
|
698
|
+
/** `accelerating` (the v2 target shape) or `linear` (P3's equivalent tier / the rollback). */
|
|
699
|
+
decay_shape: z.enum(DECAY_SHAPES).default("accelerating"),
|
|
700
|
+
/**
|
|
701
|
+
* `a` — the accelerating shape's floor term. `a > 0` is a hard requirement: at `a = 0`, `T = 1`
|
|
702
|
+
* is a fixed point, so an `unpin`ned `trust = 1` fact would never decay again — a second kind of
|
|
703
|
+
* permanent. The `(0, 0.5]` bound is checked in the `superRefine` below (with the reason in the
|
|
704
|
+
* message). Unused by `linear`, present so both shapes share one schema.
|
|
705
|
+
*/
|
|
706
|
+
decay_curvature: z.number().default(.05),
|
|
351
707
|
/** Trust ≤ this ⇒ forgotten (archived 'forgot'). 0 = "reaching zero is forgetting". */
|
|
352
708
|
forget_threshold: z.number().min(0).max(1).default(0),
|
|
353
|
-
/**
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
709
|
+
/**
|
|
710
|
+
* Per-recall gain BELOW `BASE_TRUST`, capped at `BASE_TRUST` itself. `0.1` ⇒ five recalls take a
|
|
711
|
+
* fact from 0.05 back to the base (the second crossing — from exactly 0.5 up — is the above-base
|
|
712
|
+
* segment, D20).
|
|
713
|
+
*/
|
|
714
|
+
recall_gain_below_base: z.number().gt(0).max(.5).default(.1),
|
|
715
|
+
/** Per-recall gain above `BASE_TRUST` (before marginal decay), capped at the derived ceiling. */
|
|
716
|
+
recall_gain_above_base: z.number().gt(0).max(1).default(.01),
|
|
359
717
|
/** Effective reinforcement events per fact per 24h (R10: zero-gain ones don't count). */
|
|
360
718
|
recall_daily_cap: z.number().int().nonnegative().default(3),
|
|
361
|
-
/** 1 = every in-window gain is equal; 0.5 = 0.
|
|
719
|
+
/** 1 = every in-window gain is equal; 0.5 = 0.01 / 0.005 / 0.0025 … */
|
|
362
720
|
recall_marginal_decay: z.number().min(0).max(1).default(1),
|
|
363
|
-
/** Recall alone can never exceed this (and never pins — see D11). */
|
|
364
|
-
recall_ceiling: z.number().min(0).max(1).default(.85),
|
|
365
|
-
/** Explicit `helpful` / `unhelpful` step. */
|
|
366
|
-
feedback_delta: z.number().min(0).max(1).default(.05),
|
|
367
|
-
/** 0 = unlimited; >0 also caps feedback reinforcement per fact per 24h. */
|
|
368
|
-
feedback_daily_cap: z.number().int().nonnegative().default(0),
|
|
369
721
|
/** A revised fact inherits the settled trust (and pin) of the row it replaces. */
|
|
370
722
|
inherit_trust_on_update: z.boolean().default(true),
|
|
371
723
|
/** Pinned (permanent) facts are never physically purged. */
|
|
372
724
|
purge_skips_pinned: z.boolean().default(true),
|
|
373
|
-
/** Calendar fallback: unused for this many days ⇒ archived 'idle'. */
|
|
374
|
-
idle_calendar_days: z.number().int().positive().default(365),
|
|
375
725
|
/** Per-tick budget for the settle sweep (the other sweeps only touch critical rows). */
|
|
376
726
|
tick_max_facts: z.number().int().positive().default(5e3),
|
|
377
727
|
presence: presenceSchema.prefault({})
|
|
728
|
+
}).superRefine((trust, ctx) => {
|
|
729
|
+
const curve = curveOf(trust);
|
|
730
|
+
const base = trust.base_lifetime_days;
|
|
731
|
+
const days = trust.max_retention_days;
|
|
732
|
+
const a = trust.decay_curvature;
|
|
733
|
+
const fail = (path, message) => {
|
|
734
|
+
ctx.addIssue({
|
|
735
|
+
code: "custom",
|
|
736
|
+
path: [path],
|
|
737
|
+
message
|
|
738
|
+
});
|
|
739
|
+
};
|
|
740
|
+
const ceiling = () => ceilingForDays(days, curve).toFixed(4);
|
|
741
|
+
if (!(a > 0) || a > .5) fail("decay_curvature", `decay_curvature must be in (0, 0.5]; got ${String(a)} — 0 会让 T=1 永不衰减(unpin 之后仍然不老,等于第二层"永久"),>0.5 会把可配寿命区间压到默认值以下`);
|
|
742
|
+
if (days <= base) fail("max_retention_days", `max_retention_days must be > base_lifetime_days (${String(base)}); got ${String(days)} — 你这个值等价于 ceiling=${ceiling()},召回加强不会产生任何增益;合法区间 (${String(base)}, ${retentionDaysLimit(trust.decay_shape, a, base).toFixed(1)})`);
|
|
743
|
+
const limit = retentionDaysLimit(trust.decay_shape, a, base);
|
|
744
|
+
if (!(days < limit)) {
|
|
745
|
+
const other = trust.decay_shape === "linear" ? `accelerating (a=${String(a)}): ${RMAX(a, base).toFixed(1)}` : `linear: ${(2 * base).toFixed(1)}`;
|
|
746
|
+
fail("max_retention_days", `max_retention_days must be < ${limit.toFixed(1)} (${trust.decay_shape}, a=${String(a)}, base=${String(base)}); legal range (${String(base)}, ${limit.toFixed(1)}); got ${String(days)} — 你这个值等价于 ceiling=${ceiling()},与"永久只由人设"冲突;另一种形状的上界:${other}`);
|
|
747
|
+
}
|
|
378
748
|
});
|
|
379
749
|
const lifecycleSchema = z.object({
|
|
380
750
|
/** Purge window, measured in ACTIVE days since `archived_clock`. */
|
|
@@ -571,8 +941,6 @@ const KIND_FILTER_DESCRIPTION = `限定记忆层;可选(取值:${MEMORY_KI
|
|
|
571
941
|
const SOURCE_REF_DESCRIPTION = "这条事实的来源(可选):未给 = 未知来源,引擎不会臆造。用 `kind:` 前缀标明来源类型——`session:<会话 id>`(会话供述)、`tool:<工具名>`(工具产出)、无前缀则记为 `manual`(调用方手工给出),前缀之后的文本原样保存。可用 `mem_admin` 的 `list`(`source=`)反查同一来源产生了哪些事实。";
|
|
572
942
|
/** P-13 `event_date`: the event time, written into `valid_from`. */
|
|
573
943
|
const EVENT_DATE_DESCRIPTION = "这件事发生的时间(可选,ISO 日期或日期时间,如 2026-10-06 或 2026-10-06T09:30:00+08:00):写入事实的事件时间,供时间窗检索与展示使用;不参与打分。未给 = 未知(不写)。";
|
|
574
|
-
/** P-13 `valid_until`: a known end, written into `valid_to` WITHOUT archiving the row. */
|
|
575
|
-
const VALID_UNTIL_DESCRIPTION = "这条事实有效到什么时候(可选,ISO 日期或日期时间):写入有效期终点,**不会归档该事实**——「仍然有效、但已知截止于某时」的事实照常参与检索。未给 = 未知(不写)。";
|
|
576
944
|
/**
|
|
577
945
|
* `title`: the short, human-readable handle of a journal entry (JOURNAL_TITLE_SEARCH §2.4/§11.1).
|
|
578
946
|
*
|
|
@@ -696,9 +1064,12 @@ function modelFacingToolResult(toolKey, action, value) {
|
|
|
696
1064
|
* - `source_ref` — the citation the answer must carry;
|
|
697
1065
|
* - `created_at` / `updated_at` — memory MAINTENANCE: when it was written and when it was last
|
|
698
1066
|
* changed is what tells the model whether a fact is fresh enough to act on (and `updated_at`
|
|
699
|
-
* differing from `created_at` is the only visible sign that it was revised)
|
|
700
|
-
*
|
|
701
|
-
*
|
|
1067
|
+
* differing from `created_at` is the only visible sign that it was revised). Both are INSTANTS
|
|
1068
|
+
* rendered as ISO-8601 WITH the host's offset (`2026-10-28T19:58:48+08:00`), so the model does no
|
|
1069
|
+
* timezone arithmetic and cannot read them as local time;
|
|
1070
|
+
* - `valid_from` — the event time (P-07/P-13), which is what an "as of when" question needs. A
|
|
1071
|
+
* CALENDAR DATE (`YYYY-MM-DD`) with no zone — deliberately a different shape from the two
|
|
1072
|
+
* instants above, because it answers a different question;
|
|
702
1073
|
* - `title` — the journal entry's short handle, for recognition and for the follow-up write flow
|
|
703
1074
|
* (an update inherits it). Display only: it never moved this hit's `score`;
|
|
704
1075
|
* - `scores` / `final` — only ever present when the caller explicitly asked for the evidence.
|
|
@@ -718,7 +1089,6 @@ const MODEL_HIT_FIELDS = [
|
|
|
718
1089
|
"created_at",
|
|
719
1090
|
"updated_at",
|
|
720
1091
|
"valid_from",
|
|
721
|
-
"valid_to",
|
|
722
1092
|
"scores",
|
|
723
1093
|
"final"
|
|
724
1094
|
];
|
|
@@ -726,14 +1096,17 @@ const MODEL_HIT_FIELDS = [
|
|
|
726
1096
|
const NULLABLE_HIT_FIELDS = /* @__PURE__ */ new Set([
|
|
727
1097
|
"updated_at",
|
|
728
1098
|
"valid_from",
|
|
729
|
-
"valid_to",
|
|
730
1099
|
"title"
|
|
731
1100
|
]);
|
|
732
1101
|
/**
|
|
733
1102
|
* The envelope a recall result carries besides its rows. Two of these are only meaningful in one
|
|
734
|
-
* mode — `total`/`offset`/`limit`/`truncated` describe
|
|
735
|
-
*
|
|
736
|
-
* (RECALL_INTERFACE §5).
|
|
1103
|
+
* mode — `total`/`offset`/`limit`/`remaining`/`truncated` describe a paged answer (`remaining` only
|
|
1104
|
+
* exists on a pure time window, which pages at 50), `degraded` describes a fuzzy pass — and the
|
|
1105
|
+
* projection keeps whichever are present, so one function serves every shape (RECALL_INTERFACE §5).
|
|
1106
|
+
*
|
|
1107
|
+
* `remaining` is here for a specific reason: "how much of the window did I not see" is a fact the
|
|
1108
|
+
* model must act on (narrow the window, or page on), and leaving it out left only `truncated: true`
|
|
1109
|
+
* with the size of the gap to be guessed.
|
|
737
1110
|
*
|
|
738
1111
|
* `weights`, `floors`, `dropped_by_floor` and `used_tokens` are the operator's diagnostics: a
|
|
739
1112
|
* threshold the caller cannot set and a drop count it cannot act on are pure tokens in the model's
|
|
@@ -744,6 +1117,7 @@ const RESULT_ENVELOPE_FIELDS = [
|
|
|
744
1117
|
"total",
|
|
745
1118
|
"offset",
|
|
746
1119
|
"limit",
|
|
1120
|
+
"remaining",
|
|
747
1121
|
"truncated"
|
|
748
1122
|
];
|
|
749
1123
|
/** Copy the envelope fields a payload actually carries, in a fixed order. */
|
|
@@ -975,10 +1349,8 @@ const RememberUnion = z.discriminatedUnion("action", [
|
|
|
975
1349
|
content: z.string().min(1).describe("要写入或替换的事实内容:一句自包含的陈述。action=add 时必填。"),
|
|
976
1350
|
kind: z.enum(MEMORY_KINDS).optional().describe(KIND_WRITE_DESCRIPTION),
|
|
977
1351
|
title: z.string().min(1).max(120).optional().describe(TITLE_WRITE_DESCRIPTION),
|
|
978
|
-
ttl_days: z.number().int().nonnegative().optional().describe("有效期天数(0 或省略 = 不设有效期;正整数 = 自写入起该天数后自动归档)。update 未给则继承被改写事实的 TTL。"),
|
|
979
1352
|
source_ref: z.string().min(1).optional().describe(SOURCE_REF_DESCRIPTION),
|
|
980
1353
|
event_date: z.string().min(1).optional().describe(EVENT_DATE_DESCRIPTION),
|
|
981
|
-
valid_until: z.string().min(1).optional().describe(VALID_UNTIL_DESCRIPTION),
|
|
982
1354
|
relations: z.array(RelationInputSchema).max(20).optional().describe(RELATIONS_DESCRIPTION)
|
|
983
1355
|
}),
|
|
984
1356
|
z.object({
|
|
@@ -988,24 +1360,14 @@ const RememberUnion = z.discriminatedUnion("action", [
|
|
|
988
1360
|
kind: z.enum(MEMORY_KINDS).optional().describe(`改写后的${KIND_WRITE_DESCRIPTION}`),
|
|
989
1361
|
title: z.string().min(1).max(120).optional().describe(`改写后的${TITLE_WRITE_DESCRIPTION}`),
|
|
990
1362
|
retitle: z.literal(true).optional().describe(RETITLE_DESCRIPTION),
|
|
991
|
-
ttl_days: z.number().int().nonnegative().optional().describe("新的有效期天数(0 = 取消有效期);可选,未给则继承被改写事实的 TTL。"),
|
|
992
1363
|
source_ref: z.string().min(1).optional().describe(SOURCE_REF_DESCRIPTION),
|
|
993
1364
|
event_date: z.string().min(1).optional().describe(EVENT_DATE_DESCRIPTION),
|
|
994
|
-
valid_until: z.string().min(1).optional().describe(VALID_UNTIL_DESCRIPTION),
|
|
995
1365
|
relations: z.array(RelationInputSchema).max(20).optional().describe(`改写后的${RELATIONS_DESCRIPTION}`)
|
|
996
1366
|
}),
|
|
997
1367
|
z.object({
|
|
998
1368
|
action: z.literal("remove"),
|
|
999
1369
|
fact_id: z.number().int().positive().describe("要删除的事实 ID。action=remove 时必填。"),
|
|
1000
1370
|
reason: z.string().optional().describe("删除原因;可选。")
|
|
1001
|
-
}),
|
|
1002
|
-
z.object({
|
|
1003
|
-
action: z.literal("helpful"),
|
|
1004
|
-
fact_id: z.number().int().positive().describe("对回答有帮助的事实 ID。action=helpful 时必填。")
|
|
1005
|
-
}),
|
|
1006
|
-
z.object({
|
|
1007
|
-
action: z.literal("unhelpful"),
|
|
1008
|
-
fact_id: z.number().int().positive().describe("对回答无帮助的事实 ID。action=unhelpful 时必填。")
|
|
1009
1371
|
})
|
|
1010
1372
|
]);
|
|
1011
1373
|
/**
|
|
@@ -1028,9 +1390,13 @@ function modelSubset(union, actions) {
|
|
|
1028
1390
|
return z.discriminatedUnion("action", options);
|
|
1029
1391
|
}
|
|
1030
1392
|
/**
|
|
1031
|
-
* The writes the model may make
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1393
|
+
* The writes the model may make — `MODEL_REMEMBER_ACTIONS` and `REMEMBER_ACTIONS` are the same three
|
|
1394
|
+
* again since the operator-side `helpful` / `unhelpful` actions were removed: a vote on a fact was a
|
|
1395
|
+
* way to move its retention clock, and a model able to vote could "refresh" a fact by voting on it.
|
|
1396
|
+
*
|
|
1397
|
+
* Permanent is set by ONE route only: an explicit `pin` from a human (`mem_admin` / CLI / settings
|
|
1398
|
+
* page). Do NOT re-add an action that promotes or demotes a fact from a write path — that is the
|
|
1399
|
+
* behaviour the retention policy exists to prevent.
|
|
1034
1400
|
*/
|
|
1035
1401
|
const MODEL_REMEMBER_ACTIONS = [
|
|
1036
1402
|
"add",
|
|
@@ -1068,9 +1434,16 @@ const TITLE_INCLUDE_EXCERPT_DESCRIPTION = "是否返回内容摘要(可选,
|
|
|
1068
1434
|
* The exact page's own field descriptions. Their numbers are the documented ones (page 10, cap 50)
|
|
1069
1435
|
* and they apply ONLY to an exact result — the fuzzy pass is a fixed shortlist of five, so a caller
|
|
1070
1436
|
* that got no exact hit cannot page it.
|
|
1437
|
+
*
|
|
1438
|
+
* `limit`'s DEFAULT is the one place the pure time window differs: a window is a LIST, so it is
|
|
1439
|
+
* paged at the contract's maximum (50) instead of the exact page's ten (core `timePage`,
|
|
1440
|
+
* `TIME_DEFAULT_LIMIT`), and its result reports `remaining` — how much of the window the page does
|
|
1441
|
+
* not show. The schema's `.max(50)` is unchanged: an omitted `limit` never goes through it, so the
|
|
1442
|
+
* default is a number the ENGINE picks, not a value the caller could send. `titles` keeps its own
|
|
1443
|
+
* description (unchanged).
|
|
1071
1444
|
*/
|
|
1072
1445
|
const SEARCH_OFFSET_DESCRIPTION = "分页偏移(从 0 开始);可选,默认 0。只作用于分页类结果(search 的 exact/time/facts);模糊检索不接受分页。";
|
|
1073
|
-
const SEARCH_LIMIT_DESCRIPTION = "返回条数上限(1-50);可选,默认 10
|
|
1446
|
+
const SEARCH_LIMIT_DESCRIPTION = "返回条数上限(1-50);可选,默认 10(纯时间召回即只给 `time` 时默认 50;该方法的结果另带 `remaining`=窗口内还有多少条未显示)。只作用于分页类结果(search 的 exact/time/facts);模糊检索固定返回 5 条。";
|
|
1074
1447
|
const TITLES_LIMIT_DESCRIPTION = "返回条数上限(1-50);可选,默认 10。只作用于标题完整相等的精确结果;模糊标题检索固定返回 5 条。";
|
|
1075
1448
|
const SEARCH_QUERY_DESCRIPTION = "检索文本(1–" + String(MAX_QUERY_CHARS) + " 字符)。已给 `time` 时可省略——只给 `time` 就是纯时间召回(`mode=time`:按 `time.field` 从新到旧,不再做字面/事实/关系/模糊检索)。";
|
|
1076
1449
|
/**
|
|
@@ -1089,7 +1462,7 @@ const SearchRequestSchema = z.object({
|
|
|
1089
1462
|
source: z.string().optional().describe("限定来源(`fact_sources.ref`):来源等于该值的事实才参与本次检索;可选,不参与打分(未给 = 不过滤)。"),
|
|
1090
1463
|
offset: z.number().int().nonnegative().optional().describe(SEARCH_OFFSET_DESCRIPTION),
|
|
1091
1464
|
limit: z.number().int().positive().max(50).optional().describe(SEARCH_LIMIT_DESCRIPTION),
|
|
1092
|
-
max_tokens: z.number().int().nonnegative().optional().describe("本次结果的总 token 上限;0
|
|
1465
|
+
max_tokens: z.number().int().nonnegative().optional().describe("本次结果的总 token 上限;0=不限制。缺省:exact/facts/模糊用配置 retrieval.max_output_tokens;纯时间召回不套预算。"),
|
|
1093
1466
|
floors: z.enum(FLOOR_PROFILES).optional().describe(FLOORS_FIELD_DESCRIPTION),
|
|
1094
1467
|
include_scores: z.boolean().optional().describe(INCLUDE_SCORES_DESCRIPTION),
|
|
1095
1468
|
time: TimeFilterSchema.optional().describe("时间过滤(可选):内容 ∩ 时间。硬过滤——窗口内的事实不会被窗口外的高分结果挤掉;给了它就按它,不再叠隐式时间窗。没给 `query` 时必填:时间窗就是整个问题(纯时间召回)。")
|
|
@@ -1194,7 +1567,7 @@ const ModelAdminUnion = modelSubset(AdminUnion, [
|
|
|
1194
1567
|
const REMEMBER_TOOL = {
|
|
1195
1568
|
key: "remember",
|
|
1196
1569
|
name: "mem_remember",
|
|
1197
|
-
description: "写入/更新/删除长期记忆事实。add=新增一条简洁的独立陈述;update=改写已有事实;remove=删除。写入没有改变任何状态时是静默 no-op(add 命中完全重复的内容、update 重存同一内容)。`kind` 必填,两层:`journal`=做了什么/聊了什么/想了什么(必须同时给 `event_date`,写事情发生的日期而不是写入日期,缺了会被拒绝);`note`=稳定事实/偏好/决策/坑。只记会改变后续行为的摘要,不抄全文;不要把每段聊天都写成 journal;`note` 可用 `relations` 写出稳定关系(`{subject, predicate, object}` 三个名字);`journal` 不写 relations,需要留关系时另写一条 note;journal 的 add 必须给 `title`(短标题,见下),总结覆盖目标/关键动作/结论/产出位置/未决项;从 journal 提炼为 note 时用 `source_ref` 指回来源。标题流程:写 journal 前标题不确定时,用 `mem_recall` 的 `titles`(2–4 个主题关键词)找候选,读候选的 title/event_date/摘要判断是否同一事务——同一事务用 `update` 该 fact_id 并省略 title(继承),不同事务 `add` 并给新的 title。同一事务复用同一 title;后续轮次都用上一次 update 返回的新 fact_id;title 在一条 revision 链内不可变,显式换 title 会被拒绝(标题为 NULL 的旧 journal 可一次性回填);发现标题写错时,用 `update` 带 `retitle: true` 与正确的 title
|
|
1570
|
+
description: "写入/更新/删除长期记忆事实。add=新增一条简洁的独立陈述;update=改写已有事实;remove=删除。写入没有改变任何状态时是静默 no-op(add 命中完全重复的内容、update 重存同一内容)。`kind` 必填,两层:`journal`=做了什么/聊了什么/想了什么(必须同时给 `event_date`,写事情发生的日期而不是写入日期,缺了会被拒绝);`note`=稳定事实/偏好/决策/坑。只记会改变后续行为的摘要,不抄全文;不要把每段聊天都写成 journal;`note` 可用 `relations` 写出稳定关系(`{subject, predicate, object}` 三个名字);`journal` 不写 relations,需要留关系时另写一条 note;journal 的 add 必须给 `title`(短标题,见下),总结覆盖目标/关键动作/结论/产出位置/未决项;从 journal 提炼为 note 时用 `source_ref` 指回来源。标题流程:写 journal 前标题不确定时,用 `mem_recall` 的 `titles`(2–4 个主题关键词)找候选,读候选的 title/event_date/摘要判断是否同一事务——同一事务用 `update` 该 fact_id 并省略 title(继承),不同事务 `add` 并给新的 title。同一事务复用同一 title;后续轮次都用上一次 update 返回的新 fact_id;title 在一条 revision 链内不可变,显式换 title 会被拒绝(标题为 NULL 的旧 journal 可一次性回填);发现标题写错时,用 `update` 带 `retitle: true` 与正确的 title 纠正一次——旧标题保留为别名,用旧标题检索仍能命中。",
|
|
1198
1571
|
input: ModelRememberUnion
|
|
1199
1572
|
};
|
|
1200
1573
|
const RECALL_TOOL = {
|
|
@@ -1220,7 +1593,7 @@ Number.MAX_SAFE_INTEGER, -Number.MAX_SAFE_INTEGER;
|
|
|
1220
1593
|
*
|
|
1221
1594
|
* Descriptions are authored per action ("action=update 时必填"), so taking the
|
|
1222
1595
|
* first branch's text verbatim told a model that `fact_id` only matters for
|
|
1223
|
-
* `update` although `remove
|
|
1596
|
+
* `update` although `remove` requires it too. The action-clause
|
|
1224
1597
|
* is stripped and replaced by the derived set of actions that make the field
|
|
1225
1598
|
* required.
|
|
1226
1599
|
*
|
|
@@ -1534,6 +1907,96 @@ function adoptWellFormed(kit) {
|
|
|
1534
1907
|
};
|
|
1535
1908
|
}
|
|
1536
1909
|
//#endregion
|
|
1910
|
+
//#region ../contract/lib/time.js
|
|
1911
|
+
/**
|
|
1912
|
+
* The ONE place a stored timestamp turns into a string a human (or a model) reads.
|
|
1913
|
+
*
|
|
1914
|
+
* ── THE BUG THIS MODULE EXISTS FOR ───────────────────────────────────────────────────────────────
|
|
1915
|
+
*
|
|
1916
|
+
* `facts.created_at` / `updated_at` / `archived_at` (and `contradiction_log.detected_at`, and the
|
|
1917
|
+
* retrieval-health stamp) are written by SQLite's `DEFAULT CURRENT_TIMESTAMP`, which is **UTC**, and
|
|
1918
|
+
* `store/memory.ts` used to hand the raw text up to every surface. Two consequences, both reported as
|
|
1919
|
+
* "记忆时间显示不对":
|
|
1920
|
+
*
|
|
1921
|
+
* 1. the UI printed `2026-10-28 11:58:48` as if it were a local wall clock (Asia/Shanghai really
|
|
1922
|
+
* meant `19:58:48`), because a zone-less string cannot say otherwise;
|
|
1923
|
+
* 2. the MODEL received the same zone-less string next to `event_date`, which is a calendar DATE
|
|
1924
|
+
* with no time and no zone. Two different kinds of value, one shape — the model had to guess
|
|
1925
|
+
* which one it was looking at, and it cannot.
|
|
1926
|
+
*
|
|
1927
|
+
* ── THE SPLIT THIS MODULE ENFORCES ───────────────────────────────────────────────────────────────
|
|
1928
|
+
*
|
|
1929
|
+
* Storage stays UTC and stays byte-identical (`facts_io` export/import/snapshot is a compatibility
|
|
1930
|
+
* surface and is deliberately NOT routed through here). The fix is in the two DISPLAY faces:
|
|
1931
|
+
*
|
|
1932
|
+
* - {@link formatOffsetIso} — the MODEL face (`mem_recall` hits, `mem_admin list/detail/
|
|
1933
|
+
* contradictions`, `mem_admin stats`, the CLI and MCP that print those payloads): an ISO-8601
|
|
1934
|
+
* instant **with an explicit offset**, e.g. `2026-10-28T19:58:48+08:00`, rendered in the host's
|
|
1935
|
+
* local zone. The offset is part of the string, so the model needs no conversion and the value is
|
|
1936
|
+
* never ambiguous again — while `event_date` stays `2026-10-28`, a calendar date with no zone.
|
|
1937
|
+
* - {@link formatLocalHuman} — the HUMAN face (the 记忆 tab): `2026-10-28 19:58`, the same instant,
|
|
1938
|
+
* local, to the minute.
|
|
1939
|
+
*
|
|
1940
|
+
* Both read the input through the SAME parser ({@link parseUtcTs}) and the same instant, so the two
|
|
1941
|
+
* faces cannot disagree. The UI deliberately formats the model face's OWN string rather than
|
|
1942
|
+
* re-deriving one from the raw column: the input it gets already carries an offset, `parseUtcTs`
|
|
1943
|
+
* leaves an explicit offset alone, and there is therefore exactly ONE parse rule and ONE pair of
|
|
1944
|
+
* renderers in the repository — not a second opinion in the browser.
|
|
1945
|
+
*
|
|
1946
|
+
* Dependency-free on purpose: this module is reachable from the DSH client bundle through the
|
|
1947
|
+
* `@avantf/mem-contract/time` subpath (the bundle must not pull in the tool schemas / zod). It has no
|
|
1948
|
+
* DSH host and no DB, so the engine, the CLI and the MCP face can all use it.
|
|
1949
|
+
*
|
|
1950
|
+
* @module contract/time
|
|
1951
|
+
*/
|
|
1952
|
+
/** Two-digit, zero-padded — the only formatting primitive the two renderers share. */
|
|
1953
|
+
function pad2$2(value) {
|
|
1954
|
+
return String(value).padStart(2, "0");
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* `'YYYY-MM-DD HH:MM:SS'` (SQLite UTC) or ISO → epoch ms; null/garbage → null.
|
|
1958
|
+
*
|
|
1959
|
+
* A stamp with NO zone designator is read as UTC, not as local time. `Date.parse` treats a zone-less
|
|
1960
|
+
* ISO string as LOCAL, and everything in this store is written in UTC (`CURRENT_TIMESTAMP`, and
|
|
1961
|
+
* {@link formatUtcTs}), so handing one over as-is would shift every quota window and every decay
|
|
1962
|
+
* measurement by the host's UTC offset — a fact reinforced at 23:00 UTC+8 would look reinforced 8
|
|
1963
|
+
* hours earlier, and the 24h cap would open or close at the wrong moment. An explicit `Z` or
|
|
1964
|
+
* `±HH:MM` is left alone (that is what makes a `formatOffsetIso` output safe to feed back in).
|
|
1965
|
+
*/
|
|
1966
|
+
function parseUtcTs(ts) {
|
|
1967
|
+
if (ts === null || ts === void 0 || ts === "") return null;
|
|
1968
|
+
const iso = ts.includes("T") ? ts : ts.replace(" ", "T");
|
|
1969
|
+
const zoned = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(iso) ? iso : `${iso}Z`;
|
|
1970
|
+
const ms = Date.parse(zoned);
|
|
1971
|
+
return Number.isFinite(ms) ? ms : null;
|
|
1972
|
+
}
|
|
1973
|
+
/** Epoch ms → `'YYYY-MM-DD HH:MM:SS'` (the SQLite `CURRENT_TIMESTAMP` shape, UTC). */
|
|
1974
|
+
function formatUtcTs(ms) {
|
|
1975
|
+
return new Date(ms).toISOString().slice(0, 19).replace("T", " ");
|
|
1976
|
+
}
|
|
1977
|
+
/** The host's offset at that instant, as `+HH:MM` / `-HH:MM` (DST-correct: read from the Date). */
|
|
1978
|
+
function offsetSuffix(at) {
|
|
1979
|
+
const minutesEast = -at.getTimezoneOffset();
|
|
1980
|
+
const sign = minutesEast < 0 ? "-" : "+";
|
|
1981
|
+
const abs = Math.abs(minutesEast);
|
|
1982
|
+
return `${sign}${pad2$2(Math.floor(abs / 60))}:${pad2$2(abs % 60)}`;
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* The MODEL face: an instant as ISO-8601 with an explicit offset, in the host's local zone
|
|
1986
|
+
* (`2026-10-28T19:58:48+08:00`).
|
|
1987
|
+
*
|
|
1988
|
+
* `null` — never `''` and never a fabricated stamp — when the value is absent or unreadable, so a
|
|
1989
|
+
* caller keeps its own "unknown" spelling (`''` for `created_at`, `null` for `updated_at`) instead of
|
|
1990
|
+
* rendering an empty time. `+00:00` (not `Z`) on a UTC host: the shape MUST carry a sign, which is
|
|
1991
|
+
* what a caller regex-checks.
|
|
1992
|
+
*/
|
|
1993
|
+
function formatOffsetIso(ts) {
|
|
1994
|
+
const ms = parseUtcTs(ts);
|
|
1995
|
+
if (ms === null) return null;
|
|
1996
|
+
const at = new Date(ms);
|
|
1997
|
+
return `${String(at.getFullYear())}-${pad2$2(at.getMonth() + 1)}-${pad2$2(at.getDate())}T${pad2$2(at.getHours())}:${pad2$2(at.getMinutes())}:${pad2$2(at.getSeconds())}` + offsetSuffix(at);
|
|
1998
|
+
}
|
|
1999
|
+
//#endregion
|
|
1537
2000
|
//#region lib/types/prompt.js
|
|
1538
2001
|
/**
|
|
1539
2002
|
* The one system-prompt section this plugin contributes (DESIGN §10).
|
|
@@ -1743,7 +2206,7 @@ function warn$1(log, message) {
|
|
|
1743
2206
|
log?.warn(message);
|
|
1744
2207
|
} catch {}
|
|
1745
2208
|
}
|
|
1746
|
-
function reasonOf$
|
|
2209
|
+
function reasonOf$3(error) {
|
|
1747
2210
|
return error instanceof Error ? error.message : String(error);
|
|
1748
2211
|
}
|
|
1749
2212
|
/**
|
|
@@ -1775,7 +2238,7 @@ async function loadGuarded(options, loadOnce, log, words) {
|
|
|
1775
2238
|
try {
|
|
1776
2239
|
return await loadOnce(options);
|
|
1777
2240
|
} catch (error) {
|
|
1778
|
-
warn$1(log, `${words.prefix} WARNING — environment initialisation failed (${reasonOf$
|
|
2241
|
+
warn$1(log, `${words.prefix} WARNING — environment initialisation failed (${reasonOf$3(error)}); ${words.loadFailure}`);
|
|
1779
2242
|
return;
|
|
1780
2243
|
}
|
|
1781
2244
|
}
|
|
@@ -1844,6 +2307,12 @@ function stillActive(ctx) {
|
|
|
1844
2307
|
* `agent/pre-step` listener), and the harness shows such a message in the trace as coming from the
|
|
1845
2308
|
* user. Without an author, a line like "记忆里有相关内容" reads as something the user said. The
|
|
1846
2309
|
* `[avantf-mem]` prefix is what keeps the provenance honest.
|
|
2310
|
+
*
|
|
2311
|
+
* The step's injection is not only this hint: the plugin's DEPENDENCY NOTICES (the `audit` entries of
|
|
2312
|
+
* the shipped `res/runtime-deps.yaml`) ride in the same message ({@link stepInjection}). They are a
|
|
2313
|
+
* different producer of text with the same delivery rule — at most once per owner session, only on a
|
|
2314
|
+
* step that already carried a user message — so merging them keeps ONE message per step instead of
|
|
2315
|
+
* two competing ones.
|
|
1847
2316
|
*/
|
|
1848
2317
|
const RELEVANCE_HINT = "[avantf-mem] 记忆里有与上条用户消息相关的内容;需要时用 `mem_recall` 检索。";
|
|
1849
2318
|
/**
|
|
@@ -1879,6 +2348,92 @@ function messageText(message) {
|
|
|
1879
2348
|
return b?.type === "text" && typeof b.text === "string" ? b.text : "";
|
|
1880
2349
|
}).filter(Boolean).join("\n");
|
|
1881
2350
|
}
|
|
2351
|
+
/**
|
|
2352
|
+
* The text of the ONE message this step injects: the session notices, then the memory hint.
|
|
2353
|
+
*
|
|
2354
|
+
* `''` means "inject nothing" — the loop's turn-end guard is `decision.messages.length === 0`, so an
|
|
2355
|
+
* empty injection has to stay empty rather than become an empty user message.
|
|
2356
|
+
*
|
|
2357
|
+
* Order: notices first. They report a capability that is not working (a repair the operator asked for
|
|
2358
|
+
* did not finish), which is the louder statement; the hint is an invitation. Neither repeats within a
|
|
2359
|
+
* session: the notice registry latches per session id, and the hint is one line by construction.
|
|
2360
|
+
*
|
|
2361
|
+
* Each body is used AS CONFIGURED, with one exception that is about YAML and not about editing: a
|
|
2362
|
+
* block scalar (`body: |`) ends with a newline, and keeping it would insert a blank line between the
|
|
2363
|
+
* notice and the hint. Trailing whitespace is therefore dropped; nothing else is rewritten, and a
|
|
2364
|
+
* whitespace-only body is skipped rather than rendered as an empty paragraph.
|
|
2365
|
+
*/
|
|
2366
|
+
function stepInjection(hint, notices) {
|
|
2367
|
+
const lines = [];
|
|
2368
|
+
for (const line of notices) {
|
|
2369
|
+
if (typeof line !== "string") continue;
|
|
2370
|
+
const body = line.trimEnd();
|
|
2371
|
+
if (body.trim() === "") continue;
|
|
2372
|
+
lines.push(body);
|
|
2373
|
+
}
|
|
2374
|
+
if (hint) lines.push(RELEVANCE_HINT);
|
|
2375
|
+
return lines.join("\n");
|
|
2376
|
+
}
|
|
2377
|
+
/**
|
|
2378
|
+
* The session-notice lines for this step, or `[]` when there are none to deliver.
|
|
2379
|
+
*
|
|
2380
|
+
* Three rules, all of them the caller's to make and this plugin's to keep:
|
|
2381
|
+
*
|
|
2382
|
+
* 1. **`ownerOnly` is a CLAIM, not a lookup.** The base registry cannot know which session owns the
|
|
2383
|
+
* plugin, so it serves only a call that claims it ({@link isOwnerSession} decides), and a
|
|
2384
|
+
* non-claiming call latches nothing — a worker/subagent session therefore never consumes the line
|
|
2385
|
+
* out from under the owner session.
|
|
2386
|
+
* 2. **Nothing is cached here.** `consume()` re-reads the item's real state on every step, so a
|
|
2387
|
+
* dependency that healed stops matching and a NEW session simply sees nothing — "fixed ⇒ silent"
|
|
2388
|
+
* needs no invalidation of its own.
|
|
2389
|
+
* 3. **Never throws.** This runs on the prompt path of a live agent: a hostile/mismatched registry
|
|
2390
|
+
* costs one skipped line, never the step.
|
|
2391
|
+
*/
|
|
2392
|
+
function noticeLines(registry, sessionId, owner) {
|
|
2393
|
+
if (registry === void 0 || sessionId === "") return [];
|
|
2394
|
+
try {
|
|
2395
|
+
const lines = registry.consume(sessionId, { ownerOnly: owner });
|
|
2396
|
+
if (!Array.isArray(lines)) return [];
|
|
2397
|
+
return lines.filter((line) => typeof line === "string");
|
|
2398
|
+
} catch {
|
|
2399
|
+
return [];
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* Whether `sessionId` is the OWNER session — a TOP-LEVEL agent, not a worker/subagent.
|
|
2404
|
+
*
|
|
2405
|
+
* Read STRUCTURALLY off the host's agent registry (`ctx.agents`), deliberately without a dependency
|
|
2406
|
+
* on `@deepseek-ai/dsh-agent`: the only question is "is this id one of the roots", and the answer
|
|
2407
|
+
* decides a claim the base registry would otherwise have to invent a role for. A subagent session is
|
|
2408
|
+
* created under its parent's context, so it is never a root — and a mission executor is a subagent
|
|
2409
|
+
* too. Those sessions keep working (memory tools, retrieval), they just do not receive the owner's
|
|
2410
|
+
* repair notice.
|
|
2411
|
+
*
|
|
2412
|
+
* "Cannot tell" DELIVERS (`true`): a host with no agent registry, an older registry without `roots()`,
|
|
2413
|
+
* a throwing read — none of those prove the session is a worker, and swallowing a real notice on a
|
|
2414
|
+
* guess would be the worse failure. This mirrors the family rule that only a PROVEN condition may
|
|
2415
|
+
* take a capability away.
|
|
2416
|
+
*/
|
|
2417
|
+
function isOwnerSession(agents, sessionId) {
|
|
2418
|
+
if (sessionId === "") return false;
|
|
2419
|
+
let roots;
|
|
2420
|
+
try {
|
|
2421
|
+
roots = agents?.roots;
|
|
2422
|
+
} catch {
|
|
2423
|
+
return true;
|
|
2424
|
+
}
|
|
2425
|
+
if (typeof roots !== "function") return true;
|
|
2426
|
+
try {
|
|
2427
|
+
const list = roots.call(agents);
|
|
2428
|
+
if (!Array.isArray(list)) return true;
|
|
2429
|
+
return list.some((agent) => {
|
|
2430
|
+
const id = agent?.id;
|
|
2431
|
+
return id !== void 0 && id !== null && String(id) === sessionId;
|
|
2432
|
+
});
|
|
2433
|
+
} catch {
|
|
2434
|
+
return true;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
1882
2437
|
//#endregion
|
|
1883
2438
|
//#region ../core/lib/config/paths.js
|
|
1884
2439
|
/**
|
|
@@ -1966,6 +2521,11 @@ const DEFAULT_COMMON_CONFIG = `# avantf-mem 公共配置(分层第②层)。
|
|
|
1966
2521
|
# dim: 768
|
|
1967
2522
|
# auto_download: true
|
|
1968
2523
|
# auto_migrate: true # 换模型/宽度后自动分批重算旧空间的向量(默认开);关掉只剩启动告警 + vectors --fix
|
|
2524
|
+
# self_heal: # 语义依赖自愈(默认关):允许启动期安装缺失的 @huggingface/transformers
|
|
2525
|
+
# enabled: false # 打开后走底座 runtime-deps 共享根(base ≥ 0.7.0 / interface v6);失败只降级到 FTS+实体
|
|
2526
|
+
# # 装哪个包 / 从哪装 / 提示说什么:见插件自带的 res/runtime-deps.yaml(随包发布)
|
|
2527
|
+
# native_self_heal: # 原生可选依赖自愈:nodejieba / hnswlib-node(默认关)
|
|
2528
|
+
# enabled: false # 装哪个包、从哪装、提示说什么,都由插件自带的 res/runtime-deps.yaml 决定
|
|
1969
2529
|
# ⚠️ cache_dir / mirror 由家族底座 @avantf/dsh-plugin-base 管理:写在这里会被忽略并告警。
|
|
1970
2530
|
# 缓存目录用环境变量 AVANTF_MEM_MODEL_CACHE,镜像用 AVANTF_MEM_MODEL_MIRROR(或 HF_ENDPOINT)。
|
|
1971
2531
|
|
|
@@ -2003,8 +2563,13 @@ const DEFAULT_COMMON_CONFIG = `# avantf-mem 公共配置(分层第②层)。
|
|
|
2003
2563
|
# purge_after_archived_days: 365 # 归档事实的物理清理窗口(活跃日)
|
|
2004
2564
|
# contradiction_threshold: 0.6
|
|
2005
2565
|
|
|
2006
|
-
# trust:
|
|
2007
|
-
#
|
|
2566
|
+
# trust: # 信任与遗忘:旋钮是「天数 + 形状」,不是斜率
|
|
2567
|
+
# base_lifetime_days: 90 # 锚点:0.5 起点 → 0 的活跃日数(新事实不被召回时的寿命)
|
|
2568
|
+
# max_retention_days: 174 # 召回加强最多能换来的活跃日数;ceiling 由它反解(accelerating 下 ≈ 0.7492)
|
|
2569
|
+
# decay_shape: accelerating # accelerating(默认)| linear(回滚档:配 153 时 ceiling 回到 0.85)
|
|
2570
|
+
# decay_curvature: 0.05 # accelerating 的下限项 a,合法区间 (0, 0.5];linear 下不参与
|
|
2571
|
+
# recall_gain_below_base: 0.1 # 低于基准 0.5 时每次召回加多少(封顶基准:0.05 起 5 次到 0.5)
|
|
2572
|
+
# recall_gain_above_base: 0.01 # 高于基准 0.5 时每次召回加多少(封顶上面反解出的 ceiling)
|
|
2008
2573
|
# presence:
|
|
2009
2574
|
# heartbeat_minutes: 60 # 常驻进程的心跳(活跃日时钟与生命周期扫描)
|
|
2010
2575
|
|
|
@@ -2107,16 +2672,60 @@ function zodShape(schema) {
|
|
|
2107
2672
|
}
|
|
2108
2673
|
}
|
|
2109
2674
|
/**
|
|
2675
|
+
* The `trust.*` keys the v2 key replacement deleted (plan §4.1/§4.3), each with the sentence that
|
|
2676
|
+
* tells the operator what to write instead.
|
|
2677
|
+
*
|
|
2678
|
+
* They are still "unknown keys" — the config loader's existing channel — but the generic
|
|
2679
|
+
* `unknown config key(s) ignored: trust.decay_per_day` would silently change a lifetime the operator
|
|
2680
|
+
* had computed on purpose (`decay_per_day` → "how many days is my memory good for?" is exactly the
|
|
2681
|
+
* number that must not drift). Each message therefore names the replacement AND the conversion, computed
|
|
2682
|
+
* from the SHIPPED defaults (`base_lifetime_days: 90`, `decay_curvature: 0.05`) with the same
|
|
2683
|
+
* `trust_curve` functions the runtime uses.
|
|
2684
|
+
*
|
|
2685
|
+
* The default curve is read from `defaultConfig`, not written out as literals: if the shipped
|
|
2686
|
+
* anchor/curvature moves, these messages move with it instead of quoting stale numbers.
|
|
2687
|
+
*/
|
|
2688
|
+
const trustKeyWarning = (() => {
|
|
2689
|
+
const shipped = curveOf(defaultConfig.trust);
|
|
2690
|
+
const asDays = (shape, ceiling) => {
|
|
2691
|
+
const days = retentionDaysForCeiling(ceiling, {
|
|
2692
|
+
...shipped,
|
|
2693
|
+
shape
|
|
2694
|
+
});
|
|
2695
|
+
return Number.isFinite(days) ? String(Math.round(days)) : "?";
|
|
2696
|
+
};
|
|
2697
|
+
return {
|
|
2698
|
+
start: () => "trust.start is gone (v2): 起点冻结为 0.5(它与锚点 base_lifetime_days、召回基准是同一个数,不再可配,D20)。这一行删掉即可。",
|
|
2699
|
+
decay_per_day: () => "trust.decay_per_day is gone (v2): 衰减形状由 trust.decay_shape 决定,\"能记多久\"用 trust.base_lifetime_days 与 trust.max_retention_days 表达。默认配置下一条 0.5 起点的事实仍然是 90 个活跃日归零——如果你的旧值就是为了这个,删掉即可。",
|
|
2700
|
+
recall_floor: () => "trust.recall_floor is gone (v2): 基准 0.5 冻结;\"低于基准先爬回基准\"由 trust.recall_gain_below_base 表达(默认 0.1 ⇒ 0.05 起五次召回回到 0.5,不是一次)。这一行删掉即可。",
|
|
2701
|
+
recall_delta: () => "trust.recall_delta is gone (v2): 召回加强改成分段增益——低于基准用 trust.recall_gain_below_base(默认 0.1,封顶基准)、高于基准用 trust.recall_gain_above_base(默认 0.01,仍乘 trust.recall_marginal_decay);两者都不再等于旧的 0.03 一档。",
|
|
2702
|
+
recall_ceiling: (value) => {
|
|
2703
|
+
const ceiling = Number(value);
|
|
2704
|
+
return `trust.recall_ceiling is gone (v2): 召回上限现在用"寿命"表达。你写的 ${String(value)} 等价于 trust.max_retention_days: ${asDays("linear", ceiling)}(decay_shape: linear) / ${asDays("accelerating", ceiling)}(decay_shape: accelerating,默认 a=0.05);ceiling 由 max_retention_days 反解,不再可配。`;
|
|
2705
|
+
}
|
|
2706
|
+
};
|
|
2707
|
+
})();
|
|
2708
|
+
/** `trust.<key>` → the version-specific message, or `undefined` for a genuine typo. */
|
|
2709
|
+
function deprecatedTrustKey(key, value) {
|
|
2710
|
+
if (!key.startsWith("trust.")) return void 0;
|
|
2711
|
+
const explain = trustKeyWarning[key.slice(6)];
|
|
2712
|
+
return explain === void 0 ? void 0 : explain(value);
|
|
2713
|
+
}
|
|
2714
|
+
/**
|
|
2110
2715
|
* Warn once about unknown config keys — at the ROOT level and inside known sections
|
|
2111
2716
|
* (TRUST_MODEL.md §7 / review R15+R23). This is generic typo protection, not legacy
|
|
2112
2717
|
* compatibility: `vector_store` or `semantics` are exactly the mistakes it catches.
|
|
2113
2718
|
*
|
|
2719
|
+
* A DELETED key earns its own sentence ({@link trustKeyWarning}): it is not a typo, and the operator
|
|
2720
|
+
* needs the replacement + the conversion, not "check the spelling".
|
|
2721
|
+
*
|
|
2114
2722
|
* Goes to the injected logger, not the process-wide default: this is the one message
|
|
2115
2723
|
* that tells an operator their config file is being partly ignored, and the DSH host
|
|
2116
2724
|
* only surfaces what the plugin's logger carries.
|
|
2117
2725
|
*/
|
|
2118
2726
|
function warnUnknownKeys(source, raw, rootShape, logger) {
|
|
2119
2727
|
const unknown = [];
|
|
2728
|
+
const explained = [];
|
|
2120
2729
|
for (const [key, value] of Object.entries(raw)) {
|
|
2121
2730
|
const section = rootShape[key];
|
|
2122
2731
|
if (section === void 0) {
|
|
@@ -2125,8 +2734,15 @@ function warnUnknownKeys(source, raw, rootShape, logger) {
|
|
|
2125
2734
|
}
|
|
2126
2735
|
const childShape = zodShape(section);
|
|
2127
2736
|
if (childShape === void 0 || !isPlainObject(value)) continue;
|
|
2128
|
-
for (const childKey of Object.keys(value))
|
|
2737
|
+
for (const childKey of Object.keys(value)) {
|
|
2738
|
+
const full = `${key}.${childKey}`;
|
|
2739
|
+
if (childShape[childKey] !== void 0) continue;
|
|
2740
|
+
const message = deprecatedTrustKey(full, value[childKey]);
|
|
2741
|
+
if (message === void 0) unknown.push(full);
|
|
2742
|
+
else explained.push(message);
|
|
2743
|
+
}
|
|
2129
2744
|
}
|
|
2745
|
+
for (const message of explained) logger.warn(`${source}: ${message}`);
|
|
2130
2746
|
if (unknown.length > 0) logger.warn(`${source}: unknown config key(s) ignored: ${unknown.join(", ")} — check the spelling against docs/DESIGN.md §3`);
|
|
2131
2747
|
}
|
|
2132
2748
|
/** Section-aware merge: top-level sections merge one level deep, scalars/arrays replace. */
|
|
@@ -2688,6 +3304,49 @@ function ensureAllAsync(options) {
|
|
|
2688
3304
|
//#endregion
|
|
2689
3305
|
//#region ../core/lib/entities/extract.js
|
|
2690
3306
|
/**
|
|
3307
|
+
* Entity extraction: POS tagging and the noun-ish names that become `entities` rows.
|
|
3308
|
+
*
|
|
3309
|
+
* Uses nodejieba (optional dependency) for POS; degrades gracefully to a character/regex fallback
|
|
3310
|
+
* when unavailable, exactly like the `jieba_available()` guard.
|
|
3311
|
+
*
|
|
3312
|
+
* Note: nodejieba's `tag()` returns `{ word, tag }` objects, and its POS flag set
|
|
3313
|
+
* differs from Python jieba (e.g. proper nouns → `nr`, 名动词 → `vn`).
|
|
3314
|
+
*
|
|
3315
|
+
* WHAT THIS MODULE IS NOT. It owns what is an entity NAME — nothing about relations. The relation
|
|
3316
|
+
* half (SVO candidate triples, explicit patterns, the name/statement/confidence gates and the
|
|
3317
|
+
* predicate vocabulary) lives in `relations/`; the two halves share exactly one thing, the tagging
|
|
3318
|
+
* pass ({@link tagText}), so a write pays for the dictionary once and drives both.
|
|
3319
|
+
*
|
|
3320
|
+
* WHERE THE MODULE COMES FROM. `nodejieba` is an optional npm dependency, so it is normally resolved
|
|
3321
|
+
* from this tree. A host that provisions it instead (the DSH plugin's `runtime-deps` self-heal) sets
|
|
3322
|
+
* the shared root with {@link useTokenizerModuleDir}; the load then resolves from THAT root first and
|
|
3323
|
+
* falls back to this tree, which is the pre-existing behaviour and also the recovery path for a root
|
|
3324
|
+
* that was garbage-collected or damaged. Nothing here decides whether a root should exist.
|
|
3325
|
+
*/
|
|
3326
|
+
/**
|
|
3327
|
+
* The shared module root the tokenizer should be loaded from, when the caller provisioned one.
|
|
3328
|
+
*
|
|
3329
|
+
* `undefined` (the default, and every CLI/MCP/test caller) = this tree's own optional dependency.
|
|
3330
|
+
* Set by {@link useTokenizerModuleDir} from the DSH plugin once the native self-heal item is READY;
|
|
3331
|
+
* a failed resolution from it falls back to this tree rather than degrading the tokenizer.
|
|
3332
|
+
*/
|
|
3333
|
+
let moduleRoot$1;
|
|
3334
|
+
/**
|
|
3335
|
+
* Point the optional tokenizer at a shared module root (the base `runtime-deps` handle's directory).
|
|
3336
|
+
*
|
|
3337
|
+
* The memo follows the ROOT: changing it forgets whatever the previous root produced, so the next
|
|
3338
|
+
* load resolves from the new one — that is the whole point of setting a root. The cost is at most one
|
|
3339
|
+
* extra dictionary parse (~1 s, once) when a root arrives after a lazily loaded copy from this tree;
|
|
3340
|
+
* Node's own module cache keeps the import itself cheap. The plugin only sets this once, when its
|
|
3341
|
+
* native self-heal item settles.
|
|
3342
|
+
*/
|
|
3343
|
+
function useTokenizerModuleDir(dir) {
|
|
3344
|
+
const next = dir === "" ? void 0 : dir;
|
|
3345
|
+
if (next === moduleRoot$1) return;
|
|
3346
|
+
moduleRoot$1 = next;
|
|
3347
|
+
jiebaLoad = void 0;
|
|
3348
|
+
}
|
|
3349
|
+
/**
|
|
2691
3350
|
* The load, memoized as a PROMISE — not as its result.
|
|
2692
3351
|
*
|
|
2693
3352
|
* `mod.load()` parses nodejieba's dictionary synchronously on the main thread (~1 s measured), and
|
|
@@ -2698,28 +3357,67 @@ function ensureAllAsync(options) {
|
|
|
2698
3357
|
* time — measured 2143 ms for two concurrent callers against 1000 ms for one.
|
|
2699
3358
|
*
|
|
2700
3359
|
* A FAILED load is cached too (the same one-attempt policy as before): a process whose jieba cannot
|
|
2701
|
-
* be imported uses the regex fallback for its lifetime rather than retrying on every write.
|
|
3360
|
+
* be imported uses the regex fallback for its lifetime rather than retrying on every write. The one
|
|
3361
|
+
* exception is a module root that arrives afterwards — see {@link useTokenizerModuleDir}.
|
|
2702
3362
|
*/
|
|
2703
3363
|
let jiebaLoad;
|
|
2704
3364
|
/** How many times the dictionary was actually parsed in this process (see `jiebaLoadAttempts`). */
|
|
2705
3365
|
let jiebaAttempts = 0;
|
|
3366
|
+
/** The root the LAST attempt resolved from (`undefined` = this tree); reported by {@link jiebaStatus}. */
|
|
3367
|
+
let jiebaAttemptedRoot;
|
|
3368
|
+
/** Why the last attempt failed, one short line — what makes an "unavailable" log line diagnosable. */
|
|
3369
|
+
let jiebaError;
|
|
2706
3370
|
function loadJieba() {
|
|
2707
3371
|
jiebaLoad ??= loadJiebaOnce();
|
|
2708
3372
|
return jiebaLoad;
|
|
2709
3373
|
}
|
|
3374
|
+
/**
|
|
3375
|
+
* Import `nodejieba` from the shared root, else from this tree.
|
|
3376
|
+
*
|
|
3377
|
+
* The shared root's own `package.json` is the resolution anchor (the same rule the semantic adapter
|
|
3378
|
+
* uses for transformers.js): its `node_modules` is what the provider installed, and importing by FILE
|
|
3379
|
+
* URL means this root's copy is the one loaded even when this tree also carries the package. A root
|
|
3380
|
+
* that no longer resolves is not an error to report here — the fallback below is the behaviour, and
|
|
3381
|
+
* the caller's dependency audit is what tells an operator about it.
|
|
3382
|
+
*/
|
|
3383
|
+
async function importTokenizer(root) {
|
|
3384
|
+
if (root !== void 0) try {
|
|
3385
|
+
return await import(pathToFileURL(createRequire(join(root, "package.json")).resolve("nodejieba")).href);
|
|
3386
|
+
} catch {}
|
|
3387
|
+
return await import("nodejieba");
|
|
3388
|
+
}
|
|
3389
|
+
/** nodejieba's export shape, normalised (CJS through `import()` may land on `default`). */
|
|
3390
|
+
function tokenizerOf(raw) {
|
|
3391
|
+
const mod = typeof raw?.tag === "function" ? raw : raw?.default ?? raw;
|
|
3392
|
+
return mod !== null && typeof mod?.tag === "function" ? mod : null;
|
|
3393
|
+
}
|
|
2710
3394
|
async function loadJiebaOnce() {
|
|
2711
3395
|
jiebaAttempts += 1;
|
|
3396
|
+
const root = moduleRoot$1;
|
|
3397
|
+
jiebaAttemptedRoot = root;
|
|
2712
3398
|
try {
|
|
2713
|
-
const
|
|
2714
|
-
|
|
3399
|
+
const mod = tokenizerOf(await importTokenizer(root));
|
|
3400
|
+
if (mod === null) throw new Error("nodejieba 的 tag() 不可用");
|
|
2715
3401
|
if (typeof mod.load === "function") mod.load();
|
|
3402
|
+
jiebaError = void 0;
|
|
2716
3403
|
return mod;
|
|
2717
|
-
} catch {
|
|
3404
|
+
} catch (error) {
|
|
3405
|
+
jiebaError = error instanceof Error ? error.message : String(error);
|
|
3406
|
+
if (moduleRoot$1 !== root) jiebaLoad = void 0;
|
|
2718
3407
|
return null;
|
|
2719
3408
|
}
|
|
2720
3409
|
}
|
|
2721
|
-
|
|
2722
|
-
|
|
3410
|
+
/** The status of the memoized load, starting one only when none has run yet. */
|
|
3411
|
+
async function jiebaProbe() {
|
|
3412
|
+
await loadJieba();
|
|
3413
|
+
const load = jiebaLoad;
|
|
3414
|
+
return {
|
|
3415
|
+
available: load !== void 0 && await load !== null,
|
|
3416
|
+
attempts: jiebaAttempts,
|
|
3417
|
+
attemptedRoot: jiebaAttemptedRoot,
|
|
3418
|
+
root: moduleRoot$1,
|
|
3419
|
+
error: jiebaError
|
|
3420
|
+
};
|
|
2723
3421
|
}
|
|
2724
3422
|
const NOUN_FLAGS = /* @__PURE__ */ new Set([
|
|
2725
3423
|
"n",
|
|
@@ -2899,6 +3597,32 @@ async function awaitStartupGate(options = {}) {
|
|
|
2899
3597
|
return await whenEventLoopIdle();
|
|
2900
3598
|
}
|
|
2901
3599
|
/**
|
|
3600
|
+
* The line one tokenizer observation deserves.
|
|
3601
|
+
*
|
|
3602
|
+
* `previous` is what makes the pair readable: a `ready` after a `degraded` is a RECOVERY (the
|
|
3603
|
+
* earlier probe ran before a loadable copy existed — the provisioned root had not been handed over,
|
|
3604
|
+
* or the package was not installed yet), not a change of story. A `degraded` line never claims more
|
|
3605
|
+
* than it knows: it names the reason and the two ways to fix it, and says that a loadable copy
|
|
3606
|
+
* arriving later is picked up automatically.
|
|
3607
|
+
*
|
|
3608
|
+
* Exported for the test that pins those transitions; it has no side effects.
|
|
3609
|
+
*/
|
|
3610
|
+
function tokenizerReport(status, elapsedMs, previous) {
|
|
3611
|
+
if (status.available) return {
|
|
3612
|
+
level: "info",
|
|
3613
|
+
message: `model bootstrap: tokenizer ready in ${elapsedMs}ms (nodejieba${status.attemptedRoot === void 0 ? "" : ", provisioned root"})${previous === "degraded" ? " — recovered: the earlier \"unavailable\" line described a probe that ran before a loadable copy existed, not this state" : ""}`,
|
|
3614
|
+
next: "ready"
|
|
3615
|
+
};
|
|
3616
|
+
const reason = status.error === void 0 || status.error === "" ? "nodejieba could not be loaded" : status.error;
|
|
3617
|
+
return {
|
|
3618
|
+
level: "warn",
|
|
3619
|
+
message: `model bootstrap: nodejieba unavailable in ${elapsedMs}ms — ${status.attemptedRoot === void 0 ? `no loadable nodejieba in this tree (${reason})` : `the provisioned root ${status.attemptedRoot} cannot load nodejieba (${reason})`}${previous === "ready" ? " (the copy that was loadable earlier is no longer resolvable — a provisioned root replaced it, or it was removed)" : ""}; entity/triple extraction runs on the regex path (weaker entity + jaccard legs) until a loadable copy exists — install nodejieba, or enable semantic.native_self_heal to have the native item provision it (a provisioned root that arrives later is picked up automatically)`,
|
|
3620
|
+
next: "degraded"
|
|
3621
|
+
};
|
|
3622
|
+
}
|
|
3623
|
+
/** The last verdict reported in THIS process; see {@link TokenizerVerdict}. */
|
|
3624
|
+
let tokenizerVerdict = "unknown";
|
|
3625
|
+
/**
|
|
2902
3626
|
* Warm ONLY the tokenizer, after the host-readiness signal and (when asked) an idle event loop.
|
|
2903
3627
|
*
|
|
2904
3628
|
* nodejieba loads its dictionary lazily on the FIRST `tag()` (measured ~950 ms, after which a call
|
|
@@ -2914,8 +3638,10 @@ async function warmTokenizer(rt, options = {}) {
|
|
|
2914
3638
|
const waited = await awaitStartupGate(options);
|
|
2915
3639
|
if (options.deferTokenizerUntilIdle === true && waited >= 200) rt.logger.info(`model bootstrap: tokenizer warm waited ${waited}ms for an idle event loop`);
|
|
2916
3640
|
const jiebaStarted = Date.now();
|
|
2917
|
-
|
|
2918
|
-
|
|
3641
|
+
const report = tokenizerReport(await jiebaProbe(), Date.now() - jiebaStarted, tokenizerVerdict);
|
|
3642
|
+
tokenizerVerdict = report.next;
|
|
3643
|
+
if (report.level === "warn") rt.logger.warn(report.message);
|
|
3644
|
+
else rt.logger.info(report.message);
|
|
2919
3645
|
}
|
|
2920
3646
|
/**
|
|
2921
3647
|
* Non-blocking embedding warm, for a caller that knows the model files are already on disk.
|
|
@@ -3097,7 +3823,7 @@ async function runToolSpec$1(rt, spec, args) {
|
|
|
3097
3823
|
//#region ../core/lib/trustHeartbeat.js
|
|
3098
3824
|
/**
|
|
3099
3825
|
* The trust heartbeat: the interval beat that advances the active-day clock and performs the
|
|
3100
|
-
* settle / forget /
|
|
3826
|
+
* settle / forget / purge sweeps (TRUST_MODEL.md §5), shared by the DSH plugin and the MCP
|
|
3101
3827
|
* server.
|
|
3102
3828
|
*
|
|
3103
3829
|
* The two surfaces used to install it separately, and the copies drifted in exactly the places a
|
|
@@ -3525,6 +4251,69 @@ function truncateToTokens(text, maxTokens, opts = {}) {
|
|
|
3525
4251
|
};
|
|
3526
4252
|
}
|
|
3527
4253
|
/**
|
|
4254
|
+
* Slice `text` into windows of at most `maxTokens` estimated tokens instead of truncating it.
|
|
4255
|
+
*
|
|
4256
|
+
* WHY THIS EXISTS. The window belongs to the loaded model and the call site cannot widen it, so a
|
|
4257
|
+
* text past it has exactly one of two fates: cut (what {@link truncateToTokens} does, and what
|
|
4258
|
+
* transformers.js does silently on its own) or split. A knowledge chunk of ~1000 characters against
|
|
4259
|
+
* a 512-token model lost half its content from the vector that way — measured in production as
|
|
4260
|
+
* "503 tokens dropped here". Splitting keeps every token in the representation; the adapter that
|
|
4261
|
+
* can only return ONE vector then pools the window vectors (`LocalBgeBackend.encode`).
|
|
4262
|
+
*
|
|
4263
|
+
* Every returned window satisfies `estimate(window) <= maxTokens` (a one-character window is the
|
|
4264
|
+
* floor; the estimator cannot exceed a limit of 1 token for a single character). Cuts land on a
|
|
4265
|
+
* sentence/clause boundary when one is in the window's final quarter, so a window does not start or
|
|
4266
|
+
* end mid-word.
|
|
4267
|
+
*
|
|
4268
|
+
* @param text - the input.
|
|
4269
|
+
* @param maxTokens - the per-window budget (already net of special tokens, as the adapter computes it).
|
|
4270
|
+
* @param opts - estimator override (tests use a flat one); defaults to {@link estimateTokens}.
|
|
4271
|
+
*/
|
|
4272
|
+
function splitToWindows(text, maxTokens, opts = {}) {
|
|
4273
|
+
const estimate = opts.estimate ?? estimateTokens;
|
|
4274
|
+
const limit = Math.max(1, Math.floor(maxTokens));
|
|
4275
|
+
const tokens = estimate(text);
|
|
4276
|
+
const chars = Array.from(text);
|
|
4277
|
+
if (chars.length === 0) return {
|
|
4278
|
+
windows: [text],
|
|
4279
|
+
tokens,
|
|
4280
|
+
windowTokens: [tokens]
|
|
4281
|
+
};
|
|
4282
|
+
if (tokens <= limit) return {
|
|
4283
|
+
windows: [text],
|
|
4284
|
+
tokens,
|
|
4285
|
+
windowTokens: [tokens]
|
|
4286
|
+
};
|
|
4287
|
+
const windows = [];
|
|
4288
|
+
const windowTokens = [];
|
|
4289
|
+
let start = 0;
|
|
4290
|
+
while (start < chars.length) {
|
|
4291
|
+
let used = 0;
|
|
4292
|
+
let end = start;
|
|
4293
|
+
while (end < chars.length) {
|
|
4294
|
+
const cost = CJK.test(chars[end]) ? 1 : chars[end].trim() === "" ? 0 : .25;
|
|
4295
|
+
if (used + cost > limit && end > start) break;
|
|
4296
|
+
used += cost;
|
|
4297
|
+
end += 1;
|
|
4298
|
+
}
|
|
4299
|
+
const floor = start + Math.floor((end - start) * .75);
|
|
4300
|
+
for (let i = end - 1; i >= floor; i--) if (BREAK.has(chars[i])) {
|
|
4301
|
+
end = i + 1;
|
|
4302
|
+
break;
|
|
4303
|
+
}
|
|
4304
|
+
while (end - start > 1 && estimate(chars.slice(start, end).join("")) > limit) end -= 1;
|
|
4305
|
+
const window = chars.slice(start, end).join("");
|
|
4306
|
+
windows.push(window);
|
|
4307
|
+
windowTokens.push(estimate(window));
|
|
4308
|
+
start = end;
|
|
4309
|
+
}
|
|
4310
|
+
return {
|
|
4311
|
+
windows,
|
|
4312
|
+
tokens,
|
|
4313
|
+
windowTokens
|
|
4314
|
+
};
|
|
4315
|
+
}
|
|
4316
|
+
/**
|
|
3528
4317
|
* Resolve the effective input window for a loaded model.
|
|
3529
4318
|
*
|
|
3530
4319
|
* @param configured - `0` (or negative) means "auto: use the model's declared window".
|
|
@@ -3548,6 +4337,14 @@ function declaredWindowOf(pipe) {
|
|
|
3548
4337
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > SENTINEL_WINDOW) return void 0;
|
|
3549
4338
|
return Math.floor(value);
|
|
3550
4339
|
}
|
|
4340
|
+
/**
|
|
4341
|
+
* Characters of a chunk that fit the window for `window` tokens, minus the per-chunk
|
|
4342
|
+
* overhead (`specialTokens` for CLS/SEP). Used to derive `knowledge.chunk_size` so the
|
|
4343
|
+
* default chunking is justified by the shipped model's window instead of by feel.
|
|
4344
|
+
*/
|
|
4345
|
+
function windowChars(window, specialTokens = 2) {
|
|
4346
|
+
return Math.max(1, Math.floor((window - specialTokens) / 1));
|
|
4347
|
+
}
|
|
3551
4348
|
//#endregion
|
|
3552
4349
|
//#region ../retrieval-core/lib/budget.js
|
|
3553
4350
|
/**
|
|
@@ -3760,6 +4557,10 @@ var LocalBgeBackend = class {
|
|
|
3760
4557
|
};
|
|
3761
4558
|
return this._representation;
|
|
3762
4559
|
}
|
|
4560
|
+
/** Point the loader at a shared runtime-dependency root before the first warmup. */
|
|
4561
|
+
useModuleDir(dir) {
|
|
4562
|
+
this.env.moduleDir = dir;
|
|
4563
|
+
}
|
|
3763
4564
|
/**
|
|
3764
4565
|
* Fire-and-forget retry for call sites that only observe availability. No-op
|
|
3765
4566
|
* while the model is ready or an attempt is already in flight; otherwise
|
|
@@ -3808,42 +4609,118 @@ var LocalBgeBackend = class {
|
|
|
3808
4609
|
return this._window;
|
|
3809
4610
|
}
|
|
3810
4611
|
/**
|
|
3811
|
-
*
|
|
4612
|
+
* The window-sized slices `text` is encoded in (see `text_budget.splitToWindows`).
|
|
3812
4613
|
*
|
|
3813
|
-
*
|
|
3814
|
-
*
|
|
3815
|
-
*
|
|
3816
|
-
*
|
|
4614
|
+
* WHERE THE BOUNDARY CAN LIVE. The feature-extraction pipeline's `_call` accepts
|
|
4615
|
+
* `{pooling, normalize, quantize, precision}` and nothing else, so `max_length` cannot be raised
|
|
4616
|
+
* from the call site — past `model_max_length` transformers.js truncates and says nothing. This
|
|
4617
|
+
* adapter is therefore the only place that can apply the model's own window, and it SPLITS rather
|
|
4618
|
+
* than cuts: a text past the window is a document chunk (or a long fact) whose tail is content,
|
|
4619
|
+
* and cutting it dropped 503 of ~1015 tokens out of the vector in production.
|
|
3817
4620
|
*/
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
if (out.truncated) {
|
|
3821
|
-
recordTruncation("embedding");
|
|
3822
|
-
if (!this._lengthWarned) {
|
|
3823
|
-
this._lengthWarned = true;
|
|
3824
|
-
retrievalLogger().warn(`semantic: input exceeded the ${this.window()}-token window of ${this.model} — text is truncated before encoding (${out.omittedTokens} tokens dropped here); check knowledge.chunk_size and semantic.max_input_tokens`);
|
|
3825
|
-
}
|
|
3826
|
-
}
|
|
3827
|
-
return out;
|
|
4621
|
+
windows(text) {
|
|
4622
|
+
return splitToWindows(text, Math.max(1, this.window() - SPECIAL_TOKENS));
|
|
3828
4623
|
}
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
4624
|
+
/**
|
|
4625
|
+
* Report an input that needed more than one window — once per process, and as an INFO line.
|
|
4626
|
+
*
|
|
4627
|
+
* Not a warning any more: nothing is lost (every window is pooled into the one vector), so the
|
|
4628
|
+
* line is a tuning hint, not a degradation. It carries the actionable number: `chunk_size` is
|
|
4629
|
+
* measured in CHARACTERS and {@link windowChars} converts the model's token window into exactly
|
|
4630
|
+
* that, which is the thing an operator (or a chunker's default) has to change to keep one window
|
|
4631
|
+
* per input. `semantic.max_input_tokens` is deliberately NOT suggested: `resolveWindow` clamps it
|
|
4632
|
+
* to the model's declared window, so raising it cannot buy a single extra token.
|
|
4633
|
+
*/
|
|
4634
|
+
reportSplit(count, tokens) {
|
|
4635
|
+
recordTruncation("embedding");
|
|
4636
|
+
if (this._lengthWarned) return;
|
|
4637
|
+
this._lengthWarned = true;
|
|
4638
|
+
retrievalLogger().info(`semantic: input of ~${tokens} tokens exceeds the ${this.window()}-token window of ${this.model} — encoded as ${count} windows and pooled, nothing dropped (one pass per input needs knowledge.chunk_size within ${windowChars(this.window())} characters for this model)`);
|
|
4639
|
+
}
|
|
4640
|
+
/** The configured width's vector: model output padded/truncated, with the one-shot dim warning. */
|
|
4641
|
+
toVector(values) {
|
|
4642
|
+
if (values.length !== this.dim && !this._dimWarned) {
|
|
3839
4643
|
this._dimWarned = true;
|
|
3840
|
-
retrievalLogger().warn(`semantic: model ${this.model} outputs dim=${
|
|
4644
|
+
retrievalLogger().warn(`semantic: model ${this.model} outputs dim=${values.length} but config semantic.dim=${this.dim} — vectors are truncated/padded; set semantic.dim to ${values.length} and reindex`);
|
|
3841
4645
|
}
|
|
3842
4646
|
const vec = new Float32Array(this.dim);
|
|
3843
|
-
for (let i = 0; i < this.dim && i <
|
|
4647
|
+
for (let i = 0; i < this.dim && i < values.length; i++) vec[i] = values[i];
|
|
3844
4648
|
return vec;
|
|
3845
4649
|
}
|
|
3846
4650
|
/**
|
|
4651
|
+
* ONE vector for several windows: token-weighted mean of the window vectors, re-normalized when the
|
|
4652
|
+
* representation normalizes.
|
|
4653
|
+
*
|
|
4654
|
+
* Weighted rather than a plain mean because the last window is usually short, and a plain mean
|
|
4655
|
+
* would over-represent a fragment. Mean-of-normalized-vectors then re-normalized is the mean
|
|
4656
|
+
* DIRECTION of the windows — the same coordinate space the model's own mean pooling produces, so
|
|
4657
|
+
* `representationKey` (`pooling`/`normalize`/window) does not move and stored vectors stay
|
|
4658
|
+
* comparable with the ones this writes.
|
|
4659
|
+
*/
|
|
4660
|
+
pool(vectors, weights) {
|
|
4661
|
+
const width = vectors[0]?.length ?? 0;
|
|
4662
|
+
if (width === 0 || vectors.length === 0) return new Float32Array(this.dim);
|
|
4663
|
+
const total = weights.reduce((sum, w) => sum + Math.max(0, w), 0);
|
|
4664
|
+
const out = new Float64Array(width);
|
|
4665
|
+
for (let i = 0; i < vectors.length; i++) {
|
|
4666
|
+
const weight = total > 0 ? Math.max(0, weights[i] ?? 0) / total : 1 / vectors.length;
|
|
4667
|
+
const vec = vectors[i];
|
|
4668
|
+
for (let d = 0; d < width; d++) out[d] += (vec[d] ?? 0) * weight;
|
|
4669
|
+
}
|
|
4670
|
+
if (this.normalize) {
|
|
4671
|
+
let norm = 0;
|
|
4672
|
+
for (let d = 0; d < width; d++) norm += out[d] * out[d];
|
|
4673
|
+
norm = Math.sqrt(norm);
|
|
4674
|
+
if (norm > 0) for (let d = 0; d < width; d++) out[d] /= norm;
|
|
4675
|
+
}
|
|
4676
|
+
return this.toVector(Array.from(out));
|
|
4677
|
+
}
|
|
4678
|
+
/** Encode the windows of one text in ONE model call, falling back to one call per window. */
|
|
4679
|
+
async encodeWindows(pipe, windows, weights) {
|
|
4680
|
+
const opts = {
|
|
4681
|
+
pooling: this.pooling,
|
|
4682
|
+
normalize: this.normalize
|
|
4683
|
+
};
|
|
4684
|
+
try {
|
|
4685
|
+
const out = await pipe(windows, opts);
|
|
4686
|
+
const rows = out.dims?.[0];
|
|
4687
|
+
const flat = out.data;
|
|
4688
|
+
if (rows === windows.length) {
|
|
4689
|
+
const width = Math.floor(flat.length / rows);
|
|
4690
|
+
if (width > 0) {
|
|
4691
|
+
const vecs = [];
|
|
4692
|
+
for (let i = 0; i < rows; i++) vecs.push(Array.from(flat.slice(i * width, (i + 1) * width), Number));
|
|
4693
|
+
return this.pool(vecs, weights);
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
} catch {}
|
|
4697
|
+
const vecs = [];
|
|
4698
|
+
for (const window of windows) {
|
|
4699
|
+
const out = await pipe(window, opts);
|
|
4700
|
+
vecs.push(Array.from(out.data));
|
|
4701
|
+
}
|
|
4702
|
+
return this.pool(vecs, weights);
|
|
4703
|
+
}
|
|
4704
|
+
/** Encode one plan: a single window verbatim, several pooled. */
|
|
4705
|
+
async encodePlan(pipe, plan) {
|
|
4706
|
+
const first = plan.windows[0];
|
|
4707
|
+
if (plan.windows.length <= 1) {
|
|
4708
|
+
const out = await pipe(first ?? "", {
|
|
4709
|
+
pooling: this.pooling,
|
|
4710
|
+
normalize: this.normalize
|
|
4711
|
+
});
|
|
4712
|
+
return this.toVector(Array.from(out.data));
|
|
4713
|
+
}
|
|
4714
|
+
this.reportSplit(plan.windows.length, plan.tokens);
|
|
4715
|
+
return await this.encodeWindows(pipe, plan.windows, plan.windowTokens);
|
|
4716
|
+
}
|
|
4717
|
+
async encode(text) {
|
|
4718
|
+
await this.warmUp();
|
|
4719
|
+
const pipe = this._pipe;
|
|
4720
|
+
if (!this._available || pipe === null) throw new Error("local_bge:语义后端不可用(请安装 @huggingface/transformers 并等模型下载完成)");
|
|
4721
|
+
return await this.encodePlan(pipe, this.windows(text));
|
|
4722
|
+
}
|
|
4723
|
+
/**
|
|
3847
4724
|
* Encode several texts in ONE model call.
|
|
3848
4725
|
*
|
|
3849
4726
|
* This used to be a serial loop over `encode()`, and production code never called it — so
|
|
@@ -3856,11 +4733,22 @@ var LocalBgeBackend = class {
|
|
|
3856
4733
|
if (texts.length === 0) return [];
|
|
3857
4734
|
if (texts.length === 1) return [await this.encode(texts[0])];
|
|
3858
4735
|
await this.warmUp();
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
4736
|
+
const pipe = this._pipe;
|
|
4737
|
+
if (!this._available || pipe === null) throw new Error("local_bge:语义后端不可用(请安装 @huggingface/transformers 并等模型下载完成)");
|
|
4738
|
+
const plans = texts.map((text) => this.windows(text));
|
|
4739
|
+
const singles = [];
|
|
4740
|
+
const longs = [];
|
|
4741
|
+
plans.forEach((plan, index) => {
|
|
4742
|
+
if (plan.windows.length <= 1) singles.push({
|
|
4743
|
+
text: plan.windows[0] ?? texts[index],
|
|
4744
|
+
index
|
|
4745
|
+
});
|
|
4746
|
+
else longs.push({
|
|
4747
|
+
plan,
|
|
4748
|
+
index
|
|
4749
|
+
});
|
|
4750
|
+
});
|
|
4751
|
+
const sorted = singles.sort((a, b) => a.text.length - b.text.length);
|
|
3864
4752
|
const result = new Array(texts.length);
|
|
3865
4753
|
for (let i = 0; i < sorted.length; i += BATCH_WINDOW) {
|
|
3866
4754
|
const group = sorted.slice(i, i + BATCH_WINDOW);
|
|
@@ -3869,15 +4757,15 @@ var LocalBgeBackend = class {
|
|
|
3869
4757
|
result[g.index] = vecs[j];
|
|
3870
4758
|
});
|
|
3871
4759
|
}
|
|
4760
|
+
for (const { plan, index } of longs) result[index] = await this.encodePlan(pipe, plan);
|
|
3872
4761
|
return result;
|
|
3873
4762
|
}
|
|
3874
4763
|
/** One model call for a length-homogeneous group; falls back to per-text calls on failure. */
|
|
3875
4764
|
async encodeGroup(texts) {
|
|
3876
4765
|
const pipe = this._pipe;
|
|
3877
4766
|
if (pipe === null) throw new Error("local_bge:语义后端不可用");
|
|
3878
|
-
const bounded = texts.map((t) => this.bound(t).text);
|
|
3879
4767
|
try {
|
|
3880
|
-
const out = await pipe(
|
|
4768
|
+
const out = await pipe(texts, {
|
|
3881
4769
|
pooling: this.pooling,
|
|
3882
4770
|
normalize: this.normalize
|
|
3883
4771
|
});
|
|
@@ -3904,12 +4792,32 @@ var LocalBgeBackend = class {
|
|
|
3904
4792
|
}
|
|
3905
4793
|
};
|
|
3906
4794
|
async function createFeatureExtraction(model, env) {
|
|
3907
|
-
const mod = await
|
|
4795
|
+
const mod = await loadTransformers(env.moduleDir);
|
|
3908
4796
|
if (!mod.pipeline) throw new Error("transformers.js 的 pipeline 不可用");
|
|
3909
4797
|
applyModelEnv(mod, env);
|
|
3910
4798
|
return await mod.pipeline("feature-extraction", model);
|
|
3911
4799
|
}
|
|
3912
4800
|
/**
|
|
4801
|
+
* Load transformers.js from the shared runtime-dependency root, else from this tree.
|
|
4802
|
+
*
|
|
4803
|
+
* The shared root is the base provider's content-addressed install (`<home>/deps/runtime/<key>`), and
|
|
4804
|
+
* its `package.json` is the anchor `createRequire` needs: resolving `@huggingface/transformers` from
|
|
4805
|
+
* there picks the copy the provider verified, together with its transitive `onnxruntime-node` — no
|
|
4806
|
+
* second copy, and no dependence on this bundle's own node_modules. The import is by file URL because
|
|
4807
|
+
* the resolved entry is an ESM file inside the root, not a bare specifier this module could resolve.
|
|
4808
|
+
*
|
|
4809
|
+
* A root that no longer resolves (GC, damaged tree, a package whose entry moved) is a WARNING and the
|
|
4810
|
+
* pre-existing optional resolution: a broken self-heal must not be worse than no self-heal.
|
|
4811
|
+
*/
|
|
4812
|
+
async function loadTransformers(moduleDir) {
|
|
4813
|
+
if (moduleDir !== void 0 && moduleDir.trim() !== "") try {
|
|
4814
|
+
return await import(pathToFileURL(createRequire(join(moduleDir, "package.json")).resolve("@huggingface/transformers")).href);
|
|
4815
|
+
} catch (error) {
|
|
4816
|
+
retrievalLogger().warn(`semantic: could not load @huggingface/transformers from the shared runtime root ${moduleDir} (${describeError$1(error)}); falling back to this tree's optional dependency`);
|
|
4817
|
+
}
|
|
4818
|
+
return await import("@huggingface/transformers");
|
|
4819
|
+
}
|
|
4820
|
+
/**
|
|
3913
4821
|
* Apply this project's cache/mirror policy to transformers.js.
|
|
3914
4822
|
*
|
|
3915
4823
|
* `mod.env` is PROCESS-GLOBAL and last-writer-wins. Setting the keys from a single place is what
|
|
@@ -4007,6 +4915,60 @@ function dot(a, b) {
|
|
|
4007
4915
|
//#region ../retrieval-core/lib/adapters/hnswlib.js
|
|
4008
4916
|
const require = createRequire(import.meta.url);
|
|
4009
4917
|
/**
|
|
4918
|
+
* The shared module root `hnswlib-node` should be loaded from, when the caller provisioned one.
|
|
4919
|
+
*
|
|
4920
|
+
* `undefined` (every CLI/MCP/direct caller) = this tree's own optional dependency. The DSH plugin
|
|
4921
|
+
* sets it from the native self-heal item's `handle.dir` once that item is ready; a root that no
|
|
4922
|
+
* longer resolves falls back to this tree, which is the pre-existing behaviour and must never be
|
|
4923
|
+
* worse than no self-heal.
|
|
4924
|
+
*/
|
|
4925
|
+
let moduleRoot;
|
|
4926
|
+
/**
|
|
4927
|
+
* The loaded binding, memoized (`undefined` = not attempted, `null` = the last attempt failed).
|
|
4928
|
+
*
|
|
4929
|
+
* Native addons are expensive to `dlopen` and there is exactly one per process; memoizing also makes
|
|
4930
|
+
* every store in the process shares one load. The cache follows the ROOT (see
|
|
4931
|
+
* {@link useHnswlibModuleDir}), because a root that arrives later is the one event that can turn a
|
|
4932
|
+
* failed load into a working index.
|
|
4933
|
+
*/
|
|
4934
|
+
let loaded;
|
|
4935
|
+
/**
|
|
4936
|
+
* Point the loader at a shared module root (the base `runtime-deps` handle's directory).
|
|
4937
|
+
*
|
|
4938
|
+
* The cache follows the ROOT: a change forgets what was loaded, because the next `require` must come
|
|
4939
|
+
* from the new root (that is the whole point of setting one). It costs nothing in practice — Node's
|
|
4940
|
+
* own `require` cache makes re-entering the same file cheap, and a live native index is untouched
|
|
4941
|
+
* (see `HnswlibVectorStore.useModuleDir`, which only rebuilds when it has no native index at all).
|
|
4942
|
+
*/
|
|
4943
|
+
function useHnswlibModuleDir(dir) {
|
|
4944
|
+
const next = dir === "" ? void 0 : dir;
|
|
4945
|
+
if (next === moduleRoot) return;
|
|
4946
|
+
moduleRoot = next;
|
|
4947
|
+
loaded = void 0;
|
|
4948
|
+
}
|
|
4949
|
+
/** Does this module actually carry the constructor this adapter needs? */
|
|
4950
|
+
function usable(mod) {
|
|
4951
|
+
return typeof mod?.HierarchicalNSW === "function";
|
|
4952
|
+
}
|
|
4953
|
+
/** `require('hnswlib-node')` from the shared root, else from this tree; `null` when neither works. */
|
|
4954
|
+
function loadHnswlib() {
|
|
4955
|
+
if (loaded !== void 0) return loaded;
|
|
4956
|
+
let mod = null;
|
|
4957
|
+
if (moduleRoot !== void 0) try {
|
|
4958
|
+
const entry = createRequire(join(moduleRoot, "package.json")).resolve("hnswlib-node");
|
|
4959
|
+
const candidate = require(entry);
|
|
4960
|
+
if (usable(candidate)) mod = candidate;
|
|
4961
|
+
} catch {}
|
|
4962
|
+
if (mod === null) try {
|
|
4963
|
+
const candidate = require("hnswlib-node");
|
|
4964
|
+
mod = usable(candidate) ? candidate : null;
|
|
4965
|
+
} catch {
|
|
4966
|
+
mod = null;
|
|
4967
|
+
}
|
|
4968
|
+
loaded = mod;
|
|
4969
|
+
return mod;
|
|
4970
|
+
}
|
|
4971
|
+
/**
|
|
4010
4972
|
* When to COMPACT tombstones away, as a fraction of the live count.
|
|
4011
4973
|
*
|
|
4012
4974
|
* Deleted points stay in the graph (excluded from results, but still traversed), so they cost
|
|
@@ -4065,15 +5027,41 @@ var HnswlibVectorStore = class {
|
|
|
4065
5027
|
this.dim = dim;
|
|
4066
5028
|
this.efSearch = Math.max(16, Math.floor(efSearch));
|
|
4067
5029
|
this.fallback = new LocalNumpyVectorStore(dim);
|
|
5030
|
+
this.lib = this.openNative(Math.max(1024, dim * 2));
|
|
5031
|
+
}
|
|
5032
|
+
/**
|
|
5033
|
+
* Build a native index at `max` capacity, or `null` when the binding cannot be loaded.
|
|
5034
|
+
*
|
|
5035
|
+
* One place for "make an index": the constructor, `reindex()` and `rebuild()` each used to spell
|
|
5036
|
+
* the `require` out themselves, which is how a module root set after construction could never
|
|
5037
|
+
* reach them. `null` is the caller's signal to serve from {@link fallback} — a missing binding
|
|
5038
|
+
* degrades, it never throws.
|
|
5039
|
+
*/
|
|
5040
|
+
openNative(max) {
|
|
5041
|
+
const mod = loadHnswlib();
|
|
5042
|
+
if (mod === null) return null;
|
|
4068
5043
|
try {
|
|
4069
|
-
const
|
|
4070
|
-
|
|
4071
|
-
lib
|
|
4072
|
-
this.lib = lib;
|
|
5044
|
+
const lib = new mod.HierarchicalNSW("cosine", this.dim);
|
|
5045
|
+
lib.initIndex(Math.max(1024, max), 16, 100);
|
|
5046
|
+
return lib;
|
|
4073
5047
|
} catch {
|
|
4074
|
-
|
|
5048
|
+
return null;
|
|
4075
5049
|
}
|
|
4076
5050
|
}
|
|
5051
|
+
/**
|
|
5052
|
+
* Point this store's native side at a shared module root and, when the binding was unavailable
|
|
5053
|
+
* until now, rebuild the index from the tracked vectors under it.
|
|
5054
|
+
*
|
|
5055
|
+
* This is the case the whole hook exists for: `mem` builds its vector store during mount, while the
|
|
5056
|
+
* native self-heal item is still installing, so the constructor may have fallen back. When the item
|
|
5057
|
+
* settles, the plugin calls this — and a store that already serves natively is left alone (the
|
|
5058
|
+
* binding is the same one).
|
|
5059
|
+
*/
|
|
5060
|
+
useModuleDir(dir) {
|
|
5061
|
+
useHnswlibModuleDir(dir);
|
|
5062
|
+
if (this.lib !== null) return;
|
|
5063
|
+
this.rebuild(this.liveRows());
|
|
5064
|
+
}
|
|
4077
5065
|
add(id, vec) {
|
|
4078
5066
|
if (vec.length !== this.dim) throw new Error(`向量维度不匹配:得到 ${vec.length},需要 ${this.dim}`);
|
|
4079
5067
|
if (this.lib) try {
|
|
@@ -4229,10 +5217,8 @@ var HnswlibVectorStore = class {
|
|
|
4229
5217
|
/** Recreate the serving index (native when possible) from the tracked vectors. */
|
|
4230
5218
|
reindex() {
|
|
4231
5219
|
const stored = [...this.vectors.entries()];
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
const lib = new HierarchicalNSW("cosine", this.dim);
|
|
4235
|
-
lib.initIndex(Math.max(1024, (stored.length || 1) * 2), 16, 100);
|
|
5220
|
+
const lib = this.openNative(Math.max(1024, (stored.length || 1) * 2));
|
|
5221
|
+
if (lib !== null) {
|
|
4236
5222
|
this.lib = lib;
|
|
4237
5223
|
this.efOn = null;
|
|
4238
5224
|
for (const [i, v] of stored) this.lib.addPoint(Array.from(v), i, false);
|
|
@@ -4240,9 +5226,8 @@ var HnswlibVectorStore = class {
|
|
|
4240
5226
|
this.rebuilds += 1;
|
|
4241
5227
|
this.persist();
|
|
4242
5228
|
return;
|
|
4243
|
-
} catch {
|
|
4244
|
-
this.lib = null;
|
|
4245
5229
|
}
|
|
5230
|
+
this.lib = null;
|
|
4246
5231
|
this.fallback = new LocalNumpyVectorStore(this.dim);
|
|
4247
5232
|
for (const [i, v] of stored) this.fallback.add(i, v);
|
|
4248
5233
|
this.graphElements = stored.length;
|
|
@@ -4268,15 +5253,8 @@ var HnswlibVectorStore = class {
|
|
|
4268
5253
|
for (const r of list) this.vectors.set(r.id, r.vec);
|
|
4269
5254
|
return;
|
|
4270
5255
|
}
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
const lib = new HierarchicalNSW("cosine", this.dim);
|
|
4274
|
-
lib.initIndex(Math.max(1024, (list.length || 1) * 2), 16, 100);
|
|
4275
|
-
this.lib = lib;
|
|
4276
|
-
this.efOn = null;
|
|
4277
|
-
} catch {
|
|
4278
|
-
this.lib = null;
|
|
4279
|
-
}
|
|
5256
|
+
this.lib = this.openNative(Math.max(1024, (list.length || 1) * 2));
|
|
5257
|
+
if (this.lib !== null) this.efOn = null;
|
|
4280
5258
|
for (const r of list) this.add(r.id, r.vec);
|
|
4281
5259
|
this.rebuilds += 1;
|
|
4282
5260
|
this.persist();
|
|
@@ -4454,6 +5432,19 @@ var AutoVectorStore = class {
|
|
|
4454
5432
|
this.pendingPersistence = path;
|
|
4455
5433
|
this.delegate.attachPersistence?.(path);
|
|
4456
5434
|
}
|
|
5435
|
+
/**
|
|
5436
|
+
* Point the ANN side at a shared module root (the plugin's native self-heal item) — for THIS store
|
|
5437
|
+
* when it already migrated, and for the one it may migrate to later.
|
|
5438
|
+
*
|
|
5439
|
+
* The delegate call covers the first case; the module-level root (set by
|
|
5440
|
+
* `HnswlibVectorStore.useModuleDir` / `useHnswlibModuleDir`) is what a FUTURE
|
|
5441
|
+
* `new HnswlibVectorStore` in `maybeUpgrade` reads. Below the threshold nothing changes: an
|
|
5442
|
+
* unmigrated store keeps serving from brute force, which is the intended backend at that size.
|
|
5443
|
+
*/
|
|
5444
|
+
useModuleDir(dir) {
|
|
5445
|
+
if (this.delegate.useModuleDir !== void 0) this.delegate.useModuleDir(dir);
|
|
5446
|
+
else useHnswlibModuleDir(dir);
|
|
5447
|
+
}
|
|
4457
5448
|
flush() {
|
|
4458
5449
|
this.delegate.flush?.();
|
|
4459
5450
|
}
|
|
@@ -4821,12 +5812,18 @@ const DDL = [
|
|
|
4821
5812
|
conflict_checked INTEGER NOT NULL DEFAULT 0,
|
|
4822
5813
|
archived_at TIMESTAMP,
|
|
4823
5814
|
archive_reason TEXT,
|
|
5815
|
+
-- DEPRECATED (v2, D18): the per-fact TTL is gone. The column is kept so export / import /
|
|
5816
|
+
-- snapshot formats stay readable and a rollback still finds its data, but nothing reads or
|
|
5817
|
+
-- writes it any more (new rows keep the DEFAULT 0).
|
|
4824
5818
|
ttl_days INTEGER DEFAULT 0,
|
|
4825
5819
|
-- P-07/P-13: when the fact became true and when it stopped being true. NULL = unknown, and no
|
|
4826
|
-
-- retrieval leg reads either column (the envelope only carries
|
|
4827
|
-
--
|
|
4828
|
-
--
|
|
4829
|
-
--
|
|
5820
|
+
-- retrieval leg reads either column (the envelope only carries valid_from now, and only when
|
|
5821
|
+
-- non-null). valid_from is the caller's event time (event_date, the event filter axis).
|
|
5822
|
+
-- DEPRECATED (v2, D19): valid_to is no longer written (valid_until is gone, and so is the
|
|
5823
|
+
-- archival audit stamp). UNLIKE ttl_days it does NOT round-trip: export writes valid_to: null
|
|
5824
|
+
-- and restore ignores the field, deliberately, so an old "known end" cannot be resurrected as a
|
|
5825
|
+
-- validity window. The column survives only so old exports/files stay readable and the DDL does
|
|
5826
|
+
-- not churn; do not read a rollback guarantee into it.
|
|
4830
5827
|
valid_from TIMESTAMP,
|
|
4831
5828
|
valid_to TIMESTAMP,
|
|
4832
5829
|
-- The INSTANTS those two bounds name, in epoch milliseconds (migration 12).
|
|
@@ -4853,10 +5850,8 @@ const DDL = [
|
|
|
4853
5850
|
`CREATE INDEX IF NOT EXISTS idx_facts_supersedes ON facts(supersedes_id)`,
|
|
4854
5851
|
`CREATE INDEX IF NOT EXISTS idx_facts_trust ON facts(status, pinned, settle_clock)`,
|
|
4855
5852
|
`CREATE INDEX IF NOT EXISTS idx_facts_updated ON facts(status, updated_at)`,
|
|
4856
|
-
`CREATE INDEX IF NOT EXISTS idx_facts_ttl ON facts(status, ttl_days)`,
|
|
4857
5853
|
`CREATE INDEX IF NOT EXISTS idx_facts_purge ON facts(status, pinned, archived_clock)`,
|
|
4858
5854
|
`CREATE INDEX IF NOT EXISTS idx_facts_forget ON facts(status, pinned, trust_score)`,
|
|
4859
|
-
`CREATE INDEX IF NOT EXISTS idx_facts_idle_cutoff ON facts(status, pinned, julianday(COALESCE(last_retrieved_at, created_at)))`,
|
|
4860
5855
|
`CREATE INDEX IF NOT EXISTS idx_facts_bonus_window ON facts(bonus_window_at)`,
|
|
4861
5856
|
`CREATE TABLE IF NOT EXISTS fact_sources (
|
|
4862
5857
|
fact_id INTEGER NOT NULL REFERENCES facts(fact_id) ON DELETE CASCADE,
|
|
@@ -7197,7 +8192,7 @@ function timeParams(range) {
|
|
|
7197
8192
|
return timeSql(range)?.params ?? [];
|
|
7198
8193
|
}
|
|
7199
8194
|
/**
|
|
7200
|
-
* One stored `valid_from
|
|
8195
|
+
* One stored event-time (`valid_from`) string → epoch ms, using the SAME grammar as the filters.
|
|
7201
8196
|
*
|
|
7202
8197
|
* This is what the write path stamps into `valid_from_ms` and what migration 12's backfill applies to
|
|
7203
8198
|
* existing rows, so "the event time a caller wrote" and "the event time a window compares" can never
|
|
@@ -8091,6 +9086,14 @@ const MEMORY_SCHEMA = {
|
|
|
8091
9086
|
db.exec(TITLE_ALIASES_TABLE_DDL);
|
|
8092
9087
|
for (const sql of TITLE_ALIASES_INDEXES) db.exec(sql);
|
|
8093
9088
|
}
|
|
9089
|
+
},
|
|
9090
|
+
{
|
|
9091
|
+
version: 18,
|
|
9092
|
+
name: "retire-ttl-and-valid-to",
|
|
9093
|
+
up: (db) => {
|
|
9094
|
+
db.exec("DROP INDEX IF EXISTS idx_facts_idle_cutoff");
|
|
9095
|
+
db.exec("DROP INDEX IF EXISTS idx_facts_ttl");
|
|
9096
|
+
}
|
|
8094
9097
|
}
|
|
8095
9098
|
]
|
|
8096
9099
|
};
|
|
@@ -8343,6 +9346,102 @@ function entityBags(db, ids, q) {
|
|
|
8343
9346
|
return out;
|
|
8344
9347
|
}
|
|
8345
9348
|
//#endregion
|
|
9349
|
+
//#region ../core/lib/lifecycle/trust.js
|
|
9350
|
+
const DAY_MS$1 = 864e5;
|
|
9351
|
+
/** The reinforcement quota window is a CALENDAR day ("每天" is a calendar notion, §2.3). */
|
|
9352
|
+
const QUOTA_WINDOW_MS = DAY_MS$1;
|
|
9353
|
+
function clamp01(v) {
|
|
9354
|
+
if (!Number.isFinite(v)) return 0;
|
|
9355
|
+
return Math.min(1, Math.max(0, v));
|
|
9356
|
+
}
|
|
9357
|
+
/** The curve of one `trust` section — the single mapping from config keys to the math. */
|
|
9358
|
+
function trustCurveOf(cfg) {
|
|
9359
|
+
return curveOf(cfg);
|
|
9360
|
+
}
|
|
9361
|
+
function isPinned(row) {
|
|
9362
|
+
return Number(row.pinned) === 1;
|
|
9363
|
+
}
|
|
9364
|
+
/** eff(f) — the trust a caller should act on right now (spec §2.2). */
|
|
9365
|
+
function effectiveTrust(row, clock, cfg, enabled = cfg.enabled) {
|
|
9366
|
+
if (!enabled) return clamp01(row.trust_score);
|
|
9367
|
+
if (isPinned(row)) return 1;
|
|
9368
|
+
return clamp01(trustAfterDays(row.trust_score, clock - row.settle_clock, trustCurveOf(cfg)));
|
|
9369
|
+
}
|
|
9370
|
+
/**
|
|
9371
|
+
* What a caller should SEE for a row — the R11 display rule (spec §6):
|
|
9372
|
+
* `active` → `eff()`; **non-active → the stored value verbatim** (the decay
|
|
9373
|
+
* formula keeps running mathematically for archived rows, so the projection must
|
|
9374
|
+
* not use it), and `enabled=false` → stored value (D12).
|
|
9375
|
+
*/
|
|
9376
|
+
function displayTrust(row, clock, cfg, enabled = cfg.enabled) {
|
|
9377
|
+
if (row.status !== "active") return clamp01(row.trust_score);
|
|
9378
|
+
return effectiveTrust(row, clock, cfg, enabled);
|
|
9379
|
+
}
|
|
9380
|
+
/**
|
|
9381
|
+
* Remaining active days: the inverse of the curve at the current eff (`daysToZero`).
|
|
9382
|
+
* `null` for pinned rows, non-active rows and when trust is disabled — the contract's
|
|
9383
|
+
* `remaining_days` (spec §6 / R11).
|
|
9384
|
+
*/
|
|
9385
|
+
function remainingDays(row, clock, cfg, enabled = cfg.enabled) {
|
|
9386
|
+
if (row.status !== "active" || isPinned(row) || !enabled) return null;
|
|
9387
|
+
return daysToZero(effectiveTrust(row, clock, cfg, enabled), trustCurveOf(cfg));
|
|
9388
|
+
}
|
|
9389
|
+
function rollWindow(row, nowMs) {
|
|
9390
|
+
const at = row.bonus_window_at;
|
|
9391
|
+
const started = parseUtcTs(at);
|
|
9392
|
+
if (at === null || started === null || nowMs - started >= QUOTA_WINDOW_MS) return {
|
|
9393
|
+
count: 0,
|
|
9394
|
+
at: formatUtcTs(nowMs),
|
|
9395
|
+
reset: true
|
|
9396
|
+
};
|
|
9397
|
+
return {
|
|
9398
|
+
count: row.bonus_count ?? 0,
|
|
9399
|
+
at,
|
|
9400
|
+
reset: false
|
|
9401
|
+
};
|
|
9402
|
+
}
|
|
9403
|
+
/**
|
|
9404
|
+
* Apply one recall to a fact: settle, then grant a bonus inside the daily quota.
|
|
9405
|
+
* Returns `next === settled` when the quota is exhausted, the ceiling is reached,
|
|
9406
|
+
* the fact is pinned, or the gain would be zero (R2/R10/D11).
|
|
9407
|
+
*
|
|
9408
|
+
* The gain is SEGMENTED at the frozen anchor `BASE_TRUST` (D20):
|
|
9409
|
+
* - strictly below the base: `min(BASE_TRUST, T + recall_gain_below_base)`, so the shipped
|
|
9410
|
+
* `recall_gain_below_base: 0.1` takes 0.05 → 0.5 in five recalls and never past the base;
|
|
9411
|
+
* - AT OR ABOVE the base: `recall_gain_above_base × marginal_decay^count`, capped at the ceiling
|
|
9412
|
+
* INVERTED from `max_retention_days` (there is no `recall_ceiling` key any more, D16). The
|
|
9413
|
+
* boundary is strictly `<` on purpose: a fact sitting exactly on the base (a fresh one, on the
|
|
9414
|
+
* first recall after it has finished settling) takes the above-base branch and starts climbing.
|
|
9415
|
+
*/
|
|
9416
|
+
function grantRecallBonus(row, clock, nowMs, cfg, enabled = cfg.enabled) {
|
|
9417
|
+
const settled = effectiveTrust(row, clock, cfg, enabled);
|
|
9418
|
+
const window = rollWindow(row, nowMs);
|
|
9419
|
+
if (row.status !== "active" || isPinned(row) || !enabled || window.count >= cfg.recall_daily_cap) return {
|
|
9420
|
+
settled,
|
|
9421
|
+
next: settled,
|
|
9422
|
+
granted: false,
|
|
9423
|
+
bonusCount: window.count,
|
|
9424
|
+
windowAt: window.at
|
|
9425
|
+
};
|
|
9426
|
+
const gain = cfg.recall_gain_above_base * Math.pow(cfg.recall_marginal_decay, window.count);
|
|
9427
|
+
const ceiling = ceilingForDays(cfg.max_retention_days, trustCurveOf(cfg));
|
|
9428
|
+
const next = settled < .5 ? Math.max(settled, Math.min(BASE_TRUST, settled + cfg.recall_gain_below_base)) : Math.max(settled, Math.min(ceiling, settled + gain));
|
|
9429
|
+
if (next <= settled + 1e-9) return {
|
|
9430
|
+
settled,
|
|
9431
|
+
next: settled,
|
|
9432
|
+
granted: false,
|
|
9433
|
+
bonusCount: window.count,
|
|
9434
|
+
windowAt: window.at
|
|
9435
|
+
};
|
|
9436
|
+
return {
|
|
9437
|
+
settled,
|
|
9438
|
+
next,
|
|
9439
|
+
granted: true,
|
|
9440
|
+
bonusCount: window.count + 1,
|
|
9441
|
+
windowAt: window.at
|
|
9442
|
+
};
|
|
9443
|
+
}
|
|
9444
|
+
//#endregion
|
|
8346
9445
|
//#region ../core/lib/db/dao/facts.js
|
|
8347
9446
|
/**
|
|
8348
9447
|
* The column a pure time window orders by — the axis the caller filtered on.
|
|
@@ -8380,7 +9479,7 @@ const FACT_COLUMNS_NO_BLOB = `fact_id, content, kind, title, tags, trust_score,
|
|
|
8380
9479
|
pinned, pinned_at, bonus_count, bonus_window_at, last_reinforced_at, archived_clock,
|
|
8381
9480
|
retrieval_count, helpful_count, last_retrieved_at, embedding_model, vector_store, status,
|
|
8382
9481
|
supersedes_id, entities_version, conflict_checked, archived_at, archive_reason, ttl_days,
|
|
8383
|
-
valid_from,
|
|
9482
|
+
valid_from, valid_from_ms, assert_count, created_at, updated_at`;
|
|
8384
9483
|
/**
|
|
8385
9484
|
* The FTS leg's exact SQL, exported so the plan guard (`test/fts_plan.spec.ts`) and the bench
|
|
8386
9485
|
* control group EXPLAIN the production text itself instead of a copy that can silently drift.
|
|
@@ -8698,12 +9797,11 @@ var FactsDao = class FactsDao {
|
|
|
8698
9797
|
const out = /* @__PURE__ */ new Map();
|
|
8699
9798
|
if (!ids.length) return out;
|
|
8700
9799
|
const { placeholders, values } = inList(ids);
|
|
8701
|
-
const rows = this.db.prepare(`SELECT fact_id, created_at, updated_at, valid_from,
|
|
9800
|
+
const rows = this.db.prepare(`SELECT fact_id, created_at, updated_at, valid_from, title FROM facts WHERE fact_id IN (${placeholders})`).all(...values);
|
|
8702
9801
|
for (const row of rows) out.set(row.fact_id, {
|
|
8703
9802
|
created_at: row.created_at === null ? "" : String(row.created_at),
|
|
8704
9803
|
updated_at: row.updated_at === null ? null : String(row.updated_at),
|
|
8705
9804
|
valid_from: row.valid_from === null || row.valid_from === void 0 ? null : String(row.valid_from),
|
|
8706
|
-
valid_to: row.valid_to === null || row.valid_to === void 0 ? null : String(row.valid_to),
|
|
8707
9805
|
title: row.title === null || row.title === void 0 || row.title === "" ? null : String(row.title)
|
|
8708
9806
|
});
|
|
8709
9807
|
return out;
|
|
@@ -8743,31 +9841,33 @@ var FactsDao = class FactsDao {
|
|
|
8743
9841
|
* `createdAt` is named EXPLICITLY rather than left to the column DEFAULT, because a
|
|
8744
9842
|
* superseding revision must INHERIT the archived row's `created_at` (the memory's first
|
|
8745
9843
|
* assertion date, distinct from when its text was rewritten). `updated_at` and
|
|
8746
|
-
* `last_retrieved_at` are stamped `CURRENT_TIMESTAMP` in the same statement
|
|
8747
|
-
*
|
|
8748
|
-
*
|
|
9844
|
+
* `last_retrieved_at` are stamped `CURRENT_TIMESTAMP` in the same statement, so a revision
|
|
9845
|
+
* carries a fresh "last used" observation like the row it replaces (that column no longer
|
|
9846
|
+
* feeds any decision — see TRUST_MODEL.md §2.6).
|
|
9847
|
+
*
|
|
9848
|
+
* `ttl_days` is NOT named: TTL is gone (v2), the column keeps its DEFAULT 0 and nothing
|
|
9849
|
+
* reads or writes it any more.
|
|
8749
9850
|
*/
|
|
8750
9851
|
insertRevision(v) {
|
|
8751
9852
|
return this.db.prepare(`INSERT OR IGNORE INTO facts
|
|
8752
|
-
(content, kind, title,
|
|
9853
|
+
(content, kind, title, supersedes_id, hrr_vector,
|
|
8753
9854
|
trust_score, settle_clock, pinned, pinned_at, bonus_count, bonus_window_at,
|
|
8754
|
-
entities_version, conflict_checked, valid_from,
|
|
9855
|
+
entities_version, conflict_checked, valid_from, valid_from_ms,
|
|
8755
9856
|
created_at, updated_at, last_retrieved_at)
|
|
8756
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
8757
|
-
?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`).run(v.content, v.kind, v.title ?? null, v.
|
|
9857
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, 0, ?, ?,
|
|
9858
|
+
?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`).run(v.content, v.kind, v.title ?? null, v.supersedesId, v.hrr, v.trust, v.clock, v.pinned, v.pinnedAt, v.windowAt, 3, v.validFrom ?? null, v.validFromMs ?? null, v.createdAt);
|
|
8758
9859
|
}
|
|
8759
9860
|
/**
|
|
8760
|
-
* P-10: land an explicitly given
|
|
8761
|
-
* caller named `event_date`
|
|
8762
|
-
*
|
|
9861
|
+
* P-10: land an explicitly given event time on a row that already exists (a duplicate `add`
|
|
9862
|
+
* whose caller named `event_date`). An omitted argument keeps what the row has, exactly like
|
|
9863
|
+
* `kind` (see `add`'s doc).
|
|
8763
9864
|
*/
|
|
8764
|
-
applyValidity(factId, validFrom
|
|
8765
|
-
if (validFrom === void 0
|
|
9865
|
+
applyValidity(factId, validFrom) {
|
|
9866
|
+
if (validFrom === void 0) return;
|
|
8766
9867
|
this.db.prepare(`UPDATE facts
|
|
8767
9868
|
SET valid_from = COALESCE(?, valid_from), valid_from_ms = COALESCE(?, valid_from_ms),
|
|
8768
|
-
valid_to = COALESCE(?, valid_to), valid_to_ms = COALESCE(?, valid_to_ms),
|
|
8769
9869
|
updated_at = CURRENT_TIMESTAMP
|
|
8770
|
-
WHERE fact_id = ?`).run(validFrom
|
|
9870
|
+
WHERE fact_id = ?`).run(validFrom.text, validFrom.ms, factId);
|
|
8771
9871
|
}
|
|
8772
9872
|
/** P-10: the row's assertion counter (`1` when the column is somehow NULL on a legacy row). */
|
|
8773
9873
|
assertCount(factId) {
|
|
@@ -8787,17 +9887,6 @@ var FactsDao = class FactsDao {
|
|
|
8787
9887
|
return Number(row?.n ?? 1);
|
|
8788
9888
|
}
|
|
8789
9889
|
/**
|
|
8790
|
-
* P-07 audit: stamp `valid_to` on a row that is leaving the active corpus (supersede / a
|
|
8791
|
-
* contradiction verdict). `COALESCE` keeps an end the caller already declared (`valid_until`):
|
|
8792
|
-
* overwriting it with the archival instant would replace a real answer with an administrative one.
|
|
8793
|
-
*/
|
|
8794
|
-
markValidTo(factId, at, atMs = null) {
|
|
8795
|
-
this.db.prepare(`UPDATE facts
|
|
8796
|
-
SET valid_to = COALESCE(valid_to, ?),
|
|
8797
|
-
valid_to_ms = CASE WHEN valid_to IS NULL THEN ? ELSE valid_to_ms END
|
|
8798
|
-
WHERE fact_id = ?`).run(at, atMs, factId);
|
|
8799
|
-
}
|
|
8800
|
-
/**
|
|
8801
9890
|
* P-07: "who superseded this fact", derived from the existing `supersedes_id` link — no
|
|
8802
9891
|
* redundant column. `MIN` is deterministic when several revisions named the same predecessor
|
|
8803
9892
|
* (which a merge can produce); the caller renders it as a single direct successor.
|
|
@@ -8958,17 +10047,22 @@ var FactsDao = class FactsDao {
|
|
|
8958
10047
|
}
|
|
8959
10048
|
return changes;
|
|
8960
10049
|
}
|
|
8961
|
-
/**
|
|
8962
|
-
|
|
10050
|
+
/**
|
|
10051
|
+
* Revive an archived row (R19: clear the archive bookkeeping / refresh `last_retrieved_at`).
|
|
10052
|
+
*
|
|
10053
|
+
* `ttl_days` is deliberately NOT named any more: an omitted column keeps whatever the row has
|
|
10054
|
+
* (the old writing value is preserved as data), and there is no TTL left to inherit.
|
|
10055
|
+
*/
|
|
10056
|
+
revive(factId, kind, trust, clock) {
|
|
8963
10057
|
return this.db.prepare(`UPDATE facts
|
|
8964
10058
|
SET status = 'active', archived_at = NULL, archive_reason = NULL, archived_clock = NULL,
|
|
8965
|
-
kind = ?,
|
|
10059
|
+
kind = ?, trust_score = ?, settle_clock = ?,
|
|
8966
10060
|
last_retrieved_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
|
8967
|
-
WHERE fact_id = ? AND status = 'archived'`).run(kind,
|
|
10061
|
+
WHERE fact_id = ? AND status = 'archived'`).run(kind, trust, clock, factId).changes;
|
|
8968
10062
|
}
|
|
8969
|
-
/** Land explicit kind
|
|
8970
|
-
applyValues(factId, kind
|
|
8971
|
-
this.db.prepare("UPDATE facts SET kind = ?,
|
|
10063
|
+
/** Land an explicit kind on a row that already exists (pure duplicate). */
|
|
10064
|
+
applyValues(factId, kind) {
|
|
10065
|
+
this.db.prepare("UPDATE facts SET kind = ?, updated_at = CURRENT_TIMESTAMP WHERE fact_id = ?").run(kind, factId);
|
|
8972
10066
|
}
|
|
8973
10067
|
/**
|
|
8974
10068
|
* Land a title on a row that already exists — the ONE write behind every "backfill" path
|
|
@@ -9045,13 +10139,12 @@ var FactsDao = class FactsDao {
|
|
|
9045
10139
|
this.db.prepare("UPDATE facts SET supersedes_id = ? WHERE fact_id = ? AND supersedes_id IS NULL").run(supersedesId, factId);
|
|
9046
10140
|
}
|
|
9047
10141
|
/**
|
|
9048
|
-
* Restore an archived fact, settling its trust and refreshing
|
|
10142
|
+
* Restore an archived fact, settling its trust and refreshing `last_retrieved_at` (spec §2.6).
|
|
9049
10143
|
*
|
|
9050
10144
|
* `AND status = 'archived'` is load-bearing, like the guard on every sibling above: this UPDATE
|
|
9051
10145
|
* clears the archive bookkeeping and refreshes `last_retrieved_at`, so ungated it would take an
|
|
9052
|
-
* ACTIVE row and
|
|
9053
|
-
*
|
|
9054
|
-
* returned `1` either way, so the caller could not tell that a row it never asked about had moved.
|
|
10146
|
+
* ACTIVE row and reset `trust_score` to the settled value it computed. It returned `1` either
|
|
10147
|
+
* way, so the caller could not tell that a row it never asked about had moved.
|
|
9055
10148
|
*/
|
|
9056
10149
|
restore(factId, trust, clock) {
|
|
9057
10150
|
return this.db.prepare(`UPDATE facts
|
|
@@ -9059,32 +10152,6 @@ var FactsDao = class FactsDao {
|
|
|
9059
10152
|
trust_score = ?, settle_clock = ?, last_retrieved_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
|
9060
10153
|
WHERE fact_id = ? AND status = 'archived'`).run(trust, clock, factId).changes;
|
|
9061
10154
|
}
|
|
9062
|
-
bumpHelpful(factId, delta) {
|
|
9063
|
-
this.db.prepare("UPDATE facts SET helpful_count = MAX(0, helpful_count + ?) WHERE fact_id = ?").run(delta, factId);
|
|
9064
|
-
}
|
|
9065
|
-
/** Feedback outcome: pin at the permanent threshold (trust snaps to 1). */
|
|
9066
|
-
pinOutcome(factId, clock, bonusCount, windowAt) {
|
|
9067
|
-
this.db.prepare(`UPDATE facts
|
|
9068
|
-
SET trust_score = 1, pinned = 1, pinned_at = COALESCE(pinned_at, CURRENT_TIMESTAMP),
|
|
9069
|
-
settle_clock = ?, bonus_count = ?, bonus_window_at = ?, archived_clock = NULL,
|
|
9070
|
-
last_reinforced_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
|
9071
|
-
WHERE fact_id = ?`).run(clock, bonusCount, windowAt, factId);
|
|
9072
|
-
}
|
|
9073
|
-
/** Feedback outcome: the forget line was reached — archive in the same statement. */
|
|
9074
|
-
forgetOutcome(factId, trust, clock, bonusCount, windowAt) {
|
|
9075
|
-
this.db.prepare(`UPDATE facts
|
|
9076
|
-
SET trust_score = ?, settle_clock = ?, bonus_count = ?, bonus_window_at = ?,
|
|
9077
|
-
status = 'archived', archived_at = CURRENT_TIMESTAMP, archived_clock = ?,
|
|
9078
|
-
archive_reason = 'forgot', updated_at = CURRENT_TIMESTAMP
|
|
9079
|
-
WHERE fact_id = ?`).run(trust, clock, bonusCount, windowAt, clock, factId);
|
|
9080
|
-
}
|
|
9081
|
-
/** Feedback outcome: reinforced (or decayed) but still active. */
|
|
9082
|
-
reinforceOutcome(factId, trust, clock, bonusCount, windowAt) {
|
|
9083
|
-
this.db.prepare(`UPDATE facts
|
|
9084
|
-
SET trust_score = ?, settle_clock = ?, bonus_count = ?, bonus_window_at = ?,
|
|
9085
|
-
last_reinforced_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
|
9086
|
-
WHERE fact_id = ?`).run(trust, clock, bonusCount, windowAt, factId);
|
|
9087
|
-
}
|
|
9088
10155
|
pin(factId, clock) {
|
|
9089
10156
|
return this.db.prepare(`UPDATE facts
|
|
9090
10157
|
SET pinned = 1, pinned_at = COALESCE(pinned_at, CURRENT_TIMESTAMP), trust_score = 1,
|
|
@@ -9173,17 +10240,25 @@ var FactsDao = class FactsDao {
|
|
|
9173
10240
|
return Number(this.db.prepare("SELECT COUNT(*) AS n FROM facts WHERE status = 'active' AND pinned = 1").get()?.n ?? 0);
|
|
9174
10241
|
}
|
|
9175
10242
|
/**
|
|
9176
|
-
* ACTIVE facts whose decayed trust
|
|
9177
|
-
*
|
|
10243
|
+
* ACTIVE, unpinned facts whose decayed trust reaches zero within `withinDays` ACTIVE days.
|
|
10244
|
+
*
|
|
10245
|
+
* EXACT for every shape, not a linear approximation: the predicate is
|
|
10246
|
+
* `trustAfterDays(eff(now), withinDays, curve) <= 0`, which is the same question the tick asks
|
|
10247
|
+
* when it archives a row. The caller passes the THRESHOLD (`ceilingForDays(withinDays, curve)`)
|
|
10248
|
+
* because "days until zero" is no longer `trust ÷ slope` under `decay_shape: 'accelerating'`;
|
|
10249
|
+
* a slope-based SQL predicate under-reported every fact in `(slope × 7, ceilingForDays(7)]`
|
|
10250
|
+
* (0.0277 … 0.0515 at the shipped defaults) once accelerating became the default.
|
|
10251
|
+
*
|
|
10252
|
+
* COST: one row per active unpinned fact, evaluated in JS — the predicate needs `exp`, which the
|
|
10253
|
+
* SQL layer cannot express portably. `trust_diagnose` is an operator-only diagnostic and the
|
|
10254
|
+
* active set is what `mem_admin list` already walks; if it ever shows up in a profile, prefilter
|
|
10255
|
+
* with an index-friendly bound rather than going back to a slope approximation.
|
|
9178
10256
|
*/
|
|
9179
10257
|
countForgettingWithin(p) {
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
clock: p.clock,
|
|
9185
|
-
soon: p.withinDays
|
|
9186
|
-
})?.n ?? 0);
|
|
10258
|
+
const rows = this.db.prepare(`SELECT trust_score, settle_clock FROM facts WHERE status = 'active' AND pinned = 0`).all();
|
|
10259
|
+
let n = 0;
|
|
10260
|
+
for (const row of rows) if (trustAfterDays(Number(row.trust_score), p.clock - Number(row.settle_clock), p.curve) <= p.threshold) n += 1;
|
|
10261
|
+
return n;
|
|
9187
10262
|
}
|
|
9188
10263
|
/** Facts with a still-live (calendar 24h) reinforcement window — the per-fact quota. */
|
|
9189
10264
|
/**
|
|
@@ -9203,31 +10278,15 @@ var FactsDao = class FactsDao {
|
|
|
9203
10278
|
granted: Number(row?.granted ?? 0)
|
|
9204
10279
|
};
|
|
9205
10280
|
}
|
|
9206
|
-
/** ACTIVE, unpinned facts unused for `days` CALENDAR days (the idle-archival fallback). */
|
|
9207
|
-
countIdleCandidates(days) {
|
|
9208
|
-
return Number(this.db.prepare(`SELECT COUNT(*) AS n FROM facts
|
|
9209
|
-
WHERE status = 'active' AND pinned = 0
|
|
9210
|
-
AND julianday(COALESCE(last_retrieved_at, created_at)) < julianday('now', :modifier)`).get({ modifier: `-${days} days` })?.n ?? 0);
|
|
9211
|
-
}
|
|
9212
10281
|
/**
|
|
9213
|
-
* Rows still due for
|
|
10282
|
+
* Rows still due for ② after a budgeted tick — what the NEXT pass will archive.
|
|
9214
10283
|
*
|
|
9215
|
-
*
|
|
9216
|
-
*
|
|
9217
|
-
*
|
|
10284
|
+
* The same predicate the archival statement runs (`FORGET_PREDICATE`), so "backlog" and "what
|
|
10285
|
+
* `archiveForgotten` will do next" cannot disagree. Only called when ② actually filled its
|
|
10286
|
+
* budget, so the common tick pays nothing for it.
|
|
9218
10287
|
*/
|
|
9219
10288
|
countArchiveBacklog(p) {
|
|
9220
|
-
|
|
9221
|
-
forgetThreshold: p.forgetThreshold,
|
|
9222
|
-
idleModifier: `-${p.idleCalendarDays} days`
|
|
9223
|
-
};
|
|
9224
|
-
return Number(this.db.prepare(`SELECT COUNT(*) AS n FROM (
|
|
9225
|
-
SELECT fact_id FROM facts WHERE status = 'active' AND ttl_days > 0
|
|
9226
|
-
AND julianday('now') - julianday(created_at) > ttl_days
|
|
9227
|
-
UNION SELECT fact_id FROM facts WHERE status = 'active' AND pinned = 0 AND trust_score <= :forgetThreshold
|
|
9228
|
-
UNION SELECT fact_id FROM facts WHERE status = 'active' AND pinned = 0
|
|
9229
|
-
AND julianday(COALESCE(last_retrieved_at, created_at)) < julianday('now', :idleModifier)
|
|
9230
|
-
)`).get(params)?.n ?? 0);
|
|
10289
|
+
return Number(this.db.prepare(`SELECT COUNT(*) AS n FROM facts WHERE ${FactsDao.FORGET_PREDICATE}`).get({ forgetThreshold: p.forgetThreshold })?.n ?? 0);
|
|
9231
10290
|
}
|
|
9232
10291
|
/**
|
|
9233
10292
|
* Drop unusable (stale-dim) vectors so a re-encode can replace them — and re-queue the same
|
|
@@ -9566,48 +10625,60 @@ var FactsDao = class FactsDao {
|
|
|
9566
10625
|
* row, and `settle_clock < c` is a seek + range scan of exactly the pending rows.
|
|
9567
10626
|
*/
|
|
9568
10627
|
static SETTLE_PREDICATE = "status = 'active' AND pinned = 0 AND settle_clock < CAST(:clock AS INTEGER)";
|
|
9569
|
-
/**
|
|
10628
|
+
/**
|
|
10629
|
+
* The ONE forgetting predicate, shared by the archival statement and its backlog count.
|
|
10630
|
+
*
|
|
10631
|
+
* Kept as a constant for the same reason as {@link SETTLE_PREDICATE}: the count answers "how many
|
|
10632
|
+
* will the next pass archive", so it must be the same WHERE body the archival itself runs —
|
|
10633
|
+
* otherwise `archived_deferred` and the actual sweep would drift apart.
|
|
10634
|
+
*/
|
|
10635
|
+
static FORGET_PREDICATE = "status = 'active' AND pinned = 0 AND trust_score <= :forgetThreshold";
|
|
10636
|
+
/**
|
|
10637
|
+
* ① materialize decay, bounded to `budget` rows.
|
|
10638
|
+
*
|
|
10639
|
+
* PER ROW, through `trustAfterDays` — not one UPDATE with an inline `trust_score − step·Δ`. The
|
|
10640
|
+
* formula lives in `contract/src/trust_curve.ts` and nowhere else; the `linear` branch of that
|
|
10641
|
+
* function is bit-identical to the statement this replaced (`T − days·slope`, the same two
|
|
10642
|
+
* operations in the same order), so P3's equivalent tier materializes exactly the same numbers.
|
|
10643
|
+
*
|
|
10644
|
+
* The SELECT is the old UPDATE's WHERE body with the old subquery's ORDER/LIMIT, so the row SET is
|
|
10645
|
+
* the same one the statement would have touched (and `settled` still counts rows matched, not rows
|
|
10646
|
+
* whose value happened to move).
|
|
10647
|
+
*
|
|
10648
|
+
* COST: one `UPDATE` PER ROW inside the tick's single `IMMEDIATE` transaction (the accelerating
|
|
10649
|
+
* formula needs `exp`, which SQL cannot express here, and a second SQL implementation would
|
|
10650
|
+
* drift). That is why every caller is budgeted by `trust.tick_max_facts` (5000 by default) and
|
|
10651
|
+
* `admin maintenance` is the only unbudgeted pass — measured ~22 ms per 5000 rows, inside the
|
|
10652
|
+
* tick's budget; it is a deliberate trade, not a regression.
|
|
10653
|
+
*/
|
|
9570
10654
|
settleBudgeted(p) {
|
|
9571
|
-
|
|
9572
|
-
SET trust_score = MAX(0, MIN(1, trust_score - :step * (:clock - settle_clock))),
|
|
9573
|
-
settle_clock = :clock
|
|
10655
|
+
const rows = this.db.prepare(`SELECT fact_id, trust_score, settle_clock FROM facts
|
|
9574
10656
|
WHERE ${FactsDao.SETTLE_PREDICATE}
|
|
9575
|
-
|
|
10657
|
+
ORDER BY settle_clock ASC LIMIT :budget`).all({
|
|
10658
|
+
clock: p.clock,
|
|
10659
|
+
budget: p.budget
|
|
10660
|
+
});
|
|
10661
|
+
return this.settleRows(rows, p.clock, p.curve);
|
|
9576
10662
|
}
|
|
9577
10663
|
/** ① materialize decay for every pending row (no budget). */
|
|
9578
10664
|
settleAll(p) {
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
10665
|
+
const rows = this.db.prepare(`SELECT fact_id, trust_score, settle_clock FROM facts
|
|
10666
|
+
WHERE ${FactsDao.SETTLE_PREDICATE}
|
|
10667
|
+
ORDER BY settle_clock ASC`).all({ clock: p.clock });
|
|
10668
|
+
return this.settleRows(rows, p.clock, p.curve);
|
|
10669
|
+
}
|
|
10670
|
+
/** The shared per-row write of ①: settle the curve at `clock`, then stamp `settle_clock`. */
|
|
10671
|
+
settleRows(rows, clock, curve) {
|
|
10672
|
+
const stmt = this.db.prepare("UPDATE facts SET trust_score = ?, settle_clock = ? WHERE fact_id = ?");
|
|
10673
|
+
for (const row of rows) stmt.run(clamp01(trustAfterDays(Number(row.trust_score), clock - Number(row.settle_clock), curve)), clock, row.fact_id);
|
|
10674
|
+
return rows.length;
|
|
9583
10675
|
}
|
|
9584
10676
|
/** Rows still awaiting ① (the budget ran out). */
|
|
9585
10677
|
countPendingSettle(p) {
|
|
9586
10678
|
return Number(this.db.prepare(`SELECT COUNT(*) AS n FROM facts WHERE ${FactsDao.SETTLE_PREDICATE}`).get(p)?.n ?? 0);
|
|
9587
10679
|
}
|
|
9588
10680
|
/**
|
|
9589
|
-
* ②
|
|
9590
|
-
*
|
|
9591
|
-
* `budget` (0 = none) caps how many rows ONE tick may archive: the tick is a single IMMEDIATE
|
|
9592
|
-
* transaction shared by several processes, so an unbudgeted pass over a large backlog held the
|
|
9593
|
-
* write lock for as long as it took (measured 14400 rows in one tick). The remainder stays due
|
|
9594
|
-
* and is reported as a deferred count, then archived by the next tick.
|
|
9595
|
-
*/
|
|
9596
|
-
archiveExpiredByTtl(p) {
|
|
9597
|
-
return this.db.prepare(`UPDATE facts
|
|
9598
|
-
SET status = 'archived', archived_at = CURRENT_TIMESTAMP, archived_clock = :clock, archive_reason = 'ttl'
|
|
9599
|
-
WHERE status = 'active' AND ttl_days > 0
|
|
9600
|
-
AND julianday('now') - julianday(created_at) > ttl_days
|
|
9601
|
-
AND (:budget = 0 OR fact_id IN (
|
|
9602
|
-
SELECT fact_id FROM facts WHERE status = 'active' AND ttl_days > 0
|
|
9603
|
-
AND julianday('now') - julianday(created_at) > ttl_days LIMIT :budget))
|
|
9604
|
-
RETURNING fact_id`).all({
|
|
9605
|
-
clock: p.clock,
|
|
9606
|
-
budget: p.budget ?? 0
|
|
9607
|
-
}).map((row) => row.fact_id);
|
|
9608
|
-
}
|
|
9609
|
-
/**
|
|
9610
|
-
* ③ trust-driven forgetting (① already clamped trust to the threshold).
|
|
10681
|
+
* ② trust-driven forgetting (① already clamped trust to the threshold).
|
|
9611
10682
|
*
|
|
9612
10683
|
* Sargable as written (`status`/`pinned` equality + `trust_score <= ?`), and served by
|
|
9613
10684
|
* `idx_facts_forget(status, pinned, trust_score)`: before that index this statement read every
|
|
@@ -9616,9 +10687,9 @@ var FactsDao = class FactsDao {
|
|
|
9616
10687
|
archiveForgotten(p) {
|
|
9617
10688
|
return this.db.prepare(`UPDATE facts
|
|
9618
10689
|
SET status = 'archived', archived_at = CURRENT_TIMESTAMP, archived_clock = :clock, archive_reason = 'forgot'
|
|
9619
|
-
WHERE
|
|
10690
|
+
WHERE ${FactsDao.FORGET_PREDICATE}
|
|
9620
10691
|
AND (:budget = 0 OR fact_id IN (
|
|
9621
|
-
SELECT fact_id FROM facts WHERE
|
|
10692
|
+
SELECT fact_id FROM facts WHERE ${FactsDao.FORGET_PREDICATE} LIMIT :budget))
|
|
9622
10693
|
RETURNING fact_id`).all({
|
|
9623
10694
|
clock: p.clock,
|
|
9624
10695
|
forgetThreshold: p.forgetThreshold,
|
|
@@ -9626,29 +10697,7 @@ var FactsDao = class FactsDao {
|
|
|
9626
10697
|
}).map((row) => row.fact_id);
|
|
9627
10698
|
}
|
|
9628
10699
|
/**
|
|
9629
|
-
*
|
|
9630
|
-
*
|
|
9631
|
-
* The comparison is written as `<expression> < julianday('now', :modifier)` — the same
|
|
9632
|
-
* expression `idx_facts_idle_cutoff` indexes — so it is a range seek. The old form
|
|
9633
|
-
* (`julianday('now') - <expression> > :days`) called `julianday` on EVERY active row and could
|
|
9634
|
-
* not use any index (measured 33–212 ms at 180k).
|
|
9635
|
-
*/
|
|
9636
|
-
archiveIdle(p) {
|
|
9637
|
-
return this.db.prepare(`UPDATE facts
|
|
9638
|
-
SET status = 'archived', archived_at = CURRENT_TIMESTAMP, archived_clock = :clock, archive_reason = 'idle'
|
|
9639
|
-
WHERE status = 'active' AND pinned = 0
|
|
9640
|
-
AND julianday(COALESCE(last_retrieved_at, created_at)) < julianday('now', :idleModifier)
|
|
9641
|
-
AND (:budget = 0 OR fact_id IN (
|
|
9642
|
-
SELECT fact_id FROM facts WHERE status = 'active' AND pinned = 0
|
|
9643
|
-
AND julianday(COALESCE(last_retrieved_at, created_at)) < julianday('now', :idleModifier) LIMIT :budget))
|
|
9644
|
-
RETURNING fact_id`).all({
|
|
9645
|
-
clock: p.clock,
|
|
9646
|
-
idleModifier: `-${p.idleCalendarDays} days`,
|
|
9647
|
-
budget: p.budget ?? 0
|
|
9648
|
-
}).map((row) => row.fact_id);
|
|
9649
|
-
}
|
|
9650
|
-
/**
|
|
9651
|
-
* ⑤ physical delete of archived rows past the retention window.
|
|
10700
|
+
* ③ physical delete of archived rows past the retention window.
|
|
9652
10701
|
*
|
|
9653
10702
|
* TWO statements in this order, sharing one predicate:
|
|
9654
10703
|
*
|
|
@@ -9657,7 +10706,7 @@ var FactsDao = class FactsDao {
|
|
|
9657
10706
|
* DELETE below failed with `SQLITE_CONSTRAINT_FOREIGNKEY` whenever a revision chain
|
|
9658
10707
|
* crossed `purge_after_archived_days`. Because the whole tick is ONE transaction and it
|
|
9659
10708
|
* runs from the store's constructor, that failure did not merely skip the purge: it rolled
|
|
9660
|
-
* back settle/
|
|
10709
|
+
* back settle/forget with it and left the process unable to start, permanently.
|
|
9661
10710
|
* 2. delete them.
|
|
9662
10711
|
*
|
|
9663
10712
|
* The predicate is built once and interpolated into both statements, so "which rows are
|
|
@@ -9702,7 +10751,7 @@ var FactsDao = class FactsDao {
|
|
|
9702
10751
|
purgeModifier: `-${p.purgeAfterDays} days`
|
|
9703
10752
|
};
|
|
9704
10753
|
}
|
|
9705
|
-
/** Archived rows still due for
|
|
10754
|
+
/** Archived rows still due for ③ after a budgeted pass (0 when the pass finished them). */
|
|
9706
10755
|
countPurgeBacklog(p) {
|
|
9707
10756
|
const [a, b] = FactsDao.purgeBranches(p);
|
|
9708
10757
|
return Number(this.db.prepare(`SELECT COUNT(*) AS n FROM (
|
|
@@ -9795,7 +10844,7 @@ function pathRecency(path) {
|
|
|
9795
10844
|
for (const edge of path.edges) newest = Math.max(newest, edgeTime(edge));
|
|
9796
10845
|
return newest;
|
|
9797
10846
|
}
|
|
9798
|
-
/** Dedup key of a path: the ordered edge ids (
|
|
10847
|
+
/** Dedup key of a path: the ordered edge ids ("路径去重按 (edge ids)"). */
|
|
9799
10848
|
function pathKey(path) {
|
|
9800
10849
|
return path.edges.map((edge) => edge.id).join("\0");
|
|
9801
10850
|
}
|
|
@@ -10799,156 +11848,6 @@ var FactSourcesDao = class {
|
|
|
10799
11848
|
}
|
|
10800
11849
|
};
|
|
10801
11850
|
//#endregion
|
|
10802
|
-
//#region ../core/lib/lifecycle/trust.js
|
|
10803
|
-
const DAY_MS$1 = 864e5;
|
|
10804
|
-
/** The reinforcement quota window is a CALENDAR day ("每天" is a calendar notion, §2.3). */
|
|
10805
|
-
const QUOTA_WINDOW_MS = DAY_MS$1;
|
|
10806
|
-
function clamp01(v) {
|
|
10807
|
-
if (!Number.isFinite(v)) return 0;
|
|
10808
|
-
return Math.min(1, Math.max(0, v));
|
|
10809
|
-
}
|
|
10810
|
-
function isPinned(row) {
|
|
10811
|
-
return Number(row.pinned) === 1;
|
|
10812
|
-
}
|
|
10813
|
-
/**
|
|
10814
|
-
* `'YYYY-MM-DD HH:MM:SS'` (SQLite UTC) or ISO → epoch ms; null/garbage → null.
|
|
10815
|
-
*
|
|
10816
|
-
* A stamp with NO zone designator is read as UTC, not as local time. `Date.parse` treats a zone-less
|
|
10817
|
-
* ISO string as LOCAL, and everything in this store is written in UTC (`CURRENT_TIMESTAMP`, and
|
|
10818
|
-
* {@link formatUtcTs}), so handing one over as-is would shift every quota window and every decay
|
|
10819
|
-
* measurement by the host's UTC offset — a fact reinforced at 23:00 UTC+8 would look reinforced 8
|
|
10820
|
-
* hours earlier, and the 24h cap would open or close at the wrong moment. An explicit `Z` or
|
|
10821
|
-
* `±HH:MM` is left alone.
|
|
10822
|
-
*/
|
|
10823
|
-
function parseUtcTs(ts) {
|
|
10824
|
-
if (ts === null || ts === void 0 || ts === "") return null;
|
|
10825
|
-
const iso = ts.includes("T") ? ts : ts.replace(" ", "T");
|
|
10826
|
-
const zoned = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(iso) ? iso : `${iso}Z`;
|
|
10827
|
-
const ms = Date.parse(zoned);
|
|
10828
|
-
return Number.isFinite(ms) ? ms : null;
|
|
10829
|
-
}
|
|
10830
|
-
/** Epoch ms → `'YYYY-MM-DD HH:MM:SS'` (the SQLite `CURRENT_TIMESTAMP` shape, UTC). */
|
|
10831
|
-
function formatUtcTs(ms) {
|
|
10832
|
-
return new Date(ms).toISOString().slice(0, 19).replace("T", " ");
|
|
10833
|
-
}
|
|
10834
|
-
/** eff(f) — the trust a caller should act on right now (spec §2.2). */
|
|
10835
|
-
function effectiveTrust(row, clock, cfg, enabled = cfg.enabled) {
|
|
10836
|
-
if (!enabled) return clamp01(row.trust_score);
|
|
10837
|
-
if (isPinned(row)) return 1;
|
|
10838
|
-
return clamp01(row.trust_score - cfg.decay_per_day * (clock - row.settle_clock));
|
|
10839
|
-
}
|
|
10840
|
-
/**
|
|
10841
|
-
* What a caller should SEE for a row — the R11 display rule (spec §6):
|
|
10842
|
-
* `active` → `eff()`; **non-active → the stored value verbatim** (the decay
|
|
10843
|
-
* formula keeps running mathematically for archived rows, so the projection must
|
|
10844
|
-
* not use it), and `enabled=false` → stored value (D12).
|
|
10845
|
-
*/
|
|
10846
|
-
function displayTrust(row, clock, cfg, enabled = cfg.enabled) {
|
|
10847
|
-
if (row.status !== "active") return clamp01(row.trust_score);
|
|
10848
|
-
return effectiveTrust(row, clock, cfg, enabled);
|
|
10849
|
-
}
|
|
10850
|
-
/**
|
|
10851
|
-
* Remaining active days: `eff / step`. `null` for pinned rows, non-active rows and
|
|
10852
|
-
* when trust is disabled — the contract's `remaining_days` (spec §6 / R11).
|
|
10853
|
-
*/
|
|
10854
|
-
function remainingDays(row, clock, cfg, enabled = cfg.enabled) {
|
|
10855
|
-
if (row.status !== "active" || isPinned(row) || !enabled || cfg.decay_per_day <= 0) return null;
|
|
10856
|
-
return effectiveTrust(row, clock, cfg, enabled) / cfg.decay_per_day;
|
|
10857
|
-
}
|
|
10858
|
-
function rollWindow(row, nowMs) {
|
|
10859
|
-
const at = row.bonus_window_at;
|
|
10860
|
-
const started = parseUtcTs(at);
|
|
10861
|
-
if (at === null || started === null || nowMs - started >= QUOTA_WINDOW_MS) return {
|
|
10862
|
-
count: 0,
|
|
10863
|
-
at: formatUtcTs(nowMs),
|
|
10864
|
-
reset: true
|
|
10865
|
-
};
|
|
10866
|
-
return {
|
|
10867
|
-
count: row.bonus_count ?? 0,
|
|
10868
|
-
at,
|
|
10869
|
-
reset: false
|
|
10870
|
-
};
|
|
10871
|
-
}
|
|
10872
|
-
/**
|
|
10873
|
-
* Apply one recall to a fact: settle, then grant a bonus inside the daily quota.
|
|
10874
|
-
* Returns `next === settled` when the quota is exhausted, the ceiling is reached,
|
|
10875
|
-
* the fact is pinned, or the gain would be zero (R2/R10/D11).
|
|
10876
|
-
*/
|
|
10877
|
-
function grantRecallBonus(row, clock, nowMs, cfg, enabled = cfg.enabled) {
|
|
10878
|
-
const settled = effectiveTrust(row, clock, cfg, enabled);
|
|
10879
|
-
const window = rollWindow(row, nowMs);
|
|
10880
|
-
if (row.status !== "active" || isPinned(row) || !enabled || window.count >= cfg.recall_daily_cap) return {
|
|
10881
|
-
settled,
|
|
10882
|
-
next: settled,
|
|
10883
|
-
granted: false,
|
|
10884
|
-
bonusCount: window.count,
|
|
10885
|
-
windowAt: window.at
|
|
10886
|
-
};
|
|
10887
|
-
const gain = cfg.recall_delta * Math.pow(cfg.recall_marginal_decay, window.count);
|
|
10888
|
-
const next = settled <= cfg.recall_floor ? Math.max(settled, cfg.recall_floor) : Math.max(settled, Math.min(cfg.recall_ceiling, settled + gain));
|
|
10889
|
-
if (next <= settled + 1e-9) return {
|
|
10890
|
-
settled,
|
|
10891
|
-
next: settled,
|
|
10892
|
-
granted: false,
|
|
10893
|
-
bonusCount: window.count,
|
|
10894
|
-
windowAt: window.at
|
|
10895
|
-
};
|
|
10896
|
-
return {
|
|
10897
|
-
settled,
|
|
10898
|
-
next,
|
|
10899
|
-
granted: true,
|
|
10900
|
-
bonusCount: window.count + 1,
|
|
10901
|
-
windowAt: window.at
|
|
10902
|
-
};
|
|
10903
|
-
}
|
|
10904
|
-
/**
|
|
10905
|
-
* Apply one explicit `helpful` (+1) / `unhelpful` (-1) step.
|
|
10906
|
-
*
|
|
10907
|
-
* Pinned and archived rows are untouched (R7): only `helpful_count` changes for
|
|
10908
|
-
* them, so a pinned fact can never end up storing 0.95 while displaying 1.0.
|
|
10909
|
-
* When `feedback_daily_cap > 0` feedback shares the per-fact 24h window counter.
|
|
10910
|
-
*/
|
|
10911
|
-
function applyFeedbackDelta(row, clock, delta, nowMs, cfg, enabled = cfg.enabled) {
|
|
10912
|
-
const settled = effectiveTrust(row, clock, cfg, enabled);
|
|
10913
|
-
const shared = cfg.feedback_daily_cap > 0;
|
|
10914
|
-
const window = shared ? rollWindow(row, nowMs) : {
|
|
10915
|
-
count: row.bonus_count ?? 0,
|
|
10916
|
-
at: row.bonus_window_at ?? formatUtcTs(nowMs),
|
|
10917
|
-
reset: false
|
|
10918
|
-
};
|
|
10919
|
-
if (row.status !== "active" || isPinned(row) || !enabled) return {
|
|
10920
|
-
settled,
|
|
10921
|
-
next: row.trust_score,
|
|
10922
|
-
pin: false,
|
|
10923
|
-
forget: false,
|
|
10924
|
-
untouched: true,
|
|
10925
|
-
bonusCount: window.count,
|
|
10926
|
-
windowAt: window.at
|
|
10927
|
-
};
|
|
10928
|
-
if (shared && window.count >= cfg.feedback_daily_cap) return {
|
|
10929
|
-
settled,
|
|
10930
|
-
next: settled,
|
|
10931
|
-
pin: false,
|
|
10932
|
-
forget: false,
|
|
10933
|
-
untouched: false,
|
|
10934
|
-
bonusCount: window.count,
|
|
10935
|
-
windowAt: window.at
|
|
10936
|
-
};
|
|
10937
|
-
let next = clamp01(settled + delta * cfg.feedback_delta);
|
|
10938
|
-
const pin = next >= cfg.permanent_threshold;
|
|
10939
|
-
const forget = !pin && next <= cfg.forget_threshold + 1e-9;
|
|
10940
|
-
if (forget) next = cfg.forget_threshold;
|
|
10941
|
-
return {
|
|
10942
|
-
settled,
|
|
10943
|
-
next,
|
|
10944
|
-
pin,
|
|
10945
|
-
forget,
|
|
10946
|
-
untouched: false,
|
|
10947
|
-
bonusCount: shared ? window.count + 1 : window.count,
|
|
10948
|
-
windowAt: window.at
|
|
10949
|
-
};
|
|
10950
|
-
}
|
|
10951
|
-
//#endregion
|
|
10952
11851
|
//#region ../core/lib/lifecycle/presence.js
|
|
10953
11852
|
const CLOCK_KEY = "trust_clock";
|
|
10954
11853
|
const LAST_SEEN_KEY = "trust_last_seen";
|
|
@@ -10996,39 +11895,27 @@ function advancePresence(db, opts) {
|
|
|
10996
11895
|
function runTrustTick(db, cfg, opts) {
|
|
10997
11896
|
const clock = opts.clock;
|
|
10998
11897
|
const budget = opts.budget ?? cfg.trust.tick_max_facts;
|
|
10999
|
-
const
|
|
11898
|
+
const curve = trustCurveOf(cfg.trust);
|
|
11000
11899
|
const enabled = cfg.trust.enabled ? 1 : 0;
|
|
11001
11900
|
const trustEnabled = cfg.trust.enabled;
|
|
11002
11901
|
const facts = new FactsDao(db);
|
|
11003
11902
|
return db.transaction(() => {
|
|
11004
11903
|
const settled = !trustEnabled ? 0 : budget > 0 ? facts.settleBudgeted({
|
|
11005
|
-
|
|
11904
|
+
curve,
|
|
11006
11905
|
clock,
|
|
11007
11906
|
budget
|
|
11008
11907
|
}) : facts.settleAll({
|
|
11009
|
-
|
|
11908
|
+
curve,
|
|
11010
11909
|
clock
|
|
11011
11910
|
});
|
|
11012
11911
|
const skipped = trustEnabled && budget > 0 ? facts.countPendingSettle({ clock }) : 0;
|
|
11013
11912
|
const archiveBudget = budget > 0 ? budget : 0;
|
|
11014
|
-
const ttlIds = facts.archiveExpiredByTtl({
|
|
11015
|
-
clock,
|
|
11016
|
-
budget: archiveBudget
|
|
11017
|
-
});
|
|
11018
11913
|
const forgotIds = trustEnabled ? facts.archiveForgotten({
|
|
11019
11914
|
clock,
|
|
11020
11915
|
forgetThreshold: cfg.trust.forget_threshold,
|
|
11021
11916
|
budget: archiveBudget
|
|
11022
11917
|
}) : [];
|
|
11023
|
-
const
|
|
11024
|
-
clock,
|
|
11025
|
-
idleCalendarDays: cfg.trust.idle_calendar_days,
|
|
11026
|
-
budget: archiveBudget
|
|
11027
|
-
});
|
|
11028
|
-
const deferred = archiveBudget > 0 && (ttlIds.length === archiveBudget || forgotIds.length === archiveBudget || idleIds.length === archiveBudget) ? facts.countArchiveBacklog({
|
|
11029
|
-
forgetThreshold: cfg.trust.forget_threshold,
|
|
11030
|
-
idleCalendarDays: cfg.trust.idle_calendar_days
|
|
11031
|
-
}) : 0;
|
|
11918
|
+
const deferred = archiveBudget > 0 && forgotIds.length === archiveBudget ? facts.countArchiveBacklog({ forgetThreshold: cfg.trust.forget_threshold }) : 0;
|
|
11032
11919
|
const purgeArgs = {
|
|
11033
11920
|
clock,
|
|
11034
11921
|
enabled,
|
|
@@ -11043,16 +11930,10 @@ function runTrustTick(db, cfg, opts) {
|
|
|
11043
11930
|
return {
|
|
11044
11931
|
clock,
|
|
11045
11932
|
settled,
|
|
11046
|
-
archived_ttl: ttlIds.length,
|
|
11047
11933
|
archived_forgot: forgotIds.length,
|
|
11048
|
-
archived_idle: idleIds.length,
|
|
11049
11934
|
purged: purgedIds.length,
|
|
11050
11935
|
purged_ids: purgedIds,
|
|
11051
|
-
archived_ids: [
|
|
11052
|
-
...ttlIds,
|
|
11053
|
-
...forgotIds,
|
|
11054
|
-
...idleIds
|
|
11055
|
-
],
|
|
11936
|
+
archived_ids: [...forgotIds],
|
|
11056
11937
|
skipped,
|
|
11057
11938
|
archived_deferred: deferred,
|
|
11058
11939
|
purged_deferred: purgedDeferred
|
|
@@ -11076,15 +11957,15 @@ function runMaintenance(db, cfg, opts) {
|
|
|
11076
11957
|
});
|
|
11077
11958
|
return {
|
|
11078
11959
|
decayed: tick.settled,
|
|
11079
|
-
archived_ttl:
|
|
11080
|
-
archived_age: tick.archived_forgot
|
|
11960
|
+
archived_ttl: 0,
|
|
11961
|
+
archived_age: tick.archived_forgot,
|
|
11081
11962
|
purged: tick.purged,
|
|
11082
11963
|
purged_ids: tick.purged_ids,
|
|
11083
11964
|
archived_ids: tick.archived_ids,
|
|
11084
11965
|
settled: tick.settled,
|
|
11085
11966
|
clock: tick.clock,
|
|
11086
11967
|
archived_forgot: tick.archived_forgot,
|
|
11087
|
-
archived_idle:
|
|
11968
|
+
archived_idle: 0,
|
|
11088
11969
|
skipped: tick.skipped,
|
|
11089
11970
|
archived_deferred: tick.archived_deferred,
|
|
11090
11971
|
purged_deferred: tick.purged_deferred
|
|
@@ -11483,7 +12364,6 @@ const RESTORE_METADATA_FIELDS = [
|
|
|
11483
12364
|
"last_reinforced_at",
|
|
11484
12365
|
"ttl_days",
|
|
11485
12366
|
"valid_from",
|
|
11486
|
-
"valid_to",
|
|
11487
12367
|
"supersedes_source_id",
|
|
11488
12368
|
"archive",
|
|
11489
12369
|
"sources",
|
|
@@ -11629,9 +12509,7 @@ function rowOf(raw, metadataMissing, nowSql) {
|
|
|
11629
12509
|
lastRetrievedAt: textOrNull$1(row["last_retrieved_at"]),
|
|
11630
12510
|
ttlDays: Math.max(0, integerOrNull(row["ttl_days"]) ?? 0),
|
|
11631
12511
|
validFrom: textOrNull$1(row["valid_from"]),
|
|
11632
|
-
validTo: textOrNull$1(row["valid_to"]),
|
|
11633
12512
|
validFromMs: eventTimeMs(textOrNull$1(row["valid_from"])) ?? null,
|
|
11634
|
-
validToMs: eventTimeMs(textOrNull$1(row["valid_to"])) ?? null,
|
|
11635
12513
|
supersedesId: integerOrNull(row["supersedes_source_id"]),
|
|
11636
12514
|
mirrorSource: textOrNull$1(row["mirror_source"]),
|
|
11637
12515
|
mirrorTarget: textOrNull$1(row["mirror_target"]),
|
|
@@ -11674,10 +12552,11 @@ const INSERT_FACT_SQL = `INSERT INTO facts (
|
|
|
11674
12552
|
fact_id, content, kind, title, legacy_category, status, archived_at, archive_reason,
|
|
11675
12553
|
created_at, updated_at, trust_score, settle_clock, assert_count, helpful_count,
|
|
11676
12554
|
pinned, pinned_at, last_reinforced_at, bonus_count, bonus_window_at,
|
|
11677
|
-
retrieval_count, last_retrieved_at, ttl_days, valid_from,
|
|
11678
|
-
supersedes_id, mirror_source, mirror_target,
|
|
12555
|
+
retrieval_count, last_retrieved_at, ttl_days, valid_from, valid_from_ms,
|
|
12556
|
+
valid_to, valid_to_ms, supersedes_id, mirror_source, mirror_target,
|
|
11679
12557
|
hrr_vector, semantic_vector, embedding_model, entities_version, conflict_checked
|
|
11680
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
12558
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?,
|
|
12559
|
+
NULL, NULL, NULL, 0, 1)`;
|
|
11681
12560
|
function countOf(db, sql) {
|
|
11682
12561
|
return Number(db.prepare(sql).get()?.n ?? 0);
|
|
11683
12562
|
}
|
|
@@ -11728,7 +12607,7 @@ async function restoreFactsDocument(target, doc, options = {}) {
|
|
|
11728
12607
|
const insertSource = target.db.prepare("INSERT INTO fact_sources (fact_id, kind, ref) VALUES (?, ?, ?)");
|
|
11729
12608
|
for (const row of rows) {
|
|
11730
12609
|
try {
|
|
11731
|
-
insertFact.run(row.factId, row.content, row.kind, row.title, row.legacyCategory, row.status, row.archivedAt, row.archiveReason, row.createdAt, row.updatedAt, row.trustScore, row.settleClock ?? clock, row.assertCount, row.helpfulCount, row.pinned ? 1 : 0, row.pinnedAt, row.lastReinforcedAt, row.bonusCount, row.bonusWindowAt, row.retrievalCount, row.lastRetrievedAt, row.ttlDays, row.validFrom, row.
|
|
12610
|
+
insertFact.run(row.factId, row.content, row.kind, row.title, row.legacyCategory, row.status, row.archivedAt, row.archiveReason, row.createdAt, row.updatedAt, row.trustScore, row.settleClock ?? clock, row.assertCount, row.helpfulCount, row.pinned ? 1 : 0, row.pinnedAt, row.lastReinforcedAt, row.bonusCount, row.bonusWindowAt, row.retrievalCount, row.lastRetrievedAt, row.ttlDays, row.validFrom, row.validFromMs, row.supersedesId, row.mirrorSource, row.mirrorTarget);
|
|
11732
12611
|
for (const source of row.sources) insertSource.run(row.factId, source.kind, source.ref);
|
|
11733
12612
|
if (row.relations.length > 0) {
|
|
11734
12613
|
const report = writeExplicitEdgesForFact(target.db, row.factId, row.relations);
|
|
@@ -11826,9 +12705,6 @@ var SqliteMemoryRepository = class {
|
|
|
11826
12705
|
countForgettingWithin(p) {
|
|
11827
12706
|
return this.dao.countForgettingWithin(p);
|
|
11828
12707
|
}
|
|
11829
|
-
countIdleCandidates(days) {
|
|
11830
|
-
return this.dao.countIdleCandidates(days);
|
|
11831
|
-
}
|
|
11832
12708
|
countArchiveBacklog(p) {
|
|
11833
12709
|
return this.dao.countArchiveBacklog(p);
|
|
11834
12710
|
}
|
|
@@ -11911,11 +12787,11 @@ var SqliteMemoryRepository = class {
|
|
|
11911
12787
|
factId: Number(existing.fact_id)
|
|
11912
12788
|
};
|
|
11913
12789
|
}
|
|
11914
|
-
applyValidity(factId, validFrom
|
|
11915
|
-
this.dao.applyValidity(factId, validFrom
|
|
12790
|
+
applyValidity(factId, validFrom) {
|
|
12791
|
+
this.dao.applyValidity(factId, validFrom);
|
|
11916
12792
|
}
|
|
11917
|
-
applyValues(factId, kind
|
|
11918
|
-
this.dao.applyValues(factId, kind
|
|
12793
|
+
applyValues(factId, kind) {
|
|
12794
|
+
this.dao.applyValues(factId, kind);
|
|
11919
12795
|
}
|
|
11920
12796
|
setTitle(factId, title) {
|
|
11921
12797
|
this.dao.setTitle(factId, title);
|
|
@@ -11932,17 +12808,14 @@ var SqliteMemoryRepository = class {
|
|
|
11932
12808
|
incrementAssertCount(factId) {
|
|
11933
12809
|
return this.dao.incrementAssertCount(factId);
|
|
11934
12810
|
}
|
|
11935
|
-
markValidTo(factId, at, atMs) {
|
|
11936
|
-
this.dao.markValidTo(factId, at, atMs);
|
|
11937
|
-
}
|
|
11938
12811
|
linkSupersede(factId, supersedesId) {
|
|
11939
12812
|
this.dao.linkSupersede(factId, supersedesId);
|
|
11940
12813
|
}
|
|
11941
12814
|
archiveSuperseded(factId, clock) {
|
|
11942
12815
|
return this.dao.archiveSuperseded(factId, clock);
|
|
11943
12816
|
}
|
|
11944
|
-
revive(factId, kind,
|
|
11945
|
-
return this.dao.revive(factId, kind,
|
|
12817
|
+
revive(factId, kind, trust, clock) {
|
|
12818
|
+
return this.dao.revive(factId, kind, trust, clock);
|
|
11946
12819
|
}
|
|
11947
12820
|
archive(factId, clock, reason) {
|
|
11948
12821
|
return this.dao.archive(factId, clock, reason);
|
|
@@ -11950,24 +12823,12 @@ var SqliteMemoryRepository = class {
|
|
|
11950
12823
|
restore(factId, trust, clock) {
|
|
11951
12824
|
return this.dao.restore(factId, trust, clock);
|
|
11952
12825
|
}
|
|
11953
|
-
bumpHelpful(factId, delta) {
|
|
11954
|
-
this.dao.bumpHelpful(factId, delta);
|
|
11955
|
-
}
|
|
11956
12826
|
pin(factId, clock) {
|
|
11957
12827
|
return this.dao.pin(factId, clock);
|
|
11958
12828
|
}
|
|
11959
12829
|
unpin(factId, clock) {
|
|
11960
12830
|
return this.dao.unpin(factId, clock);
|
|
11961
12831
|
}
|
|
11962
|
-
pinOutcome(factId, clock, bonusCount, windowAt) {
|
|
11963
|
-
this.dao.pinOutcome(factId, clock, bonusCount, windowAt);
|
|
11964
|
-
}
|
|
11965
|
-
forgetOutcome(factId, trust, clock, bonusCount, windowAt) {
|
|
11966
|
-
this.dao.forgetOutcome(factId, trust, clock, bonusCount, windowAt);
|
|
11967
|
-
}
|
|
11968
|
-
reinforceOutcome(factId, trust, clock, bonusCount, windowAt) {
|
|
11969
|
-
this.dao.reinforceOutcome(factId, trust, clock, bonusCount, windowAt);
|
|
11970
|
-
}
|
|
11971
12832
|
touchUsage(ids) {
|
|
11972
12833
|
this.dao.touchUsage(ids);
|
|
11973
12834
|
}
|
|
@@ -15618,6 +16479,22 @@ const NO_CHECK = {
|
|
|
15618
16479
|
*/
|
|
15619
16480
|
const RETRIEVAL_HEALTH_KEY = "retrieval_health";
|
|
15620
16481
|
/**
|
|
16482
|
+
* The MODEL face's ONE instant renderer: a stored stamp → ISO-8601 **with the host's offset**
|
|
16483
|
+
* (`2026-10-28T19:58:48+08:00`), via `@avantf/mem-contract/time`'s `formatOffsetIso`.
|
|
16484
|
+
*
|
|
16485
|
+
* Every timestamp this store hands UP goes through here, and none of them is localized anywhere
|
|
16486
|
+
* else: the DB column stays UTC (`CURRENT_TIMESTAMP`) and the export document stays byte-identical
|
|
16487
|
+
* (`facts_io` reads rows directly), while the model receives an unambiguous instant and needs no
|
|
16488
|
+
* timezone arithmetic of its own. `null` means "absent/unreadable" — the CALLER keeps its existing
|
|
16489
|
+
* spelling (`''` for `created_at`, `null` for the nullable ones) instead of an empty time.
|
|
16490
|
+
*
|
|
16491
|
+
* NOT applied to `valid_from` / `event_date`: that is a calendar DATE the caller wrote, with no time
|
|
16492
|
+
* and no zone, and rendering it through a clock would invent a midnight.
|
|
16493
|
+
*/
|
|
16494
|
+
function modelInstant(value) {
|
|
16495
|
+
return formatOffsetIso(value === null || value === void 0 ? null : String(value));
|
|
16496
|
+
}
|
|
16497
|
+
/**
|
|
15621
16498
|
* The ONE stored-title-row → `TitleCandidate` mapper, shared by the exact page and the fuzzy
|
|
15622
16499
|
* shortlist so a candidate cannot describe the same row two ways.
|
|
15623
16500
|
*
|
|
@@ -15635,8 +16512,8 @@ function titleCandidatesOf(rows, includeExcerpt) {
|
|
|
15635
16512
|
matched_title: row.matched_title === null || row.matched_title === void 0 ? String(row.title) : String(row.matched_title),
|
|
15636
16513
|
memory_kind: mapLegacyKind(row.kind),
|
|
15637
16514
|
status: row.status === "archived" ? "archived" : "active",
|
|
15638
|
-
created_at: row.created_at
|
|
15639
|
-
updated_at:
|
|
16515
|
+
created_at: modelInstant(row.created_at) ?? "",
|
|
16516
|
+
updated_at: modelInstant(row.updated_at),
|
|
15640
16517
|
...row.valid_from === null || row.valid_from === void 0 ? {} : { event_date: String(row.valid_from) },
|
|
15641
16518
|
...includeExcerpt && row.excerpt !== null ? { excerpt: String(row.excerpt) } : {}
|
|
15642
16519
|
}));
|
|
@@ -15663,8 +16540,8 @@ function factEvidenceOf(row) {
|
|
|
15663
16540
|
status: row["status"] === "archived" ? "archived" : "active",
|
|
15664
16541
|
...title === null || title === void 0 || title === "" ? {} : { title: String(title) },
|
|
15665
16542
|
...row["valid_from"] === null || row["valid_from"] === void 0 ? {} : { event_date: String(row["valid_from"]) },
|
|
15666
|
-
created_at: row["created_at"]
|
|
15667
|
-
updated_at:
|
|
16543
|
+
created_at: modelInstant(row["created_at"]) ?? "",
|
|
16544
|
+
updated_at: modelInstant(row["updated_at"])
|
|
15668
16545
|
};
|
|
15669
16546
|
}
|
|
15670
16547
|
var MemoryStore = class {
|
|
@@ -15816,7 +16693,7 @@ var MemoryStore = class {
|
|
|
15816
16693
|
dim: this.vstore.dim
|
|
15817
16694
|
});
|
|
15818
16695
|
}
|
|
15819
|
-
async add(content, kind,
|
|
16696
|
+
async add(content, kind, opts) {
|
|
15820
16697
|
const normalized = normalizeWrite(content).trim();
|
|
15821
16698
|
if (!normalized) throw new Error("内容不能为空");
|
|
15822
16699
|
const secret = detectSecretShape(normalized);
|
|
@@ -15829,7 +16706,7 @@ var MemoryStore = class {
|
|
|
15829
16706
|
if (checkedKind !== void 0 && requiresEventDate(checkedKind) && opts?.eventDate === void 0) throw new Error(JOURNAL_NEEDS_EVENT_DATE);
|
|
15830
16707
|
const explicitTriples = opts?.relations === void 0 ? void 0 : await this.relations.explicitRelationTriples(checkedKind ?? "note", opts.relations);
|
|
15831
16708
|
const title = this.resolveWriteTitle(opts?.title, checkedKind);
|
|
15832
|
-
const { fact_id, is_new, revived, assert_count } = await this.persistFact(normalized, checkedKind,
|
|
16709
|
+
const { fact_id, is_new, revived, assert_count } = await this.persistFact(normalized, checkedKind, entityWrites, triples, {
|
|
15833
16710
|
...opts,
|
|
15834
16711
|
title,
|
|
15835
16712
|
relations: void 0
|
|
@@ -15920,7 +16797,7 @@ var MemoryStore = class {
|
|
|
15920
16797
|
}], this.vectorWriteTarget(this.vectorSpace()));
|
|
15921
16798
|
return encoded === 1;
|
|
15922
16799
|
}
|
|
15923
|
-
async update(fact_id, content, kind,
|
|
16800
|
+
async update(fact_id, content, kind, opts) {
|
|
15924
16801
|
const row = await this.getRow(fact_id);
|
|
15925
16802
|
if (!row) throw new Error(`找不到 fact_id=${fact_id}`);
|
|
15926
16803
|
const normalized = normalizeWrite(content).trim();
|
|
@@ -15936,7 +16813,7 @@ var MemoryStore = class {
|
|
|
15936
16813
|
if (requiresEventDate(effectiveKind) && opts?.eventDate === void 0 && row.valid_from == null) throw new Error(JOURNAL_NEEDS_EVENT_DATE);
|
|
15937
16814
|
const title = this.resolveWriteTitle(opts?.title, effectiveKind);
|
|
15938
16815
|
const explicitTriples = opts?.relations === void 0 ? void 0 : await this.relations.explicitRelationTriples(effectiveKind, opts.relations);
|
|
15939
|
-
const { fact_id: newId, is_new, revived, assert_count } = await this.persistFact(normalized, checkedKind,
|
|
16816
|
+
const { fact_id: newId, is_new, revived, assert_count } = await this.persistFact(normalized, checkedKind, entityWrites, triples, {
|
|
15940
16817
|
...opts,
|
|
15941
16818
|
supersedesId: fact_id,
|
|
15942
16819
|
archiveOldIfActive: true,
|
|
@@ -15970,9 +16847,9 @@ var MemoryStore = class {
|
|
|
15970
16847
|
return changed > 0;
|
|
15971
16848
|
}
|
|
15972
16849
|
/**
|
|
15973
|
-
* Bring an archived fact back (spec §2.6): a `forgot` fact is lifted to
|
|
15974
|
-
* `
|
|
15975
|
-
*
|
|
16850
|
+
* Bring an archived fact back (spec §2.6): a `forgot` fact is lifted to the frozen base
|
|
16851
|
+
* `BASE_TRUST` (otherwise the very next tick would re-archive it, R4),
|
|
16852
|
+
* `last_retrieved_at` is refreshed (observation only — no clock reads it any more),
|
|
15976
16853
|
* and `settle_clock` moves to now so the downtime is not charged.
|
|
15977
16854
|
*/
|
|
15978
16855
|
async restore(fact_id) {
|
|
@@ -15981,7 +16858,7 @@ var MemoryStore = class {
|
|
|
15981
16858
|
const trust = this.config.trust;
|
|
15982
16859
|
const clock = await this.lifecycle.clock();
|
|
15983
16860
|
const stored = Number(row.trust_score);
|
|
15984
|
-
const next = String(row.archive_reason) === "forgot" || stored <= trust.forget_threshold ? Math.max(stored,
|
|
16861
|
+
const next = String(row.archive_reason) === "forgot" || stored <= trust.forget_threshold ? Math.max(stored, BASE_TRUST) : stored;
|
|
15985
16862
|
if (await this.facts.restore(fact_id, next, clock) === 0) return false;
|
|
15986
16863
|
const vec = bytesToFloat32(row.semantic_vector ?? null);
|
|
15987
16864
|
if (vec && vec.length === this.vstore.dim) this.vstore.add(fact_id, vec);
|
|
@@ -15989,42 +16866,6 @@ var MemoryStore = class {
|
|
|
15989
16866
|
if ((await this.detectContradictions(fact_id)).complete) await this.facts.markConflictChecked([fact_id]);
|
|
15990
16867
|
return true;
|
|
15991
16868
|
}
|
|
15992
|
-
async helpful(fact_id) {
|
|
15993
|
-
return await this.applyFeedback(fact_id, 1);
|
|
15994
|
-
}
|
|
15995
|
-
async unhelpful(fact_id) {
|
|
15996
|
-
return await this.applyFeedback(fact_id, -1);
|
|
15997
|
-
}
|
|
15998
|
-
/**
|
|
15999
|
-
* Explicit feedback (spec §2.4). Pinned/archived rows only move `helpful_count`
|
|
16000
|
-
* (R7); otherwise the decay is settled first, then `±feedback_delta`, pin at the
|
|
16001
|
-
* permanent threshold (snap to 1.0) or archive immediately at the forget line.
|
|
16002
|
-
*/
|
|
16003
|
-
async applyFeedback(fact_id, delta) {
|
|
16004
|
-
if (await this.getRow(fact_id) === void 0) return null;
|
|
16005
|
-
const trust = this.config.trust;
|
|
16006
|
-
const evict = await this.lifecycle.immediateTransaction(async () => {
|
|
16007
|
-
const row = await this.getRow(fact_id);
|
|
16008
|
-
if (!row) return null;
|
|
16009
|
-
const clock = await this.lifecycle.clock();
|
|
16010
|
-
const outcome = applyFeedbackDelta(asTrustRow(row), clock, delta, Date.now(), trust);
|
|
16011
|
-
await this.facts.bumpHelpful(fact_id, delta);
|
|
16012
|
-
if (outcome.untouched) return null;
|
|
16013
|
-
if (outcome.pin) {
|
|
16014
|
-
await this.facts.pinOutcome(fact_id, clock, outcome.bonusCount, outcome.windowAt);
|
|
16015
|
-
return null;
|
|
16016
|
-
}
|
|
16017
|
-
if (outcome.forget) {
|
|
16018
|
-
await this.facts.forgetOutcome(fact_id, outcome.next, clock, outcome.bonusCount, outcome.windowAt);
|
|
16019
|
-
await this.contradictions.resolveForFact(fact_id);
|
|
16020
|
-
return fact_id;
|
|
16021
|
-
}
|
|
16022
|
-
await this.facts.reinforceOutcome(fact_id, outcome.next, clock, outcome.bonusCount, outcome.windowAt);
|
|
16023
|
-
return null;
|
|
16024
|
-
});
|
|
16025
|
-
if (evict !== null) evictVectors(this.vstore, [evict]);
|
|
16026
|
-
return (await this.getRow(fact_id))?.helpful_count ?? 0;
|
|
16027
|
-
}
|
|
16028
16869
|
/** Permanent-memory administration (spec §6). */
|
|
16029
16870
|
async pin(fact_id) {
|
|
16030
16871
|
return await this.facts.pin(fact_id, await this.lifecycle.clock()) > 0;
|
|
@@ -16042,15 +16883,15 @@ var MemoryStore = class {
|
|
|
16042
16883
|
const clock = await this.lifecycle.clock();
|
|
16043
16884
|
const active = await this.facts.countActive();
|
|
16044
16885
|
const pinned = await this.facts.countPinnedActive();
|
|
16045
|
-
const
|
|
16046
|
-
|
|
16886
|
+
const curve = curveOf(trust);
|
|
16887
|
+
const forgettingSoon = await this.facts.countForgettingWithin({
|
|
16888
|
+
threshold: ceilingForDays(FORGETTING_SOON_DAYS, curve),
|
|
16047
16889
|
clock,
|
|
16048
|
-
|
|
16049
|
-
})
|
|
16890
|
+
curve
|
|
16891
|
+
});
|
|
16050
16892
|
const bonus = await this.facts.bonusStatedToday();
|
|
16051
16893
|
const reinforcedToday = bonus.count;
|
|
16052
16894
|
const grantedToday = bonus.granted;
|
|
16053
|
-
const idleCandidates = await this.facts.countIdleCandidates(trust.idle_calendar_days);
|
|
16054
16895
|
const reasons = await this.facts.archivedByReason();
|
|
16055
16896
|
const oldest = await this.facts.minSettleClock();
|
|
16056
16897
|
return {
|
|
@@ -16061,7 +16902,6 @@ var MemoryStore = class {
|
|
|
16061
16902
|
forgetting_soon: forgettingSoon,
|
|
16062
16903
|
reinforced_today: reinforcedToday,
|
|
16063
16904
|
bonus_granted_today: grantedToday,
|
|
16064
|
-
idle_candidates: idleCandidates,
|
|
16065
16905
|
conflict_pending: await this.facts.countPendingConflicts(),
|
|
16066
16906
|
entities_stale: await this.facts.countStaleEntities(3),
|
|
16067
16907
|
archived_by_reason: Object.fromEntries(reasons.map((r) => [r.reason ?? "unknown", r.n])),
|
|
@@ -16194,9 +17034,48 @@ var MemoryStore = class {
|
|
|
16194
17034
|
await (this.semantic.warmUp?.() ?? Promise.resolve());
|
|
16195
17035
|
return this.semantic.isAvailable();
|
|
16196
17036
|
}
|
|
16197
|
-
/**
|
|
17037
|
+
/**
|
|
17038
|
+
* Point the semantic backend at a shared runtime-dependency root (the base provider's
|
|
17039
|
+
* `runtime-deps` handle) before it warms. Best-effort by contract: a backend without external
|
|
17040
|
+
* module resolution ignores it, and a missing root is reported by the backend itself.
|
|
17041
|
+
*/
|
|
17042
|
+
useSemanticModuleDir(dir) {
|
|
17043
|
+
this.semantic.useModuleDir?.(dir);
|
|
17044
|
+
}
|
|
17045
|
+
/**
|
|
17046
|
+
* Point the vector store at a shared runtime-dependency root for its NATIVE binding
|
|
17047
|
+
* (`hnswlib-node`), after the DSH plugin's native self-heal item is ready.
|
|
17048
|
+
*
|
|
17049
|
+
* Same best-effort contract as {@link useSemanticModuleDir}: a store with no native binding (the
|
|
17050
|
+
* brute-force backend, or an `auto` store still on it) ignores it, and a store that already fell
|
|
17051
|
+
* back to brute force rebuilds its tracked vectors under the new root. Nothing here decides whether
|
|
17052
|
+
* a root should exist — the caller's dependency audit does.
|
|
17053
|
+
*/
|
|
17054
|
+
useNativeModuleDir(dir) {
|
|
17055
|
+
this.vstore.useModuleDir?.(dir);
|
|
17056
|
+
}
|
|
17057
|
+
/**
|
|
17058
|
+
* Vectors currently in the LIVE index (sync, one map read).
|
|
17059
|
+
*
|
|
17060
|
+
* Exposed for the plugin's dependency audit: an `hnswlib-node` notice is conditioned on the corpus
|
|
17061
|
+
* actually being large enough for an ANN index to matter (`min_vectors`), and a session line that
|
|
17062
|
+
* asks "how big is the store right now" must not run a database query to answer.
|
|
17063
|
+
*/
|
|
17064
|
+
liveVectorCount() {
|
|
17065
|
+
return this.vstore.count();
|
|
17066
|
+
}
|
|
17067
|
+
/**
|
|
17068
|
+
* List contradiction candidates (`resolved` 0 = open, 1 = closed with a recorded verdict).
|
|
17069
|
+
*
|
|
17070
|
+
* `detected_at` is mapped to the model face's instant shape here (the DAO hands back the raw UTC
|
|
17071
|
+
* column): this payload is what the 记忆 tab's 未处理矛盾 table and the CLI both render, and it must
|
|
17072
|
+
* not be the one surface still printing a zone-less stamp.
|
|
17073
|
+
*/
|
|
16198
17074
|
async listContradictions(opts) {
|
|
16199
|
-
return await this.contradictions.list(opts?.resolved ?? 0, opts?.kind, opts?.threshold, opts?.limit ?? 10)
|
|
17075
|
+
return (await this.contradictions.list(opts?.resolved ?? 0, opts?.kind, opts?.threshold, opts?.limit ?? 10)).map((row) => ({
|
|
17076
|
+
...row,
|
|
17077
|
+
detected_at: modelInstant(row.detected_at) ?? row.detected_at
|
|
17078
|
+
}));
|
|
16200
17079
|
}
|
|
16201
17080
|
/**
|
|
16202
17081
|
* Adjudicate one logged conflict — the outlet the detector never had.
|
|
@@ -16242,7 +17121,6 @@ var MemoryStore = class {
|
|
|
16242
17121
|
reason: "loser_not_in_pair"
|
|
16243
17122
|
};
|
|
16244
17123
|
await this.archive(loserFactId, "contradiction");
|
|
16245
|
-
await this.facts.markValidTo(loserFactId, formatUtcTs(Date.now()), Date.now());
|
|
16246
17124
|
await this.contradictions.resolve(id, resolution, loserFactId);
|
|
16247
17125
|
return {
|
|
16248
17126
|
resolved: true,
|
|
@@ -16629,10 +17507,21 @@ var MemoryStore = class {
|
|
|
16629
17507
|
* ({@link timePage}, `mode:"time"`): the window is the whole question, so there is nothing to
|
|
16630
17508
|
* match, plan or rank.
|
|
16631
17509
|
*
|
|
16632
|
-
*
|
|
16633
|
-
*
|
|
16634
|
-
*
|
|
16635
|
-
*
|
|
17510
|
+
* What a pass does to the rows it returns is one decision, and it is deliberately NOT uniform:
|
|
17511
|
+
*
|
|
17512
|
+
* - the RELATION and FUZZY passes REINFORCE them ({@link reinforce}): each answers a question
|
|
17513
|
+
* the caller asked, so the rows are what it read;
|
|
17514
|
+
* - the TIME, EXACT and FACTS passes record USAGE ONLY ({@link touchOnly}): they hand back a
|
|
17515
|
+
* LIST the caller browses — page 2 of a listing is not a second recall — so "it was looked
|
|
17516
|
+
* at" still moves `retrieval_count` / `last_retrieved_at` (the dormancy clock) while
|
|
17517
|
+
* `trust_score` / `bonus_count` stay where they are.
|
|
17518
|
+
*
|
|
17519
|
+
* A caller that must stay read-only passes `track: false` (the conditional hint's probe does);
|
|
17520
|
+
* that branch writes nothing at all, usage included. Exact, facts, relation and time also honour
|
|
17521
|
+
* the caller's output budget (their text is the caller's to bound). The one difference is the
|
|
17522
|
+
* DEFAULT: a time window applies no budget unless the caller names one — its page size (`limit`)
|
|
17523
|
+
* is what bounds the list, and the budget would only rewrite rows the caller asked for (see
|
|
17524
|
+
* {@link timePage}).
|
|
16636
17525
|
*
|
|
16637
17526
|
* It does NOT emit a retrieval-health event. Those counters describe the retrieval PIPELINE
|
|
16638
17527
|
* (legs, fusion, semantic-live rate); none of an exact, facts, relation or time answer ran a leg,
|
|
@@ -16678,32 +17567,47 @@ var MemoryStore = class {
|
|
|
16678
17567
|
* happened that day" reads in the order it happened; `score` is the constant 1 exactly like the
|
|
16679
17568
|
* exact page's, because there is no relevance number here for the model to compare.
|
|
16680
17569
|
*
|
|
16681
|
-
*
|
|
16682
|
-
*
|
|
16683
|
-
*
|
|
17570
|
+
* The PAGE is the contract's, not the caller's guess: `limit` omitted means one page of
|
|
17571
|
+
* {@link TIME_DEFAULT_LIMIT} rows (the same 50 the contract caps an explicit page at), and an
|
|
17572
|
+
* explicit `limit` (1-50) still wins. A window that holds more than one page is the normal case
|
|
17573
|
+
* for a busy day, so the envelope does not leave the caller to infer the shortfall: it reports
|
|
17574
|
+
* `remaining = total - (offset + hits.length)`, the count still unseen.
|
|
17575
|
+
*
|
|
17576
|
+
* The output budget is honoured only when the caller names it (`max_tokens`). Falling back to the
|
|
17577
|
+
* configured `retrieval.max_output_tokens` here would rewrite the text of a window's rows behind a
|
|
17578
|
+
* page the caller already bounded with `limit` — the page size is the knob for "how much of the
|
|
17579
|
+
* list", and the budget stays a knob for a RANKED result (exact/facts/relation/hybrid unchanged).
|
|
17580
|
+
* `max_tokens` (including the contract's `0` = unlimited) is passed through unchanged.
|
|
17581
|
+
*
|
|
17582
|
+
* It honours `kind`/`source`, and records usage ONLY for the rows it returns unless `track: false`
|
|
17583
|
+
* ({@link touchOnly}). A window is listed so the caller can look through it, not recalled into an
|
|
17584
|
+
* answer: the dormancy clock (`retrieval_count` / `last_retrieved_at`) moves, trust does not.
|
|
16684
17585
|
*/
|
|
16685
17586
|
async timePage(input) {
|
|
16686
17587
|
if (input.time === void 0) throw new Error("mem_recall.search 需要 `query`,或只给 `time`(纯时间召回);两者都没有时无法检索。");
|
|
16687
17588
|
const time = normalizeTimeFilter(input.time);
|
|
16688
|
-
const
|
|
17589
|
+
const page = exactPage(input.offset, input.limit);
|
|
16689
17590
|
const filters = {
|
|
16690
17591
|
time,
|
|
16691
17592
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
16692
17593
|
...input.source === void 0 ? {} : { source: input.source }
|
|
16693
17594
|
};
|
|
16694
17595
|
const total = await this.searchIndex.countInTimeWindow(filters);
|
|
17596
|
+
const limit = input.limit === void 0 ? 50 : page.limit;
|
|
17597
|
+
const offset = page.offset;
|
|
16695
17598
|
const ids = total === 0 ? [] : await this.searchIndex.timeWindowRows({
|
|
16696
17599
|
...filters,
|
|
16697
17600
|
limit,
|
|
16698
17601
|
offset
|
|
16699
17602
|
});
|
|
16700
|
-
const { kept } = applyOutputBudget(this.config, await this.factHits(ids, () => 1), input.maxTokens);
|
|
16701
|
-
if (input.track !== false) await this.
|
|
17603
|
+
const { kept } = applyOutputBudget(this.config, await this.factHits(ids, () => 1), input.maxTokens ?? 0);
|
|
17604
|
+
if (input.track !== false) await this.touchOnly(kept.map((hit) => hit.ref_id));
|
|
16702
17605
|
return {
|
|
16703
17606
|
mode: "time",
|
|
16704
17607
|
total,
|
|
16705
17608
|
offset,
|
|
16706
17609
|
limit,
|
|
17610
|
+
remaining: Math.max(0, total - (offset + kept.length)),
|
|
16707
17611
|
truncated: offset + kept.length < total,
|
|
16708
17612
|
hits: kept
|
|
16709
17613
|
};
|
|
@@ -16735,7 +17639,7 @@ var MemoryStore = class {
|
|
|
16735
17639
|
offset
|
|
16736
17640
|
});
|
|
16737
17641
|
const { kept } = applyOutputBudget(this.config, await this.factHits(ids, () => 1), input.maxTokens);
|
|
16738
|
-
if (input.track !== false) await this.
|
|
17642
|
+
if (input.track !== false) await this.touchOnly(kept.map((hit) => hit.ref_id));
|
|
16739
17643
|
return {
|
|
16740
17644
|
mode: "exact",
|
|
16741
17645
|
total,
|
|
@@ -16772,8 +17676,8 @@ var MemoryStore = class {
|
|
|
16772
17676
|
* malformed window is the caller's error exactly as it is in the exact page;
|
|
16773
17677
|
* - the page is FACT-level (`total`/`offset`/`limit`/`truncated`), and the caller's output budget
|
|
16774
17678
|
* only shortens `content` — dropping a row would make `total` disagree with the page;
|
|
16775
|
-
* - the returned facts
|
|
16776
|
-
*
|
|
17679
|
+
* - the returned facts get usage recorded but NO reinforcement unless `track: false`: a facts
|
|
17680
|
+
* page is a list the caller browses, so only the dormancy clock moves ({@link touchOnly}).
|
|
16777
17681
|
*/
|
|
16778
17682
|
async factPage(input, rawQuery) {
|
|
16779
17683
|
const query = rawQuery.trim();
|
|
@@ -16792,7 +17696,7 @@ var MemoryStore = class {
|
|
|
16792
17696
|
});
|
|
16793
17697
|
if (page.total === 0) return null;
|
|
16794
17698
|
const facts = this.budgetFactEvidence(page.rows.map(factEvidenceOf), input.maxTokens);
|
|
16795
|
-
if (input.track !== false) await this.
|
|
17699
|
+
if (input.track !== false) await this.touchOnly(facts.map((entry) => entry.fact_id));
|
|
16796
17700
|
return {
|
|
16797
17701
|
mode: "facts",
|
|
16798
17702
|
total: page.total,
|
|
@@ -16889,8 +17793,8 @@ var MemoryStore = class {
|
|
|
16889
17793
|
text: String(row["content"] ?? ""),
|
|
16890
17794
|
memory_kind: mapLegacyKind(String(row["kind"] ?? "note")),
|
|
16891
17795
|
...title === void 0 ? {} : { title },
|
|
16892
|
-
created_at: row["created_at"]
|
|
16893
|
-
updated_at:
|
|
17796
|
+
created_at: modelInstant(row["created_at"]) ?? "",
|
|
17797
|
+
updated_at: modelInstant(row["updated_at"])
|
|
16894
17798
|
});
|
|
16895
17799
|
}
|
|
16896
17800
|
return evidence;
|
|
@@ -17118,10 +18022,9 @@ var MemoryStore = class {
|
|
|
17118
18022
|
score: scoreOf(id),
|
|
17119
18023
|
source_ref: factSourceRef(id),
|
|
17120
18024
|
entities: bags.get(id) ?? [],
|
|
17121
|
-
created_at: time?.created_at ?? "",
|
|
17122
|
-
updated_at: time?.updated_at
|
|
18025
|
+
created_at: modelInstant(time?.created_at) ?? "",
|
|
18026
|
+
updated_at: modelInstant(time?.updated_at),
|
|
17123
18027
|
...time?.valid_from === null || time?.valid_from === void 0 ? {} : { valid_from: time.valid_from },
|
|
17124
|
-
...time?.valid_to === null || time?.valid_to === void 0 ? {} : { valid_to: time.valid_to },
|
|
17125
18028
|
...time?.title === null || time?.title === void 0 ? {} : { title: time.title }
|
|
17126
18029
|
};
|
|
17127
18030
|
});
|
|
@@ -17186,6 +18089,19 @@ var MemoryStore = class {
|
|
|
17186
18089
|
} catch {}
|
|
17187
18090
|
}
|
|
17188
18091
|
/**
|
|
18092
|
+
* Record the USAGE of these facts and nothing else — no trust bonus, no quota, no
|
|
18093
|
+
* `last_reinforced_at`.
|
|
18094
|
+
*
|
|
18095
|
+
* This is the paginated passes' half of {@link reinforce}: `timePage` / `exactContentPage` /
|
|
18096
|
+
* `factPage` return a list the caller browses rather than a recall it acted on, so "someone
|
|
18097
|
+
* looked at this row" still moves `retrieval_count` / `last_retrieved_at` (observation and
|
|
18098
|
+
* export only — no lifecycle step reads that clock) while `trust_score` / `bonus_count` stay
|
|
18099
|
+
* put.
|
|
18100
|
+
*/
|
|
18101
|
+
async touchOnly(ids) {
|
|
18102
|
+
if (ids.length) await this.touchUsage(ids);
|
|
18103
|
+
}
|
|
18104
|
+
/**
|
|
17189
18105
|
* Semantic leg. `queryVector` short-circuits the encode when the caller already
|
|
17190
18106
|
* encoded this exact query (the cross-store router does, for both stores at once);
|
|
17191
18107
|
* `onVector` publishes the vector this leg actually used, so the relaxed retry pass can reuse it
|
|
@@ -17294,25 +18210,22 @@ var MemoryStore = class {
|
|
|
17294
18210
|
/**
|
|
17295
18211
|
* Insert (or resolve to) a fact row.
|
|
17296
18212
|
*
|
|
17297
|
-
* `kind`
|
|
17298
|
-
*
|
|
17299
|
-
*
|
|
17300
|
-
*
|
|
17301
|
-
*
|
|
17302
|
-
* omitted one keeps what the row already has, and a brand-new row falls back to the
|
|
17303
|
-
* defaults (`general` / `0`).
|
|
18213
|
+
* `kind` is `undefined` when the CALLER specified nothing — the distinction is load-bearing.
|
|
18214
|
+
* A duplicate-content hit must not clobber the existing row's value with the insert default:
|
|
18215
|
+
* re-adding a fact without a kind used to reset it to `general`. The rule for all three
|
|
18216
|
+
* branches is therefore: an explicitly provided value lands, an omitted one keeps what the row
|
|
18217
|
+
* already has, and a brand-new row falls back to the default (`note`).
|
|
17304
18218
|
*/
|
|
17305
|
-
async persistFact(content, kind,
|
|
18219
|
+
async persistFact(content, kind, entities, triples, opts, explicitEdges = void 0) {
|
|
17306
18220
|
const hrr = hrrToBytes(encodeHrrEntityVector(entities.map((e) => e.name)));
|
|
17307
18221
|
const trust = this.config.trust;
|
|
17308
18222
|
const clock = await this.lifecycle.clock();
|
|
17309
18223
|
const nowIso = formatUtcTs(Date.now());
|
|
17310
18224
|
const replaced = opts?.supersedesId === void 0 ? void 0 : await this.getRow(opts.supersedesId);
|
|
17311
18225
|
const superseded = trust.inherit_trust_on_update ? replaced : void 0;
|
|
17312
|
-
const startTrust = superseded === void 0 ?
|
|
18226
|
+
const startTrust = superseded === void 0 ? BASE_TRUST : effectiveTrust(asTrustRow(superseded), clock, trust);
|
|
17313
18227
|
const startPinned = superseded !== void 0 && isPinned(asTrustRow(superseded)) ? 1 : 0;
|
|
17314
18228
|
const newKind = kind ?? (replaced === void 0 ? "note" : mapLegacyKind(replaced.kind));
|
|
17315
|
-
const newTtl = ttlDays ?? (replaced === void 0 ? 0 : Number(replaced.ttl_days ?? 0));
|
|
17316
18229
|
/**
|
|
17317
18230
|
* The title this revision would carry (JOURNAL_TITLE_SEARCH §2.2/§11.2, v17's correction).
|
|
17318
18231
|
*
|
|
@@ -17344,13 +18257,11 @@ var MemoryStore = class {
|
|
|
17344
18257
|
}
|
|
17345
18258
|
const createdAt = replaced === void 0 || replaced.created_at === null ? nowIso : String(replaced.created_at);
|
|
17346
18259
|
const validFrom = opts?.eventDate === void 0 ? replaced === void 0 ? null : nullableEventTime(replaced.valid_from, replaced.valid_from_ms) : normalizeEventTime(opts.eventDate, "event_date");
|
|
17347
|
-
const validTo = opts?.validUntil === void 0 ? replaced === void 0 ? null : nullableEventTime(replaced.valid_to, replaced.valid_to_ms) : normalizeEventTime(opts.validUntil, "valid_until");
|
|
17348
18260
|
const { supersededId, ...result } = await this.lifecycle.transaction(async () => {
|
|
17349
18261
|
const cur = await this.facts.insertRevision({
|
|
17350
18262
|
content,
|
|
17351
18263
|
kind: newKind,
|
|
17352
18264
|
title: desiredTitle,
|
|
17353
|
-
ttlDays: newTtl,
|
|
17354
18265
|
supersedesId: opts?.supersedesId ?? null,
|
|
17355
18266
|
hrr,
|
|
17356
18267
|
trust: clamp01(startTrust),
|
|
@@ -17360,9 +18271,7 @@ var MemoryStore = class {
|
|
|
17360
18271
|
windowAt: nowIso,
|
|
17361
18272
|
createdAt,
|
|
17362
18273
|
validFrom: validFrom?.text ?? null,
|
|
17363
|
-
|
|
17364
|
-
validFromMs: validFrom?.ms ?? null,
|
|
17365
|
-
validToMs: validTo?.ms ?? null
|
|
18274
|
+
validFromMs: validFrom?.ms ?? null
|
|
17366
18275
|
});
|
|
17367
18276
|
let fact_id;
|
|
17368
18277
|
let is_new;
|
|
@@ -17372,18 +18281,16 @@ var MemoryStore = class {
|
|
|
17372
18281
|
fact_id = Number(row.fact_id);
|
|
17373
18282
|
is_new = false;
|
|
17374
18283
|
const rowKind = mapLegacyKind(row.kind);
|
|
17375
|
-
const rowTtl = Number(row.ttl_days ?? 0);
|
|
17376
18284
|
const keepKind = kind ?? rowKind;
|
|
17377
|
-
const keepTtl = ttlDays ?? rowTtl;
|
|
17378
18285
|
const rowTitle = storedTitle(row.title);
|
|
17379
18286
|
const selfRetitle = retitle && opts?.supersedesId !== void 0 && Number(opts.supersedesId) === fact_id;
|
|
17380
18287
|
if (opts?.title !== void 0 && opts.title !== null && rowTitle !== null && opts.title !== rowTitle && !selfRetitle) throw new Error(TITLE_CONFLICT);
|
|
17381
18288
|
const keepTitle = selfRetitle ? desiredTitle : rowTitle ?? desiredTitle;
|
|
17382
18289
|
if (opts?.requireJournalTitle === true && keepKind === "journal" && keepTitle === null) throw new Error(TITLE_REQUIRED);
|
|
17383
|
-
await this.facts.applyValidity(fact_id, opts?.eventDate === void 0 ? void 0 : validFrom ?? void 0
|
|
18290
|
+
await this.facts.applyValidity(fact_id, opts?.eventDate === void 0 ? void 0 : validFrom ?? void 0);
|
|
17384
18291
|
const settled = effectiveTrust(asTrustRow(row), clock, trust);
|
|
17385
|
-
const revived = await this.facts.revive(fact_id, keepKind,
|
|
17386
|
-
if (!revived &&
|
|
18292
|
+
const revived = await this.facts.revive(fact_id, keepKind, Math.max(settled, BASE_TRUST), clock) > 0;
|
|
18293
|
+
if (!revived && keepKind !== rowKind) await this.facts.applyValues(fact_id, keepKind);
|
|
17387
18294
|
if (keepTitle !== null && keepTitle !== rowTitle) {
|
|
17388
18295
|
await this.facts.setTitle(fact_id, keepTitle);
|
|
17389
18296
|
if (selfRetitle) {
|
|
@@ -17471,10 +18378,7 @@ var MemoryStore = class {
|
|
|
17471
18378
|
if (!opts?.supersedesId || opts.supersedesId === fact_id) return null;
|
|
17472
18379
|
await this.facts.linkSupersede(fact_id, opts.supersedesId);
|
|
17473
18380
|
if (opts.archiveOldIfActive) {
|
|
17474
|
-
if (await this.facts.archiveSuperseded(opts.supersedesId, await this.lifecycle.clock()) > 0)
|
|
17475
|
-
await this.contradictions.resolveForFact(opts.supersedesId);
|
|
17476
|
-
await this.facts.markValidTo(opts.supersedesId, formatUtcTs(Date.now()), Date.now());
|
|
17477
|
-
}
|
|
18381
|
+
if (await this.facts.archiveSuperseded(opts.supersedesId, await this.lifecycle.clock()) > 0) await this.contradictions.resolveForFact(opts.supersedesId);
|
|
17478
18382
|
}
|
|
17479
18383
|
await this.triples.deleteForFact(opts.supersedesId);
|
|
17480
18384
|
await this.relations.deleteEdgesForFact(opts.supersedesId);
|
|
@@ -17631,8 +18535,8 @@ var MemoryStore = class {
|
|
|
17631
18535
|
pinned: isPinned(row),
|
|
17632
18536
|
remaining_days: remainingDays(row, clock, this.config.trust),
|
|
17633
18537
|
helpful_count: Number(r.helpful_count),
|
|
17634
|
-
created_at:
|
|
17635
|
-
archived_at:
|
|
18538
|
+
created_at: modelInstant(r.created_at) ?? "",
|
|
18539
|
+
archived_at: modelInstant(r.archived_at),
|
|
17636
18540
|
archive_reason: r.archive_reason ? String(r.archive_reason) : null
|
|
17637
18541
|
};
|
|
17638
18542
|
}
|
|
@@ -17645,14 +18549,13 @@ var MemoryStore = class {
|
|
|
17645
18549
|
entities,
|
|
17646
18550
|
triples,
|
|
17647
18551
|
settle_clock: Number(r.settle_clock),
|
|
17648
|
-
pinned_at:
|
|
17649
|
-
last_reinforced_at:
|
|
17650
|
-
updated_at:
|
|
18552
|
+
pinned_at: modelInstant(r.pinned_at),
|
|
18553
|
+
last_reinforced_at: modelInstant(r.last_reinforced_at),
|
|
18554
|
+
updated_at: modelInstant(r.updated_at),
|
|
17651
18555
|
bonus_count: Number(r.bonus_count ?? 0),
|
|
17652
|
-
bonus_window_at:
|
|
18556
|
+
bonus_window_at: modelInstant(r.bonus_window_at),
|
|
17653
18557
|
sources: await this.sources.listForFact(factId),
|
|
17654
18558
|
valid_from: nullableString(r.valid_from),
|
|
17655
|
-
valid_to: nullableString(r.valid_to),
|
|
17656
18559
|
superseded_by: await this.facts.supersededBy(factId),
|
|
17657
18560
|
assert_count: Number(r.assert_count ?? 1),
|
|
17658
18561
|
relations: await this.relations.explicitRelationsForFact(factId)
|
|
@@ -17758,10 +18661,11 @@ function nullableEventTime(text, ms) {
|
|
|
17758
18661
|
* inserts SQL rows.
|
|
17759
18662
|
*
|
|
17760
18663
|
* TWO CONSUMERS, TWO SEMANTICS. `import`/`merge` remain a CONTENT-IDENTITY UNION and consume only
|
|
17761
|
-
* a subset: kind,
|
|
17762
|
-
*
|
|
17763
|
-
*
|
|
17764
|
-
*
|
|
18664
|
+
* a subset: kind, embedded source ref and valid_from → event_date. They do NOT restore
|
|
18665
|
+
* created_at/updated_at/trust_score/assert_count/helpful_count/pinned, archived status, the
|
|
18666
|
+
* supersedes chain, or the retired `ttl_days` / `valid_to` values (v2: the columns stay in the
|
|
18667
|
+
* document for format stability, but nothing reads them on the way in) — `metadata_dropped` counts
|
|
18668
|
+
* each value they knowingly drop instead of pretending. `restore` is the REBUILD (see `store/facts_restore.ts`):
|
|
17765
18669
|
* only into an EMPTY facts table, keeping fact_id, status, timestamps, trust, pin, counters, every
|
|
17766
18670
|
* provenance row and the supersedes chain, then re-deriving entities. The union path never revives
|
|
17767
18671
|
* an archived row; restore is the only path that brings archived rows back. The vector, entity,
|
|
@@ -17857,7 +18761,6 @@ const FACT_COLUMNS = [
|
|
|
17857
18761
|
"helpful_count",
|
|
17858
18762
|
"last_reinforced_at",
|
|
17859
18763
|
"valid_from",
|
|
17860
|
-
"valid_to",
|
|
17861
18764
|
"supersedes_id",
|
|
17862
18765
|
"archived_at",
|
|
17863
18766
|
"archive_reason",
|
|
@@ -18007,7 +18910,7 @@ function readFactsDocument(dbPath, options) {
|
|
|
18007
18910
|
last_reinforced_at: textOrNull(cell(row, "last_reinforced_at")),
|
|
18008
18911
|
sources: sources.get(sourceId) ?? [],
|
|
18009
18912
|
valid_from: textOrNull(cell(row, "valid_from")),
|
|
18010
|
-
valid_to:
|
|
18913
|
+
valid_to: null,
|
|
18011
18914
|
supersedes_source_id: supersedes === null || supersedes === void 0 ? null : numberOf(supersedes, 0),
|
|
18012
18915
|
archive: status === "archived" ? {
|
|
18013
18916
|
at: textOrNull(cell(row, "archived_at")),
|
|
@@ -18203,7 +19106,9 @@ function emptyMetadataDropped() {
|
|
|
18203
19106
|
assert_count: 0,
|
|
18204
19107
|
helpful_count: 0,
|
|
18205
19108
|
pinned: 0,
|
|
18206
|
-
supersedes: 0
|
|
19109
|
+
supersedes: 0,
|
|
19110
|
+
ttl_days: 0,
|
|
19111
|
+
valid_to: 0
|
|
18207
19112
|
};
|
|
18208
19113
|
}
|
|
18209
19114
|
/** What a Phase-2 restore WOULD have carried but v1 does not. */
|
|
@@ -18214,7 +19119,9 @@ function metadataDroppedOf(row) {
|
|
|
18214
19119
|
assert_count: numberOf(cell(row, "assert_count"), 1) > 1 ? 1 : 0,
|
|
18215
19120
|
helpful_count: numberOf(cell(row, "helpful_count"), 0) > 0 ? 1 : 0,
|
|
18216
19121
|
pinned: boolOf(cell(row, "pinned")) ? 1 : 0,
|
|
18217
|
-
supersedes: cell(row, "supersedes_source_id") === null || cell(row, "supersedes_source_id") === void 0 ? 0 : 1
|
|
19122
|
+
supersedes: cell(row, "supersedes_source_id") === null || cell(row, "supersedes_source_id") === void 0 ? 0 : 1,
|
|
19123
|
+
ttl_days: numberOf(cell(row, "ttl_days"), 0) > 0 ? 1 : 0,
|
|
19124
|
+
valid_to: cell(row, "valid_to") === null || cell(row, "valid_to") === void 0 ? 0 : 1
|
|
18218
19125
|
};
|
|
18219
19126
|
}
|
|
18220
19127
|
function accumulateMetadataDropped(total, one) {
|
|
@@ -18224,6 +19131,8 @@ function accumulateMetadataDropped(total, one) {
|
|
|
18224
19131
|
total.helpful_count += one.helpful_count;
|
|
18225
19132
|
total.pinned += one.pinned;
|
|
18226
19133
|
total.supersedes += one.supersedes;
|
|
19134
|
+
total.ttl_days += one.ttl_days;
|
|
19135
|
+
total.valid_to += one.valid_to;
|
|
18227
19136
|
}
|
|
18228
19137
|
function sourceIdOf(row) {
|
|
18229
19138
|
const value = cell(row, "source_id");
|
|
@@ -18235,12 +19144,6 @@ function optionalString(row, key, label) {
|
|
|
18235
19144
|
if (typeof value !== "string") throw new Error(`${label} 不是字符串`);
|
|
18236
19145
|
return value === "" ? void 0 : value;
|
|
18237
19146
|
}
|
|
18238
|
-
function optionalTtlDays(row) {
|
|
18239
|
-
const value = cell(row, "ttl_days");
|
|
18240
|
-
if (value === null || value === void 0) return void 0;
|
|
18241
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new Error("ttl_days 无效(需要非负整数)");
|
|
18242
|
-
return value;
|
|
18243
|
-
}
|
|
18244
19147
|
function optionalDate(row, key, label) {
|
|
18245
19148
|
const value = cell(row, key);
|
|
18246
19149
|
if (value === null || value === void 0) return void 0;
|
|
@@ -18302,19 +19205,15 @@ function addRequestFor(identity, row, fallbackSourceRef, preferEmbeddedSources)
|
|
|
18302
19205
|
const sourceRef = (preferEmbeddedSources ? embeddedSourceRef(row) : void 0) ?? fallbackSourceRef;
|
|
18303
19206
|
const kind = mapLegacyKind(optionalString(row, "kind", "kind") ?? optionalString(row, "category", "category"));
|
|
18304
19207
|
const title = optionalString(row, "title", "title");
|
|
18305
|
-
const ttlDays = optionalTtlDays(row);
|
|
18306
19208
|
const eventDate = optionalDate(row, "valid_from", "valid_from");
|
|
18307
|
-
const validUntil = optionalDate(row, "valid_to", "valid_to");
|
|
18308
19209
|
const relations = relationsOf(row, kind);
|
|
18309
19210
|
return {
|
|
18310
19211
|
action: "add",
|
|
18311
19212
|
content: identity,
|
|
18312
19213
|
kind,
|
|
18313
19214
|
...title === void 0 ? {} : { title },
|
|
18314
|
-
...ttlDays === void 0 ? {} : { ttl_days: ttlDays },
|
|
18315
19215
|
...sourceRef === void 0 ? {} : { source_ref: sourceRef },
|
|
18316
19216
|
...eventDate === void 0 ? {} : { event_date: eventDate },
|
|
18317
|
-
...validUntil === void 0 ? {} : { valid_until: validUntil },
|
|
18318
19217
|
...relations === void 0 ? {} : { relations }
|
|
18319
19218
|
};
|
|
18320
19219
|
}
|
|
@@ -18709,13 +19608,12 @@ function walHealth(dbPath) {
|
|
|
18709
19608
|
* P-08 / P-13 / JOURNAL_TITLE_SEARCH: the `mem_remember` extras, lifted off the request and onto the
|
|
18710
19609
|
* store's write options. Only the keys actually present are carried, so an omitted argument stays
|
|
18711
19610
|
* "omitted" all the way down (the store's "unset keeps what the row has" rule — an update inherits
|
|
18712
|
-
* an omitted title
|
|
19611
|
+
* an omitted title or event time — depends on that distinction).
|
|
18713
19612
|
*/
|
|
18714
19613
|
function writeOptions(req, rules) {
|
|
18715
19614
|
return {
|
|
18716
19615
|
...req.source_ref === void 0 ? {} : { sourceRef: req.source_ref },
|
|
18717
19616
|
...req.event_date === void 0 ? {} : { eventDate: req.event_date },
|
|
18718
|
-
...req.valid_until === void 0 ? {} : { validUntil: req.valid_until },
|
|
18719
19617
|
...req.title === void 0 ? {} : { title: req.title },
|
|
18720
19618
|
...req.retitle === true ? { retitle: true } : {},
|
|
18721
19619
|
...req.relations === void 0 ? {} : { relations: req.relations },
|
|
@@ -18839,11 +19737,9 @@ async function buildRuntime(opts) {
|
|
|
18839
19737
|
},
|
|
18840
19738
|
async remember(req, rules) {
|
|
18841
19739
|
switch (req.action) {
|
|
18842
|
-
case "add": return this.memory.add(req.content, req.kind,
|
|
18843
|
-
case "update": return this.memory.update(req.fact_id, req.content, req.kind,
|
|
19740
|
+
case "add": return this.memory.add(req.content, req.kind, writeOptions(req, rules));
|
|
19741
|
+
case "update": return this.memory.update(req.fact_id, req.content, req.kind, writeOptions(req, rules));
|
|
18844
19742
|
case "remove": return this.memory.remove(req.fact_id, req.reason);
|
|
18845
|
-
case "helpful": return this.memory.helpful(req.fact_id);
|
|
18846
|
-
case "unhelpful": return this.memory.unhelpful(req.fact_id);
|
|
18847
19743
|
}
|
|
18848
19744
|
},
|
|
18849
19745
|
async recall(req) {
|
|
@@ -18873,14 +19769,19 @@ async function buildRuntime(opts) {
|
|
|
18873
19769
|
},
|
|
18874
19770
|
async admin(req) {
|
|
18875
19771
|
switch (req.action) {
|
|
18876
|
-
case "stats":
|
|
18877
|
-
|
|
18878
|
-
|
|
18879
|
-
|
|
18880
|
-
|
|
18881
|
-
|
|
18882
|
-
|
|
18883
|
-
|
|
19772
|
+
case "stats":
|
|
19773
|
+
const retrieval = retrievalHealthSummary();
|
|
19774
|
+
return {
|
|
19775
|
+
...await this.memory.countByStatus(),
|
|
19776
|
+
retrieval: {
|
|
19777
|
+
...retrieval,
|
|
19778
|
+
updated_at: formatOffsetIso(retrieval.updated_at)
|
|
19779
|
+
},
|
|
19780
|
+
vectors: await this.memory.vectorSpaceHealth(),
|
|
19781
|
+
wal: walHealth(memDb),
|
|
19782
|
+
sources: await this.memory.sourceCoverage(),
|
|
19783
|
+
validity: await this.memory.validityCoverage()
|
|
19784
|
+
};
|
|
18884
19785
|
case "list": return await this.memory.list(req.kind, req.status ?? "active", req.limit ?? 50, req.offset ?? 0, req.source, req.time);
|
|
18885
19786
|
case "detail": return await this.memory.get(req.fact_id) ?? { error: `fact_id=${req.fact_id} not found` };
|
|
18886
19787
|
case "archive": return await this.memory.archive(req.fact_id, req.reason);
|
|
@@ -19031,11 +19932,9 @@ const descriptors = [
|
|
|
19031
19932
|
content: optionalText.optional(),
|
|
19032
19933
|
fact_id: count.optional(),
|
|
19033
19934
|
kind: optionalText.optional(),
|
|
19034
|
-
ttl_days: count.optional(),
|
|
19035
19935
|
reason: optionalText.optional(),
|
|
19036
19936
|
source_ref: optionalText.optional(),
|
|
19037
19937
|
event_date: optionalText.optional(),
|
|
19038
|
-
valid_until: optionalText.optional(),
|
|
19039
19938
|
title: optionalText.optional(),
|
|
19040
19939
|
retitle: bool.optional(),
|
|
19041
19940
|
relations: z.array(z.object({
|
|
@@ -19254,7 +20153,7 @@ const VERSION_PACKAGES = [
|
|
|
19254
20153
|
/** This plugin's npm package id, which names the probe and verification keys. */
|
|
19255
20154
|
const PACKAGE_ID = String(hostContribution.package ?? "@avantf/dsh-mem");
|
|
19256
20155
|
/** Best-effort one-line reason for a caught value. */
|
|
19257
|
-
function reasonOf(error) {
|
|
20156
|
+
function reasonOf$2(error) {
|
|
19258
20157
|
return error instanceof Error ? error.message : String(error);
|
|
19259
20158
|
}
|
|
19260
20159
|
/** Emit a warning without ever letting the logger turn it into a crash. */
|
|
@@ -19294,7 +20193,7 @@ function runtimeFromCompat(module) {
|
|
|
19294
20193
|
}
|
|
19295
20194
|
/** The "the check itself failed" outcome: keep loading, say why. */
|
|
19296
20195
|
function cannotDetermine(error, log, prefix) {
|
|
19297
|
-
const message = `${prefix} WARNING — the compatibility check itself failed (${reasonOf(error)}); loading anyway`;
|
|
20196
|
+
const message = `${prefix} WARNING — the compatibility check itself failed (${reasonOf$2(error)}); loading anyway`;
|
|
19298
20197
|
warn(log, message);
|
|
19299
20198
|
return { verdict: {
|
|
19300
20199
|
load: true,
|
|
@@ -19378,6 +20277,290 @@ function verifyRegisteredFaces(input) {
|
|
|
19378
20277
|
toolNames: input.toolNames
|
|
19379
20278
|
});
|
|
19380
20279
|
}
|
|
20280
|
+
//#endregion
|
|
20281
|
+
//#region lib/types/deps_audit.js
|
|
20282
|
+
/**
|
|
20283
|
+
* The STARTUP DEPENDENCY AUDIT: one cached, synchronous answer per CONFIGURED entry to "can this
|
|
20284
|
+
* process actually load that piece right now?", recorded at the end of the mount and refreshed when a
|
|
20285
|
+
* self-heal item settles.
|
|
20286
|
+
*
|
|
20287
|
+
* WHY IT EXISTS. A session notice is the last step of the fallback chain
|
|
20288
|
+
* `npm optional → self-heal → notice`, and the decision "is this really missing?" cannot be made on
|
|
20289
|
+
* the prompt path: `pre-step` runs on every user message, and resolving a package there would be a
|
|
20290
|
+
* filesystem hit (and, for a native addon, a `dlopen`) per step. So the expensive half happens ONCE,
|
|
20291
|
+
* off the mount path, and `pre-step` only consumes the registry, whose predicates read this snapshot.
|
|
20292
|
+
*
|
|
20293
|
+
* CONFIG-DRIVEN, NOT PACKAGE-DRIVEN. This module knows two probe KINDS and nothing about which
|
|
20294
|
+
* packages exist:
|
|
20295
|
+
*
|
|
20296
|
+
* - {@link moduleProbe} — resolve AND load one specifier (a real `require`/`import`, so a native
|
|
20297
|
+
* addon's binding is loaded rather than merely found), trying the ready shared roots first and
|
|
20298
|
+
* this tree last;
|
|
20299
|
+
* - {@link itemProbe} — the state of one runtime-deps item, through a caller-supplied lookup (the
|
|
20300
|
+
* provisioner's own `resolve`, with the "declared but not settled" gate on top).
|
|
20301
|
+
*
|
|
20302
|
+
* Which entries exist, how loud each one is, and what it says come from the plugin's SHIPPED
|
|
20303
|
+
* `res/runtime-deps.yaml` (`RuntimeDepsResourceSchema` in the contract). No package name, sentence or
|
|
20304
|
+
* severity is written here.
|
|
20305
|
+
*
|
|
20306
|
+
* WHAT IT IS NOT:
|
|
20307
|
+
*
|
|
20308
|
+
* - **Not a retry loop.** `run()` at startup, `run()` again when a self-heal item settles. A package
|
|
20309
|
+
* installed by hand afterwards is picked up on the next restart, not polled for.
|
|
20310
|
+
* - **Not throwing.** It runs beside a live host: a probe that fails is a recorded state plus a log
|
|
20311
|
+
* line, never an exception out of the mount.
|
|
20312
|
+
* - **Not a second source of truth about an item.** `itemProbe` ASKS the caller; the states here are
|
|
20313
|
+
* only a cache of that answer, refreshed on every settlement.
|
|
20314
|
+
*/
|
|
20315
|
+
/** The states this build understands (a JS caller can hand over anything). */
|
|
20316
|
+
const STATES = [
|
|
20317
|
+
"available",
|
|
20318
|
+
"failed",
|
|
20319
|
+
"missing",
|
|
20320
|
+
"skipped",
|
|
20321
|
+
"pending",
|
|
20322
|
+
"unknown"
|
|
20323
|
+
];
|
|
20324
|
+
/** Reason text for a thrown value, without trusting it. */
|
|
20325
|
+
function reasonOf$1(error) {
|
|
20326
|
+
try {
|
|
20327
|
+
if (error instanceof Error && typeof error.message === "string" && error.message !== "") return error.message;
|
|
20328
|
+
return String(error);
|
|
20329
|
+
} catch {
|
|
20330
|
+
return "unknown";
|
|
20331
|
+
}
|
|
20332
|
+
}
|
|
20333
|
+
/**
|
|
20334
|
+
* Build the audit.
|
|
20335
|
+
*
|
|
20336
|
+
* Every configured entry starts `unknown`; nothing is assumed about an entry that has not been
|
|
20337
|
+
* checked. The caller runs it once (fire-and-forget) and again for the entries a settlement could
|
|
20338
|
+
* change — that ordering is what makes a session line appear only after there is an observation.
|
|
20339
|
+
*/
|
|
20340
|
+
function createDependencyAudit(options) {
|
|
20341
|
+
const states = /* @__PURE__ */ new Map();
|
|
20342
|
+
const pending = /* @__PURE__ */ new Map();
|
|
20343
|
+
const probes = /* @__PURE__ */ new Map();
|
|
20344
|
+
for (const probe of options.probes) {
|
|
20345
|
+
probes.set(probe.id, probe);
|
|
20346
|
+
states.set(probe.id, "unknown");
|
|
20347
|
+
}
|
|
20348
|
+
const warn = (message) => {
|
|
20349
|
+
try {
|
|
20350
|
+
options.logger?.warn(message);
|
|
20351
|
+
} catch {}
|
|
20352
|
+
};
|
|
20353
|
+
const info = (message) => {
|
|
20354
|
+
try {
|
|
20355
|
+
options.logger?.info(message);
|
|
20356
|
+
} catch {}
|
|
20357
|
+
};
|
|
20358
|
+
const runId = (id) => {
|
|
20359
|
+
const running = pending.get(id);
|
|
20360
|
+
if (running !== void 0) return running;
|
|
20361
|
+
const probe = probes.get(id);
|
|
20362
|
+
if (probe === void 0) return Promise.resolve();
|
|
20363
|
+
const task = (async () => {
|
|
20364
|
+
let next;
|
|
20365
|
+
try {
|
|
20366
|
+
next = await probe.check();
|
|
20367
|
+
} catch (error) {
|
|
20368
|
+
warn(`deps audit: probing ${id} failed (${reasonOf$1(error)}); its state stays unknown`);
|
|
20369
|
+
next = "unknown";
|
|
20370
|
+
}
|
|
20371
|
+
if (!STATES.includes(next)) next = "unknown";
|
|
20372
|
+
const before = states.get(id);
|
|
20373
|
+
states.set(id, next);
|
|
20374
|
+
if (before !== next) info(`deps audit: ${id} is ${next}`);
|
|
20375
|
+
})().finally(() => {
|
|
20376
|
+
pending.delete(id);
|
|
20377
|
+
});
|
|
20378
|
+
pending.set(id, task);
|
|
20379
|
+
return task;
|
|
20380
|
+
};
|
|
20381
|
+
return {
|
|
20382
|
+
state(id) {
|
|
20383
|
+
return states.get(id) ?? "unknown";
|
|
20384
|
+
},
|
|
20385
|
+
vectors() {
|
|
20386
|
+
try {
|
|
20387
|
+
return options.vectors();
|
|
20388
|
+
} catch {
|
|
20389
|
+
return 0;
|
|
20390
|
+
}
|
|
20391
|
+
},
|
|
20392
|
+
async run() {
|
|
20393
|
+
await Promise.all([...probes.keys()].map((id) => runId(id)));
|
|
20394
|
+
},
|
|
20395
|
+
runId
|
|
20396
|
+
};
|
|
20397
|
+
}
|
|
20398
|
+
/**
|
|
20399
|
+
* Resolve and LOAD one specifier, from the first root that can serve it and then from this tree.
|
|
20400
|
+
*
|
|
20401
|
+
* The `createRequire(<root>/package.json)` anchor is the same rule the semantic adapter uses: a
|
|
20402
|
+
* shared runtime-deps root's `package.json` is what makes `resolve` look in THAT root's
|
|
20403
|
+
* `node_modules`, so the audit observes the copy the runtime would load. The import is by FILE URL
|
|
20404
|
+
* because the resolved entry is inside the root, not a bare specifier this module could resolve.
|
|
20405
|
+
*
|
|
20406
|
+
* The three answers are deliberately distinct, because they lead to different repairs:
|
|
20407
|
+
*
|
|
20408
|
+
* - `available` — it loaded;
|
|
20409
|
+
* - `failed` — something RESOLVED but loading it threw (the node-gyp build that exists and does not
|
|
20410
|
+
* work: the case a bare `resolve()` check would report as healthy);
|
|
20411
|
+
* - `missing` — nothing resolved anywhere.
|
|
20412
|
+
*
|
|
20413
|
+
* A root that cannot serve the specifier is not an error: the next candidate is tried, which is what
|
|
20414
|
+
* makes a garbage-collected store no worse than no self-heal. Never throws.
|
|
20415
|
+
*/
|
|
20416
|
+
async function moduleProbe(id, specifier, roots) {
|
|
20417
|
+
const candidates = [];
|
|
20418
|
+
for (const root of roots()) {
|
|
20419
|
+
if (root === "") continue;
|
|
20420
|
+
try {
|
|
20421
|
+
candidates.push(createRequire(join(root, "package.json")).resolve(specifier));
|
|
20422
|
+
} catch {}
|
|
20423
|
+
}
|
|
20424
|
+
try {
|
|
20425
|
+
candidates.push(createRequire(import.meta.url).resolve(specifier));
|
|
20426
|
+
} catch {}
|
|
20427
|
+
if (candidates.length === 0) return "missing";
|
|
20428
|
+
let resolvedButBroken = false;
|
|
20429
|
+
for (const entry of candidates) try {
|
|
20430
|
+
await import(pathToFileURL(entry).href);
|
|
20431
|
+
return "available";
|
|
20432
|
+
} catch {
|
|
20433
|
+
resolvedButBroken = true;
|
|
20434
|
+
}
|
|
20435
|
+
return resolvedButBroken ? "failed" : "missing";
|
|
20436
|
+
}
|
|
20437
|
+
/**
|
|
20438
|
+
* Observe one runtime-deps ITEM's state through the caller's own lookup.
|
|
20439
|
+
*
|
|
20440
|
+
* The caller owns the mapping (its provisioner's `resolve` plus the "declared but not settled" gate),
|
|
20441
|
+
* so this probe adds nothing but a defensive boundary: a throwing lookup is `unknown`, not a failure
|
|
20442
|
+
* claim. `pending` is passed through because it is a MEANINGFUL state — an install in flight — and the
|
|
20443
|
+
* notice predicate treats it as "not reportable yet".
|
|
20444
|
+
*/
|
|
20445
|
+
function itemProbe(id, itemId, stateOf) {
|
|
20446
|
+
return {
|
|
20447
|
+
id,
|
|
20448
|
+
check: async () => {
|
|
20449
|
+
try {
|
|
20450
|
+
return stateOf(itemId);
|
|
20451
|
+
} catch {
|
|
20452
|
+
return "unknown";
|
|
20453
|
+
}
|
|
20454
|
+
}
|
|
20455
|
+
};
|
|
20456
|
+
}
|
|
20457
|
+
//#endregion
|
|
20458
|
+
//#region lib/types/runtime_deps_resource.js
|
|
20459
|
+
/**
|
|
20460
|
+
* The plugin's OWN dependency declaration: `res/runtime-deps.yaml`, read from the INSTALL directory.
|
|
20461
|
+
*
|
|
20462
|
+
* WHY A SHIPPED RESOURCE. The package set, the sources, the attempts, the verify list and the audit
|
|
20463
|
+
* sentences describe THIS BUILD — which module the local backend imports, which native bindings this
|
|
20464
|
+
* release looks for, what a session should be told when one of them is missing. Keeping them in a
|
|
20465
|
+
* user's `configs/common.yaml` made every deployment responsible for a list only the package can know
|
|
20466
|
+
* (and made a stale copy install a module no code loads). The operator keeps the one real policy
|
|
20467
|
+
* decision — the two `enabled` switches — and the DATA travels with the code that depends on it.
|
|
20468
|
+
*
|
|
20469
|
+
* WHERE IT IS READ FROM. `<package>/res/runtime-deps.yaml`, located from `import.meta.url`:
|
|
20470
|
+
* `lib/index.js` (bundled) and `src/*.ts` (tests, dev) are both one level below the package root, so
|
|
20471
|
+
* the same relative URL resolves to the SAME file in the checkout and in an installed tarball. It is
|
|
20472
|
+
* deliberately NOT resolved through `node_modules` lookups, `~/.avantf`, or the cwd: a host may mount
|
|
20473
|
+
* the plugin from a linked checkout, a global install or a packed tarball, and all three must read
|
|
20474
|
+
* their own copy.
|
|
20475
|
+
*
|
|
20476
|
+
* FAILURE IS EXPLICIT AND NON-FATAL. A missing/unreadable/malformed file is a PACKAGING bug, not an
|
|
20477
|
+
* environment one, so the answer is a structured failure with the path and the reason — never a
|
|
20478
|
+
* throw out of the mount and never a silent "no dependencies declared". The caller logs it as an
|
|
20479
|
+
* ERROR and degrades (no self-heal item, no audit): the plugin keeps its tools, service, prompt and
|
|
20480
|
+
* Remote face, and the runtime stays on its optional-dependency resolution.
|
|
20481
|
+
*
|
|
20482
|
+
* The base never sees this file: `envinit` maps the validated object onto the provider's own
|
|
20483
|
+
* `RuntimeDepsSpec` field by field (see `runtimeDepsSpecOf`).
|
|
20484
|
+
*/
|
|
20485
|
+
/**
|
|
20486
|
+
* `res/runtime-deps.yaml` next to the package that owns this module.
|
|
20487
|
+
*
|
|
20488
|
+
* `import.meta.url` is the anchor (not `process.cwd()`, not a config path): the file ships beside the
|
|
20489
|
+
* code, and an installed plugin must read the copy its own package manager put there.
|
|
20490
|
+
*/
|
|
20491
|
+
function runtimeDepsResourceUrl() {
|
|
20492
|
+
return new URL("../res/runtime-deps.yaml", import.meta.url);
|
|
20493
|
+
}
|
|
20494
|
+
/** Reason text for a thrown value, without trusting it. */
|
|
20495
|
+
function reasonOf(error) {
|
|
20496
|
+
try {
|
|
20497
|
+
if (error instanceof Error && typeof error.message === "string" && error.message !== "") return error.message;
|
|
20498
|
+
return String(error);
|
|
20499
|
+
} catch {
|
|
20500
|
+
return "unknown";
|
|
20501
|
+
}
|
|
20502
|
+
}
|
|
20503
|
+
/**
|
|
20504
|
+
* Read and validate the shipped declaration.
|
|
20505
|
+
*
|
|
20506
|
+
* Synchronous on purpose: it is a few kilobytes next to the entry module, read once at mount, and the
|
|
20507
|
+
* caller's plan (the item specs and the audit entries) has to exist before anything is declared.
|
|
20508
|
+
*
|
|
20509
|
+
* `url` is injectable so a test can point at a scratch install directory; production always uses
|
|
20510
|
+
* {@link runtimeDepsResourceUrl}.
|
|
20511
|
+
*/
|
|
20512
|
+
function loadRuntimeDepsResource(url = runtimeDepsResourceUrl()) {
|
|
20513
|
+
let path;
|
|
20514
|
+
try {
|
|
20515
|
+
path = fileURLToPath(url);
|
|
20516
|
+
} catch (error) {
|
|
20517
|
+
return {
|
|
20518
|
+
ok: false,
|
|
20519
|
+
path: String(url),
|
|
20520
|
+
reason: `不是可读的文件 URL(${reasonOf(error)})`
|
|
20521
|
+
};
|
|
20522
|
+
}
|
|
20523
|
+
let text;
|
|
20524
|
+
try {
|
|
20525
|
+
text = readFileSync(path, "utf8");
|
|
20526
|
+
} catch (error) {
|
|
20527
|
+
return {
|
|
20528
|
+
ok: false,
|
|
20529
|
+
path,
|
|
20530
|
+
reason: `无法读取(${reasonOf(error)})`
|
|
20531
|
+
};
|
|
20532
|
+
}
|
|
20533
|
+
let raw;
|
|
20534
|
+
try {
|
|
20535
|
+
raw = parse(text);
|
|
20536
|
+
} catch (error) {
|
|
20537
|
+
return {
|
|
20538
|
+
ok: false,
|
|
20539
|
+
path,
|
|
20540
|
+
reason: `YAML 解析失败(${reasonOf(error)})`
|
|
20541
|
+
};
|
|
20542
|
+
}
|
|
20543
|
+
if (raw === null || raw === void 0) return {
|
|
20544
|
+
ok: false,
|
|
20545
|
+
path,
|
|
20546
|
+
reason: "文件是空的(没有任何声明)"
|
|
20547
|
+
};
|
|
20548
|
+
const parsed = RuntimeDepsResourceSchema.safeParse(raw);
|
|
20549
|
+
if (!parsed.success) {
|
|
20550
|
+
const issue = parsed.error.issues[0];
|
|
20551
|
+
const where = issue === void 0 ? "" : `${issue.path.join(".") === "" ? "" : `${issue.path.join(".")}: `}`;
|
|
20552
|
+
return {
|
|
20553
|
+
ok: false,
|
|
20554
|
+
path,
|
|
20555
|
+
reason: `声明不符合 RuntimeDepsResourceSchema(${where}${issue?.message ?? "invalid"})`
|
|
20556
|
+
};
|
|
20557
|
+
}
|
|
20558
|
+
return {
|
|
20559
|
+
ok: true,
|
|
20560
|
+
path,
|
|
20561
|
+
resource: parsed.data
|
|
20562
|
+
};
|
|
20563
|
+
}
|
|
19381
20564
|
const PACKAGE = "@avantf/dsh-plugin-base";
|
|
19382
20565
|
function parseVersion(input) {
|
|
19383
20566
|
const match = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(input.trim());
|
|
@@ -19761,6 +20944,25 @@ function rangeOf(value) {
|
|
|
19761
20944
|
* models elsewhere (`AVANTF_MEM_MODEL_CACHE`) — that would install a second, never-read copy.
|
|
19762
20945
|
* A model that cannot be fetched is a WARNING: the semantic path degrades to FTS+entity, it
|
|
19763
20946
|
* never refuses the mount.
|
|
20947
|
+
* 3. **`mem:semantic-deps`** (`runtime-deps`, `startup: 'background'`) — optional: repair a missing
|
|
20948
|
+
* `@huggingface/transformers` install through the base's shared runtime-dependency store. It is
|
|
20949
|
+
* OFF unless `semantic.self_heal.enabled` says otherwise (that is a network decision, not a
|
|
20950
|
+
* startup default), needs base >= 0.7.0 / interface v6 (the `createRuntimeDeps` helper), and is
|
|
20951
|
+
* declared against the SAME content-addressed root any other family plugin would reuse. The
|
|
20952
|
+
* item, the shared root handle and the operator's configured session notices all come from that
|
|
20953
|
+
* one handle. The handle directory is handed back to the caller, which points the embedding
|
|
20954
|
+
* backend at it before the semantic warm.
|
|
20955
|
+
* 4. **`mem:native-deps`** (`runtime-deps`, `startup: 'background'`) — optional: repair the NATIVE
|
|
20956
|
+
* optional dependencies (`nodejieba`, `hnswlib-node`) when `semantic.native_self_heal.enabled`
|
|
20957
|
+
* is on. A SEPARATE item on purpose: a different package set is a different content key, so the
|
|
20958
|
+
* base's store gives it its own immutable root instead of mixing two node-gyp builds into the
|
|
20959
|
+
* transformers install. The handle directory is handed back to the caller, which points the
|
|
20960
|
+
* tokenizer and the vector store's ANN binding at it.
|
|
20961
|
+
*
|
|
20962
|
+
* Sessions are told about a missing package by NOTICES, and a notice is judged on two observations
|
|
20963
|
+
* that this module neither owns alone: the item's live state (below) and the startup dependency
|
|
20964
|
+
* audit's cached answer (`deps_audit.ts`, supplied in the plan). A repair that is still installing
|
|
20965
|
+
* matches nothing, and a failed repair whose package is loadable stays silent.
|
|
19764
20966
|
*
|
|
19765
20967
|
* Two hard rules:
|
|
19766
20968
|
*
|
|
@@ -19783,6 +20985,26 @@ const PLUGIN = "mem";
|
|
|
19783
20985
|
*/
|
|
19784
20986
|
const MODEL_ITEM = "mem:model";
|
|
19785
20987
|
/**
|
|
20988
|
+
* The shared runtime-dependency item: `@huggingface/transformers` repaired through the base's
|
|
20989
|
+
* content-addressed store when `semantic.self_heal.enabled` is on. Same root as every other family
|
|
20990
|
+
* plugin's item for the same package set.
|
|
20991
|
+
*/
|
|
20992
|
+
const SEMANTIC_DEPS_ITEM = "mem:semantic-deps";
|
|
20993
|
+
/**
|
|
20994
|
+
* The NATIVE runtime-dependency item: `nodejieba` + `hnswlib-node`, repaired through the same base
|
|
20995
|
+
* mechanism when `semantic.native_self_heal.enabled` is on.
|
|
20996
|
+
*
|
|
20997
|
+
* A SEPARATE item, not a third package in {@link SEMANTIC_DEPS_ITEM}, for two reasons that both
|
|
20998
|
+
* matter:
|
|
20999
|
+
*
|
|
21000
|
+
* - the store is content-addressed, so this spec's package set (two node-gyp builds) keys a
|
|
21001
|
+
* DIFFERENT immutable root under `<home>/deps/runtime/` — these packages are never installed into
|
|
21002
|
+
* the transformers root, and vice versa;
|
|
21003
|
+
* - the two have independent failure modes and independent consequences (semantic leg vs.
|
|
21004
|
+
* entity/jaccard legs vs. ANN index), so one install failing must not report the other as broken.
|
|
21005
|
+
*/
|
|
21006
|
+
const NATIVE_DEPS_ITEM = "mem:native-deps";
|
|
21007
|
+
/**
|
|
19786
21008
|
* The default ONNX embedding repository — the one `semantic.local_model` defaults to and the one
|
|
19787
21009
|
* {@link MODEL_FILES} describes. A DIFFERENT repo is a different file set, which is why the item
|
|
19788
21010
|
* omits `spec.files` for it (see {@link managedModelSpec}).
|
|
@@ -19824,6 +21046,23 @@ const DEADLINE_MS = 15e3;
|
|
|
19824
21046
|
* rather than ship the inversion.
|
|
19825
21047
|
*/
|
|
19826
21048
|
const MODEL_REQUEST_TIMEOUT_MS = 3e5;
|
|
21049
|
+
/**
|
|
21050
|
+
* Per-attempt budget for the `mem:semantic-deps` install. A package-manager install of
|
|
21051
|
+
* transformers.js + onnxruntime-node is tens of MB; the base's 30 s network default would time out on
|
|
21052
|
+
* a slow link and report a failure the next attempt could have avoided. The item is `background`, so
|
|
21053
|
+
* this budget spends none of the startup allowance; it only bounds one attempt.
|
|
21054
|
+
*/
|
|
21055
|
+
const SEMANTIC_DEPS_ATTEMPT_TIMEOUT_MS = 6e5;
|
|
21056
|
+
/**
|
|
21057
|
+
* Per-attempt budget for the `mem:native-deps` install.
|
|
21058
|
+
*
|
|
21059
|
+
* Longer than the semantic one on purpose: `nodejieba` and `hnswlib-node` are node-gyp packages, so
|
|
21060
|
+
* the attempt includes downloading a toolchain-compatible source tree and COMPILING it — measured
|
|
21061
|
+
* minutes on a cold cache, and slower on a laptop that is also running the host. A budget that
|
|
21062
|
+
* expires mid-compile would report a failure the very next attempt (with the compiler cache warm)
|
|
21063
|
+
* would have avoided, and the item is `background`, so this spends no startup allowance.
|
|
21064
|
+
*/
|
|
21065
|
+
const NATIVE_DEPS_ATTEMPT_TIMEOUT_MS = 9e5;
|
|
19827
21066
|
/** This module's warning token, so one grep finds every environment-initialisation line. */
|
|
19828
21067
|
const OWN_PREFIX = "envinit:";
|
|
19829
21068
|
/**
|
|
@@ -19858,6 +21097,104 @@ function sameDir(a, b) {
|
|
|
19858
21097
|
return resolve(expandHome$1(a)) === resolve(expandHome$1(b));
|
|
19859
21098
|
}
|
|
19860
21099
|
/**
|
|
21100
|
+
* The `runtime-deps` spec the base's shared store should own, or `undefined` when it should not.
|
|
21101
|
+
*
|
|
21102
|
+
* Refused when:
|
|
21103
|
+
*
|
|
21104
|
+
* 1. **self-heal is off** — the shipped default. Enabling it is a network decision the operator
|
|
21105
|
+
* makes (`semantic.self_heal.enabled`), never a startup surprise.
|
|
21106
|
+
* 2. **the backend is not `local_bge`** — this item repairs the module the local ONNX adapter
|
|
21107
|
+
* loads; a third-party backend owns its own dependencies.
|
|
21108
|
+
* 3. **no package was declared** — `spec.packages` must be non-empty (the base rejects an empty
|
|
21109
|
+
* map); declaring nothing is cleaner than a guaranteed `invalid-option`.
|
|
21110
|
+
*
|
|
21111
|
+
* The config layers are copied field by field, not passed through: the schema's defaults include an
|
|
21112
|
+
* empty `cache` placeholder that has to become the family root here (`<home>/cache/npm`), because
|
|
21113
|
+
* the schema cannot know which family root the host will resolve. An explicitly configured cache is
|
|
21114
|
+
* `~`-expanded, exactly like every other path in this plugin.
|
|
21115
|
+
*/
|
|
21116
|
+
function managedSemanticDepsSpec(semantic, declaration, home) {
|
|
21117
|
+
if (!semantic.self_heal.enabled) return void 0;
|
|
21118
|
+
if (semantic.backend !== "local_bge") return void 0;
|
|
21119
|
+
if (Object.keys(declaration.packages).length === 0) return void 0;
|
|
21120
|
+
return runtimeDepsSpecOf(declaration, home);
|
|
21121
|
+
}
|
|
21122
|
+
/**
|
|
21123
|
+
* The `runtime-deps` spec for the NATIVE packages, or `undefined` when nothing should be declared.
|
|
21124
|
+
*
|
|
21125
|
+
* Refused only when `semantic.native_self_heal.enabled` is off or the package map is empty. There is
|
|
21126
|
+
* no backend condition here (unlike the semantic spec): the tokenizer and the vector store are used
|
|
21127
|
+
* by every backend, and a `vectorStore.backend: local_numpy` deployment still pays for `nodejieba`.
|
|
21128
|
+
*/
|
|
21129
|
+
function managedNativeDepsSpec(semantic, declaration, home) {
|
|
21130
|
+
if (!semantic.native_self_heal.enabled) return void 0;
|
|
21131
|
+
if (Object.keys(declaration.packages).length === 0) return void 0;
|
|
21132
|
+
return runtimeDepsSpecOf(declaration, home);
|
|
21133
|
+
}
|
|
21134
|
+
/**
|
|
21135
|
+
* The shared shape of both `runtime-deps` specs: packages, sources, attempts, verify.
|
|
21136
|
+
*
|
|
21137
|
+
* One implementation, because the two sections use ONE provider vocabulary — a second copy of the
|
|
21138
|
+
* cache/`~` rules is how a private registry ends up working in one section and not the other. The
|
|
21139
|
+
* config layers are copied field by field, not passed through: the schema's defaults include an empty
|
|
21140
|
+
* `cache` placeholder that has to become the family root here (`<home>/cache/npm`), because the
|
|
21141
|
+
* schema cannot know which family root the host will resolve.
|
|
21142
|
+
*/
|
|
21143
|
+
function runtimeDepsSpecOf(heal, home) {
|
|
21144
|
+
const packages = {};
|
|
21145
|
+
for (const [name, range] of Object.entries(heal.packages)) packages[name] = range;
|
|
21146
|
+
return {
|
|
21147
|
+
packages,
|
|
21148
|
+
sources: heal.sources.map((source) => {
|
|
21149
|
+
const registry = source.registry.trim();
|
|
21150
|
+
const configuredCache = source.cache.trim();
|
|
21151
|
+
const cache = registry === "" && configuredCache === "" ? join(home, "cache", "npm") : configuredCache === "" ? "" : expandHome$1(configuredCache);
|
|
21152
|
+
return {
|
|
21153
|
+
id: source.id,
|
|
21154
|
+
...registry === "" ? {} : { registry },
|
|
21155
|
+
...cache === "" ? {} : { cache }
|
|
21156
|
+
};
|
|
21157
|
+
}),
|
|
21158
|
+
attempts: heal.attempts.map((attempt) => ({
|
|
21159
|
+
source: attempt.source,
|
|
21160
|
+
...attempt.manager === void 0 ? {} : { manager: attempt.manager },
|
|
21161
|
+
...Object.keys(attempt.env).length === 0 ? {} : { env: { ...attempt.env } },
|
|
21162
|
+
...attempt.allowScripts === void 0 ? {} : { allowScripts: attempt.allowScripts }
|
|
21163
|
+
})),
|
|
21164
|
+
verify: [...heal.verify]
|
|
21165
|
+
};
|
|
21166
|
+
}
|
|
21167
|
+
/**
|
|
21168
|
+
* The configured dependency-audit entries, copied field by field and resolved for execution.
|
|
21169
|
+
*
|
|
21170
|
+
* Why this is not part of a self-heal spec: the spec is what the base INSTALLS (and part of the shared
|
|
21171
|
+
* root's content key, so it has to be exactly the provider's vocabulary), while an audit entry is a
|
|
21172
|
+
* PROMPT line plus the observation behind it. Two consumers, two lifetimes — editing a sentence must
|
|
21173
|
+
* not look like a new dependency set.
|
|
21174
|
+
*
|
|
21175
|
+
* The entries are returned VERBATIM (text, severity, condition) and no default is invented: the
|
|
21176
|
+
* config's default is the empty array, so a profile that never wrote one gets no injection at all.
|
|
21177
|
+
* `min_vectors` is defaulted to `0` here, and to nothing else — this section names no package, so it
|
|
21178
|
+
* also invents no package-specific threshold.
|
|
21179
|
+
*/
|
|
21180
|
+
function managedDependencyAudit(audit) {
|
|
21181
|
+
if (!Array.isArray(audit)) return [];
|
|
21182
|
+
return audit.map((entry) => ({
|
|
21183
|
+
id: entry.id,
|
|
21184
|
+
severity: entry.severity,
|
|
21185
|
+
when: entry.when,
|
|
21186
|
+
minVectors: entry.min_vectors ?? 0,
|
|
21187
|
+
body: entry.body,
|
|
21188
|
+
probe: entry.kind === "module" ? {
|
|
21189
|
+
kind: "module",
|
|
21190
|
+
specifier: entry.specifier
|
|
21191
|
+
} : {
|
|
21192
|
+
kind: "runtime-deps",
|
|
21193
|
+
item: entry.item
|
|
21194
|
+
}
|
|
21195
|
+
}));
|
|
21196
|
+
}
|
|
21197
|
+
/**
|
|
19861
21198
|
* The family root: `$AVANTF_HOME`, else `~/.avantf/env`.
|
|
19862
21199
|
*
|
|
19863
21200
|
* `AVANTF_HOME` is the same variable the engine uses for its DATA home (`memory/`),
|
|
@@ -19947,6 +21284,121 @@ function modelItem(framework, model) {
|
|
|
19947
21284
|
};
|
|
19948
21285
|
}
|
|
19949
21286
|
/**
|
|
21287
|
+
* A `runtime-deps` item with the DELIVERY policy this plugin owns (the helper supplies only the
|
|
21288
|
+
* shape): both `onMissing` axes `degrade` — a failed install may not refuse the mount — `background`
|
|
21289
|
+
* (an install may take minutes and spends no startup budget), and the caller's per-attempt budget.
|
|
21290
|
+
*
|
|
21291
|
+
* `target.root: 'deps/runtime'` is the base's canonical store, already set by the helper. Two items
|
|
21292
|
+
* with different package sets are different content keys and therefore different immutable roots
|
|
21293
|
+
* inside it, which is what keeps `mem:semantic-deps` and `mem:native-deps` from sharing an install.
|
|
21294
|
+
*/
|
|
21295
|
+
function runtimeDepsItem(item, timeoutMs) {
|
|
21296
|
+
return {
|
|
21297
|
+
...item,
|
|
21298
|
+
onMissing: {
|
|
21299
|
+
atStartup: "degrade",
|
|
21300
|
+
atUse: "degrade"
|
|
21301
|
+
},
|
|
21302
|
+
startup: "background",
|
|
21303
|
+
policy: { timeoutMs }
|
|
21304
|
+
};
|
|
21305
|
+
}
|
|
21306
|
+
/** The provisioner's five states in the audit's vocabulary. */
|
|
21307
|
+
function dependencyStateOf(state) {
|
|
21308
|
+
switch (state?.state) {
|
|
21309
|
+
case "ready": return "available";
|
|
21310
|
+
case "failed": return "failed";
|
|
21311
|
+
case "missing": return "missing";
|
|
21312
|
+
case "skipped": return "skipped";
|
|
21313
|
+
case "pending": return "pending";
|
|
21314
|
+
default: return "unknown";
|
|
21315
|
+
}
|
|
21316
|
+
}
|
|
21317
|
+
/**
|
|
21318
|
+
* Which observed audit state one configured `when` covers.
|
|
21319
|
+
*
|
|
21320
|
+
* The same word means the same thing for both probe KINDS, so one mapping serves both:
|
|
21321
|
+
* `failed` → a load that threw / an item that failed; `missing` → nothing resolved, or the item is
|
|
21322
|
+
* missing/skipped; `degraded` → any of those three. `available`, `pending` and `unknown` match
|
|
21323
|
+
* NOTHING: a background install still in flight and an unobserved entry are both "not reportable
|
|
21324
|
+
* yet", and a healthy dependency is not a finding.
|
|
21325
|
+
*/
|
|
21326
|
+
function matchesWhen(when, state) {
|
|
21327
|
+
switch (when) {
|
|
21328
|
+
case "failed": return state === "failed";
|
|
21329
|
+
case "missing": return state === "missing" || state === "skipped";
|
|
21330
|
+
case "degraded": return state === "failed" || state === "missing" || state === "skipped";
|
|
21331
|
+
}
|
|
21332
|
+
}
|
|
21333
|
+
/**
|
|
21334
|
+
* Register one session notice per configured audit entry, judged on the AUDIT's cached state.
|
|
21335
|
+
*
|
|
21336
|
+
* The predicate is a pure read of two snapshots (`state()` is a map read, `vectors()` is the live
|
|
21337
|
+
* index count), so the prompt path never probes anything — the expensive half ran once at startup and
|
|
21338
|
+
* again on each settlement (`startDependencyAudit` / the `onSettled` path). A line is delivered when:
|
|
21339
|
+
*
|
|
21340
|
+
* - the entry's recorded state matches the configured `when` (see {@link matchesWhen}), and
|
|
21341
|
+
* - the live vector index has reached `minVectors` (0 = no threshold).
|
|
21342
|
+
*
|
|
21343
|
+
* Registration itself is total: the base registry warns and ignores a duplicate/malformed id, and an
|
|
21344
|
+
* id that is already taken costs one line, never the step.
|
|
21345
|
+
*/
|
|
21346
|
+
function registerDependencyNotices(input) {
|
|
21347
|
+
for (const notice of input.notices) input.registry.register({
|
|
21348
|
+
id: notice.id,
|
|
21349
|
+
severity: notice.severity,
|
|
21350
|
+
when: notice.when,
|
|
21351
|
+
body: notice.body
|
|
21352
|
+
}, () => matchesWhen(notice.when, input.audit.state(notice.id)) && input.audit.vectors() >= notice.minVectors);
|
|
21353
|
+
}
|
|
21354
|
+
/** Build the probes an audit entry needs — one per configured KIND, no package named here. */
|
|
21355
|
+
function probesOf(notices, roots, itemState) {
|
|
21356
|
+
return notices.map((notice) => {
|
|
21357
|
+
switch (notice.probe.kind) {
|
|
21358
|
+
case "module": {
|
|
21359
|
+
const { specifier } = notice.probe;
|
|
21360
|
+
return {
|
|
21361
|
+
id: notice.id,
|
|
21362
|
+
check: () => moduleProbe(notice.id, specifier, roots)
|
|
21363
|
+
};
|
|
21364
|
+
}
|
|
21365
|
+
case "runtime-deps": {
|
|
21366
|
+
const { item } = notice.probe;
|
|
21367
|
+
return itemProbe(notice.id, item, itemState);
|
|
21368
|
+
}
|
|
21369
|
+
}
|
|
21370
|
+
});
|
|
21371
|
+
}
|
|
21372
|
+
/**
|
|
21373
|
+
* Whether the loaded base can serve the shared `runtime-deps` item AND observe it.
|
|
21374
|
+
*
|
|
21375
|
+
* Three members are required, and all three are one capability here: `RUNTIME_DEPS_KIND` /
|
|
21376
|
+
* `runtimeDepsProvider` (interface v5, base 0.6.0) install the root, and `createRuntimeDeps`
|
|
21377
|
+
* (interface v6, base 0.7.0) is what DECLARES the item and binds the notices to its state. Since the
|
|
21378
|
+
* item is now built by that helper, a base with the provider but not the helper cannot serve this
|
|
21379
|
+
* plugin at all — so the predicate answers the caller's real question ("may I declare, wait for and
|
|
21380
|
+
* translate this item?") instead of a narrower one that would let a half-wired declaration through.
|
|
21381
|
+
*
|
|
21382
|
+
* An OLDER generation does not normally reach this point — the interface gate withholds the whole
|
|
21383
|
+
* base first (`gateOrDegrade`), so the plugin takes its legacy path. This guard covers the remaining
|
|
21384
|
+
* shape: a module that reports a current generation but does not actually carry the members (a
|
|
21385
|
+
* partial/hostile module, or a packaging mistake). Enabling self-heal must then degrade to the
|
|
21386
|
+
* runtime's optional-dependency path, not throw from `declare`. It is also what lets the fake
|
|
21387
|
+
* framework in the tests exercise that degradation instead of a TypeError.
|
|
21388
|
+
*
|
|
21389
|
+
* Exported because the startup caller asks the same question: a skipped item will never settle, so
|
|
21390
|
+
* `startManagedResources` must not wait for it (see `packages/plugin/src/index.ts`). One predicate,
|
|
21391
|
+
* two readers — the loader's skip and the caller's "do not wait" can never disagree.
|
|
21392
|
+
*
|
|
21393
|
+
* `undefined` (the test seam's partial framework, or no module at all) answers `false`: nothing is
|
|
21394
|
+
* served, so nothing may be awaited.
|
|
21395
|
+
*/
|
|
21396
|
+
function supportsRuntimeDeps(framework) {
|
|
21397
|
+
if (framework === void 0) return false;
|
|
21398
|
+
const candidate = framework;
|
|
21399
|
+
return typeof candidate.RUNTIME_DEPS_KIND === "string" && typeof candidate.runtimeDepsProvider === "function" && typeof candidate.createRuntimeDeps === "function";
|
|
21400
|
+
}
|
|
21401
|
+
/**
|
|
19950
21402
|
* The plugin-specific halves of the shared loader's sentences: mem says the FRAMEWORK is skipped and
|
|
19951
21403
|
* names the legacy provisioning path among the withheld capabilities. The mechanism is the shim's
|
|
19952
21404
|
* (`interface_gate.ts`), the wording is this plugin's.
|
|
@@ -19980,6 +21432,8 @@ function loadEnvinit(options = {}) {
|
|
|
19980
21432
|
/** The body of {@link loadEnvinit}; the cache above makes it run at most once per process. */
|
|
19981
21433
|
async function loadOnce(options) {
|
|
19982
21434
|
const home = options.home ?? resolveHome();
|
|
21435
|
+
const declaration = loadRuntimeDepsResource(options.depsResourceUrl);
|
|
21436
|
+
if (!declaration.ok) options.log?.error(`${OWN_PREFIX} ERROR — ${declaration.path} is unusable (${declaration.reason}); self-heal and the startup dependency audit are SKIPPED for this run (the plugin mounts, and every optional dependency keeps its own fallback)`);
|
|
19983
21437
|
const range = await readDependencyRange(fileURLToPath(import.meta.url));
|
|
19984
21438
|
const autoDownload = options.autoDownload ?? envDownloadsAllowed();
|
|
19985
21439
|
const deadlineMs = options.deadlineMs ?? DEADLINE_MS;
|
|
@@ -20001,9 +21455,15 @@ async function loadOnce(options) {
|
|
|
20001
21455
|
try {
|
|
20002
21456
|
compat = runtimeFromCompat(options.compatModule ?? framework);
|
|
20003
21457
|
} catch (error) {
|
|
20004
|
-
warn(options.log, `${OWN_PREFIX} WARNING — the ${COMPAT_PACKAGE} compatibility gate could not be initialised (${reasonOf(error)}); the compatibility gate is SKIPPED and the plugin will mount anyway`);
|
|
21458
|
+
warn(options.log, `${OWN_PREFIX} WARNING — the ${COMPAT_PACKAGE} compatibility gate could not be initialised (${reasonOf$2(error)}); the compatibility gate is SKIPPED and the plugin will mount anyway`);
|
|
20005
21459
|
}
|
|
20006
21460
|
let disposed = false;
|
|
21461
|
+
/** The shared `runtime-deps` handles: set when `provisionResources` declares each item. */
|
|
21462
|
+
let deps;
|
|
21463
|
+
let native;
|
|
21464
|
+
/** The startup dependency audit and its notice registry; both exist once `provisionResources` ran. */
|
|
21465
|
+
let audit;
|
|
21466
|
+
let noticeRegistry;
|
|
20007
21467
|
return {
|
|
20008
21468
|
home,
|
|
20009
21469
|
kit: framework,
|
|
@@ -20012,40 +21472,148 @@ async function loadOnce(options) {
|
|
|
20012
21472
|
tools: join(home, "tools"),
|
|
20013
21473
|
models: join(home, "models")
|
|
20014
21474
|
},
|
|
20015
|
-
|
|
21475
|
+
runtimeDeps() {
|
|
21476
|
+
return deps;
|
|
21477
|
+
},
|
|
21478
|
+
nativeDeps() {
|
|
21479
|
+
return native;
|
|
21480
|
+
},
|
|
21481
|
+
depsResource() {
|
|
21482
|
+
return declaration;
|
|
21483
|
+
},
|
|
21484
|
+
notices() {
|
|
21485
|
+
return noticeRegistry;
|
|
21486
|
+
},
|
|
21487
|
+
async startDependencyAudit() {
|
|
21488
|
+
if (audit === void 0) return;
|
|
21489
|
+
try {
|
|
21490
|
+
await audit.run();
|
|
21491
|
+
} catch (error) {
|
|
21492
|
+
warn(options.log, `${OWN_PREFIX} WARNING — the dependency audit failed (${reasonOf$2(error)}); notices stay silent`);
|
|
21493
|
+
}
|
|
21494
|
+
},
|
|
21495
|
+
provisionResources(plan, onSettled, onFailure) {
|
|
20016
21496
|
if (disposed) return;
|
|
21497
|
+
const canServe = supportsRuntimeDeps(framework);
|
|
21498
|
+
const wantedDeps = plan.semanticDeps;
|
|
21499
|
+
const wantedNative = plan.nativeDeps;
|
|
21500
|
+
if (wantedDeps !== void 0 && !canServe) warn(options.log, `${OWN_PREFIX} WARNING — semantic self-heal is enabled but the loaded ${FRAMEWORK_PACKAGE} has no runtime-deps helper (it needs base >= 0.7.0 / interface v6); the item is SKIPPED and the plugin will mount anyway`);
|
|
21501
|
+
if (wantedNative !== void 0 && !canServe) warn(options.log, `${OWN_PREFIX} WARNING — native self-heal is enabled but the loaded ${FRAMEWORK_PACKAGE} has no runtime-deps helper (it needs base >= 0.7.0 / interface v6); the item is SKIPPED and the plugin will mount anyway`);
|
|
20017
21502
|
const items = [];
|
|
20018
21503
|
if (plan.model !== void 0) items.push(modelItem(framework, plan.model));
|
|
20019
|
-
|
|
21504
|
+
/**
|
|
21505
|
+
* Declare one shared item through the helper.
|
|
21506
|
+
*
|
|
21507
|
+
* The spec fields are forwarded verbatim (the helper copies them, so the content key cannot
|
|
21508
|
+
* move under a mutating caller). Notices are NOT passed in: the helper's registry binds a line
|
|
21509
|
+
* to its ITEM's state alone, while an audit entry may observe a package that no item
|
|
21510
|
+
* of ours installs — so every notice is registered below, on one registry, from the audit.
|
|
21511
|
+
*/
|
|
21512
|
+
const declare = (itemId, spec, timeoutMs, label) => {
|
|
21513
|
+
try {
|
|
21514
|
+
const handle = framework.createRuntimeDeps({
|
|
21515
|
+
namespace: PLUGIN,
|
|
21516
|
+
itemId,
|
|
21517
|
+
...spec
|
|
21518
|
+
});
|
|
21519
|
+
items.push(runtimeDepsItem(handle.item, timeoutMs));
|
|
21520
|
+
return handle;
|
|
21521
|
+
} catch (error) {
|
|
21522
|
+
warn(options.log, `${OWN_PREFIX} WARNING — the shared ${label} runtime-deps item could not be declared (${reasonOf$2(error)}); self-heal for it is SKIPPED and the plugin will mount anyway`);
|
|
21523
|
+
return;
|
|
21524
|
+
}
|
|
21525
|
+
};
|
|
21526
|
+
if (wantedDeps !== void 0 && canServe) deps = declare(SEMANTIC_DEPS_ITEM, wantedDeps.spec, SEMANTIC_DEPS_ATTEMPT_TIMEOUT_MS, "semantic");
|
|
21527
|
+
if (wantedNative !== void 0 && canServe) native = declare(NATIVE_DEPS_ITEM, wantedNative.spec, NATIVE_DEPS_ATTEMPT_TIMEOUT_MS, "native");
|
|
21528
|
+
const handles = [deps, native].filter((handle) => handle !== void 0);
|
|
20020
21529
|
const maxItemTimeoutMs = items.reduce((max, item) => Math.max(max, item.policy?.timeoutMs ?? 0), 0);
|
|
20021
|
-
|
|
20022
|
-
|
|
20023
|
-
|
|
20024
|
-
|
|
20025
|
-
|
|
20026
|
-
|
|
20027
|
-
|
|
20028
|
-
|
|
21530
|
+
/** The shared roots a module probe may resolve from, in declaration order (ready ones only). */
|
|
21531
|
+
const roots = () => handles.map((handle) => handle.resolve()?.dir).filter((dir) => dir !== void 0 && dir !== "");
|
|
21532
|
+
let resources;
|
|
21533
|
+
if (items.length > 0) {
|
|
21534
|
+
resources = framework.createProvisioner({
|
|
21535
|
+
home,
|
|
21536
|
+
envinitRange: range,
|
|
21537
|
+
logger: provisionLogger(options.log),
|
|
21538
|
+
policy: {
|
|
21539
|
+
autoDownload: autoDownload ? {
|
|
21540
|
+
[framework.MODEL_CACHE_KIND]: plan.modelAutoDownload,
|
|
21541
|
+
...handles.length === 0 ? {} : { [framework.RUNTIME_DEPS_KIND]: true }
|
|
21542
|
+
} : false,
|
|
21543
|
+
deadlineMs: Math.max(deadlineMs, maxItemTimeoutMs),
|
|
21544
|
+
mirrors: { archive: [...plan.archiveMirrors] }
|
|
21545
|
+
}
|
|
21546
|
+
});
|
|
21547
|
+
resources.register(framework.binaryArchiveProvider());
|
|
21548
|
+
resources.register(framework.modelCacheProvider());
|
|
21549
|
+
if (handles.length > 0) resources.register(framework.runtimeDepsProvider());
|
|
21550
|
+
resources.declare({
|
|
21551
|
+
plugin: PLUGIN,
|
|
21552
|
+
items
|
|
21553
|
+
});
|
|
21554
|
+
disposers.push(resources);
|
|
21555
|
+
}
|
|
21556
|
+
const inflight = new Set(items.map((item) => item.id));
|
|
21557
|
+
const itemState = (id) => {
|
|
21558
|
+
if (inflight.has(id)) return "pending";
|
|
21559
|
+
if (resources === void 0) return "missing";
|
|
21560
|
+
return dependencyStateOf(resources.resolve(id));
|
|
21561
|
+
};
|
|
21562
|
+
const notices = plan.dependencyAudit ?? [];
|
|
21563
|
+
if (notices.length > 0) {
|
|
21564
|
+
const createRegistry = framework.createSessionNotices;
|
|
21565
|
+
if (typeof createRegistry !== "function") warn(options.log, `${OWN_PREFIX} WARNING — ${notices.length} dependency-audit entr(y/ies) are declared but the loaded ${FRAMEWORK_PACKAGE} has no session-notice registry (it needs base >= 0.7.0 / interface v6); no notice will be delivered`);
|
|
21566
|
+
audit = createDependencyAudit({
|
|
21567
|
+
probes: probesOf(notices, roots, itemState),
|
|
21568
|
+
vectors: plan.vectorCount ?? (() => 0),
|
|
21569
|
+
logger: {
|
|
21570
|
+
info: (message) => {
|
|
21571
|
+
options.log?.info(`${OWN_PREFIX} ${message}`);
|
|
21572
|
+
},
|
|
21573
|
+
warn: (message) => {
|
|
21574
|
+
options.log?.warn(`${OWN_PREFIX} ${message}`);
|
|
21575
|
+
}
|
|
21576
|
+
}
|
|
21577
|
+
});
|
|
21578
|
+
if (typeof createRegistry === "function") {
|
|
21579
|
+
noticeRegistry = createRegistry.call(framework, provisionLogger(options.log));
|
|
21580
|
+
registerDependencyNotices({
|
|
21581
|
+
registry: noticeRegistry,
|
|
21582
|
+
notices,
|
|
21583
|
+
audit
|
|
21584
|
+
});
|
|
20029
21585
|
}
|
|
21586
|
+
}
|
|
21587
|
+
if (resources === void 0) return;
|
|
21588
|
+
for (const handle of handles) handle.ensure(resources).catch((error) => {
|
|
21589
|
+
warn(options.log, `${OWN_PREFIX} WARNING — the shared runtime-deps item could not be dispatched (${reasonOf$2(error)}); the plugin continues`);
|
|
20030
21590
|
});
|
|
20031
|
-
resources.register(framework.binaryArchiveProvider());
|
|
20032
|
-
resources.register(framework.modelCacheProvider());
|
|
20033
|
-
resources.declare({
|
|
20034
|
-
plugin: PLUGIN,
|
|
20035
|
-
items
|
|
20036
|
-
});
|
|
20037
|
-
disposers.push(resources);
|
|
20038
21591
|
resources.ensure({
|
|
20039
21592
|
only: items.map((item) => item.id),
|
|
20040
21593
|
onSettled: (entry) => {
|
|
20041
21594
|
try {
|
|
20042
|
-
|
|
21595
|
+
inflight.delete(entry.id);
|
|
21596
|
+
const shared = handles.find((handle) => handle.item.id === entry.id)?.resolve();
|
|
21597
|
+
const state = resources.resolve(entry.id);
|
|
21598
|
+
onSettled(entry, shared !== void 0 ? shared.dir : state !== void 0 && state.state === "ready" ? state.handle.dir : void 0);
|
|
21599
|
+
if (audit !== void 0) audit.run().catch((error) => {
|
|
21600
|
+
warn(options.log, `${OWN_PREFIX} WARNING — the dependency audit after ${entry.id} failed (${reasonOf$2(error)})`);
|
|
21601
|
+
});
|
|
20043
21602
|
} catch (error) {
|
|
20044
|
-
warn(options.log, `${OWN_PREFIX} WARNING — the onSettled callback failed (${reasonOf(error)})`);
|
|
21603
|
+
warn(options.log, `${OWN_PREFIX} WARNING — the onSettled callback failed (${reasonOf$2(error)})`);
|
|
20045
21604
|
}
|
|
20046
21605
|
}
|
|
20047
21606
|
}).catch((error) => {
|
|
20048
|
-
warn(options.log, `${OWN_PREFIX} WARNING — declaring the background resources failed (${reasonOf(error)}); the plugin continues (tools stay registered)`);
|
|
21607
|
+
warn(options.log, `${OWN_PREFIX} WARNING — declaring the background resources failed (${reasonOf$2(error)}); the plugin continues (tools stay registered)`);
|
|
21608
|
+
inflight.clear();
|
|
21609
|
+
if (audit !== void 0) audit.run().catch((auditError) => {
|
|
21610
|
+
warn(options.log, `${OWN_PREFIX} WARNING — the dependency audit after a dispatch failure failed (${reasonOf$2(auditError)})`);
|
|
21611
|
+
});
|
|
21612
|
+
try {
|
|
21613
|
+
onFailure?.(error);
|
|
21614
|
+
} catch (callbackError) {
|
|
21615
|
+
warn(options.log, `${OWN_PREFIX} WARNING — the onFailure callback failed (${reasonOf$2(callbackError)})`);
|
|
21616
|
+
}
|
|
20049
21617
|
});
|
|
20050
21618
|
},
|
|
20051
21619
|
dispose() {
|
|
@@ -20190,7 +21758,7 @@ function tokenize(rawInput) {
|
|
|
20190
21758
|
return tokens;
|
|
20191
21759
|
}
|
|
20192
21760
|
function metadataDroppedLine(dropped) {
|
|
20193
|
-
return " metadata_dropped: timestamps=" + String(dropped.timestamps) + ", trust=" + String(dropped.trust) + ", assert_count=" + String(dropped.assert_count) + ", helpful_count=" + String(dropped.helpful_count) + ", pinned=" + String(dropped.pinned) + ", supersedes=" + String(dropped.supersedes);
|
|
21761
|
+
return " metadata_dropped: timestamps=" + String(dropped.timestamps) + ", trust=" + String(dropped.trust) + ", assert_count=" + String(dropped.assert_count) + ", helpful_count=" + String(dropped.helpful_count) + ", pinned=" + String(dropped.pinned) + ", supersedes=" + String(dropped.supersedes) + ", ttl_days=" + String(dropped.ttl_days) + ", valid_to=" + String(dropped.valid_to);
|
|
20194
21762
|
}
|
|
20195
21763
|
/** Failures are rendered in full up to {@link FAILURES_SHOWN}; the rest is a count, never silence. */
|
|
20196
21764
|
const FAILURES_SHOWN = 5;
|
|
@@ -20682,6 +22250,26 @@ function degradedLegNote(value) {
|
|
|
20682
22250
|
const result = value?.result;
|
|
20683
22251
|
return result !== null && typeof result === "object" && result.degraded === true ? DEGRADED_LEG_NOTE : void 0;
|
|
20684
22252
|
}
|
|
22253
|
+
/**
|
|
22254
|
+
* The paging sentence of a PURE TIME window (`mode:"time"`, RECALL_INTERFACE §2.4), as one line the
|
|
22255
|
+
* model can read without doing arithmetic: 「共 N 条,已显示 M 条,还有 K 条未显示」.
|
|
22256
|
+
*
|
|
22257
|
+
* The fields it spells out (`total` / `hits.length` / `remaining`) are already in the JSON block; the
|
|
22258
|
+
* sentence exists because a page of 50 out of a 286-row day otherwise looks like the whole day unless
|
|
22259
|
+
* the reader stops to compare four numbers. Emitted ONLY when something is left unseen — a window that
|
|
22260
|
+
* fits on its page needs no note, and `remaining` is 0 exactly then.
|
|
22261
|
+
*
|
|
22262
|
+
* Same rules as {@link degradedLegNote}: a SECOND block, appended after the parseable JSON one, and
|
|
22263
|
+
* absent whenever the payload is not a time result (including older hosts, which send no `remaining`).
|
|
22264
|
+
*/
|
|
22265
|
+
function timePageNote(value) {
|
|
22266
|
+
const result = value?.result;
|
|
22267
|
+
if (result === null || typeof result !== "object" || result.mode !== "time") return void 0;
|
|
22268
|
+
const { total, remaining, hits } = result;
|
|
22269
|
+
if (typeof total !== "number" || typeof remaining !== "number" || remaining <= 0) return void 0;
|
|
22270
|
+
if (!Array.isArray(hits)) return void 0;
|
|
22271
|
+
return `时间窗口共 ${String(total)} 条,已显示 ${String(hits.length)} 条,还有 ${String(remaining)} 条未显示。`;
|
|
22272
|
+
}
|
|
20685
22273
|
/** The tool output spec registered on every mem tool. */
|
|
20686
22274
|
const OUTPUT = {
|
|
20687
22275
|
schema: {
|
|
@@ -20693,10 +22281,15 @@ const OUTPUT = {
|
|
|
20693
22281
|
type: "text",
|
|
20694
22282
|
text: JSON.stringify(toWellFormedDeep(value), null, 2)
|
|
20695
22283
|
}];
|
|
20696
|
-
const
|
|
20697
|
-
if (
|
|
22284
|
+
const degraded = degradedLegNote(value);
|
|
22285
|
+
if (degraded !== void 0) blocks.push({
|
|
22286
|
+
type: "text",
|
|
22287
|
+
text: degraded
|
|
22288
|
+
});
|
|
22289
|
+
const paging = timePageNote(value);
|
|
22290
|
+
if (paging !== void 0) blocks.push({
|
|
20698
22291
|
type: "text",
|
|
20699
|
-
text:
|
|
22292
|
+
text: paging
|
|
20700
22293
|
});
|
|
20701
22294
|
return blocks;
|
|
20702
22295
|
}
|
|
@@ -20954,6 +22547,22 @@ function hostLogger(ctx) {
|
|
|
20954
22547
|
};
|
|
20955
22548
|
}
|
|
20956
22549
|
/**
|
|
22550
|
+
* The host's agent registry (`ctx.agents`), read defensively.
|
|
22551
|
+
*
|
|
22552
|
+
* It is the one place this plugin learns whether a session is a top-level (owner) agent or a
|
|
22553
|
+
* worker/subagent, and that decision only decides whether a session NOTICE is delivered. The read is
|
|
22554
|
+
* deliberately structural ({@link isOwnerSession} takes `unknown`): a host may not load the agent
|
|
22555
|
+
* registry at all, and `agent/pre-step` itself is emitted by the agent loop, so an absent registry is
|
|
22556
|
+
* "cannot tell" — never a reason to throw on the prompt path.
|
|
22557
|
+
*/
|
|
22558
|
+
function agentRegistry(ctx) {
|
|
22559
|
+
try {
|
|
22560
|
+
return ctx.agents;
|
|
22561
|
+
} catch {
|
|
22562
|
+
return;
|
|
22563
|
+
}
|
|
22564
|
+
}
|
|
22565
|
+
/**
|
|
20957
22566
|
* Declare the expensive resources through the family framework, warm the tokenizer and the embedding
|
|
20958
22567
|
* model, and continue initialisation as each item settles.
|
|
20959
22568
|
*
|
|
@@ -20970,12 +22579,37 @@ function hostLogger(ctx) {
|
|
|
20970
22579
|
* — `managedModelSpec` decides): the framework's root would then collect a copy nothing reads. In
|
|
20971
22580
|
* that case, and whenever no item will warm the model, the warm is started here directly.
|
|
20972
22581
|
*
|
|
22582
|
+
* **Optional dependency repair.** `mem:semantic-deps` (shared `runtime-deps` store) is declared only
|
|
22583
|
+
* when `semantic.self_heal.enabled` is on (`managedSemanticDepsSpec` decides). When it is present, the
|
|
22584
|
+
* semantic warm waits for BOTH items: the shared root has to exist before the backend chooses it, and
|
|
22585
|
+
* the model files have to be on disk before the pipeline opens them. On success the handle directory
|
|
22586
|
+
* is handed to the backend (`useSemanticModuleDir`) so the import comes from the verified shared root;
|
|
22587
|
+
* on failure the backend keeps its own optional-dependency resolution.
|
|
22588
|
+
*
|
|
22589
|
+
* SESSION NOTICES are NOT carried by these handles: they are the `audit` entries of the plugin's own
|
|
22590
|
+
* `res/runtime-deps.yaml`, registered on one registry (`env.notices()`) whose predicates read the
|
|
22591
|
+
* startup dependency audit's cached state (`deps_audit.ts`). That is why a repair that is still
|
|
22592
|
+
* installing cannot be reported as a failure, and why an entry about a PACKAGE (rather than an item)
|
|
22593
|
+
* can be delivered at all.
|
|
22594
|
+
*
|
|
22595
|
+
* **NATIVE optional dependency repair.** `mem:native-deps` (a SEPARATE `runtime-deps` item: different
|
|
22596
|
+
* package set ⇒ different content-addressed root) repairs `nodejieba` and `hnswlib-node` when
|
|
22597
|
+
* `semantic.native_self_heal.enabled` is on. When it is present the TOKENIZER warm is deferred to its
|
|
22598
|
+
* settlement: the dictionary parse (~1 s, synchronous) has to happen once, from the root the process
|
|
22599
|
+
* will actually use, not from a tree that is about to be superseded. On success both module roots are
|
|
22600
|
+
* handed to their loaders (`useTokenizerModuleDir`, `MemoryStore.useNativeModuleDir`).
|
|
22601
|
+
*
|
|
20973
22602
|
* Outcomes handled below:
|
|
20974
22603
|
*
|
|
20975
22604
|
* - `mem:model` settles (present/installed) → warm the semantic path from the managed root, now
|
|
20976
22605
|
* that the files are on disk. That ordering is the whole point: the runtime's constructor warm is
|
|
20977
22606
|
* deferred while the framework is up (`deferWarm`), so nothing can race the install, and the
|
|
20978
22607
|
* explicit warm here is what starts the model.
|
|
22608
|
+
* - `mem:semantic-deps` settles (present/installed) → point the backend at the shared root, then
|
|
22609
|
+
* warm. failed/skipped → keep the optional path and still warm. Either way the package's audit
|
|
22610
|
+
* entry is refreshed, because the notice's other half is "is it loadable NOW".
|
|
22611
|
+
* - `mem:native-deps` settles → hand both module roots over, refresh the two native audit entries,
|
|
22612
|
+
* then warm the tokenizer (or retry it, when an earlier lazy write already failed).
|
|
20979
22613
|
* - `mem:model` failed/skipped → one WARNING, then STILL warm: with `semantic.auto_download` on the
|
|
20980
22614
|
* runtime takes its own download path, and with it off the semantic leg degrades to FTS+entity.
|
|
20981
22615
|
* Either way the mount is untouched.
|
|
@@ -20989,20 +22623,65 @@ function hostLogger(ctx) {
|
|
|
20989
22623
|
function startManagedResources(env, rt, logger, waitFor) {
|
|
20990
22624
|
const { semantic, tools } = rt.config.common;
|
|
20991
22625
|
const model = managedModelSpec(semantic, env.roots.models);
|
|
22626
|
+
const loaded = env.depsResource();
|
|
22627
|
+
const spec = loaded.ok ? loaded.resource : void 0;
|
|
22628
|
+
const depsSpec = spec === void 0 ? void 0 : managedSemanticDepsSpec(semantic, spec.semantic, env.home);
|
|
22629
|
+
const semanticDeps = depsSpec === void 0 ? void 0 : { spec: depsSpec };
|
|
22630
|
+
const nativeSpec = spec === void 0 ? void 0 : managedNativeDepsSpec(semantic, spec.native, env.home);
|
|
22631
|
+
const nativeDeps = nativeSpec === void 0 ? void 0 : { spec: nativeSpec };
|
|
20992
22632
|
const plan = {
|
|
20993
22633
|
archiveMirrors: tools.mirror,
|
|
20994
22634
|
modelAutoDownload: semantic.auto_download,
|
|
20995
|
-
model
|
|
22635
|
+
model,
|
|
22636
|
+
semanticDeps,
|
|
22637
|
+
nativeDeps,
|
|
22638
|
+
dependencyAudit: managedDependencyAudit(spec?.audit),
|
|
22639
|
+
vectorCount: () => rt.memory.liveVectorCount()
|
|
22640
|
+
};
|
|
22641
|
+
const waitsForNative = nativeDeps !== void 0 && supportsRuntimeDeps(env.kit);
|
|
22642
|
+
let tokenizerWarmStarted = false;
|
|
22643
|
+
const warmTokenizer = () => {
|
|
22644
|
+
if (tokenizerWarmStarted) return;
|
|
22645
|
+
tokenizerWarmStarted = true;
|
|
22646
|
+
warmTokenizerAsync(rt, { ...waitFor === void 0 ? {} : { waitFor } });
|
|
22647
|
+
};
|
|
22648
|
+
if (!waitsForNative) warmTokenizer();
|
|
22649
|
+
const pending = /* @__PURE__ */ new Set();
|
|
22650
|
+
if (model !== void 0) pending.add(MODEL_ITEM);
|
|
22651
|
+
if (semanticDeps !== void 0 && supportsRuntimeDeps(env.kit)) pending.add(SEMANTIC_DEPS_ITEM);
|
|
22652
|
+
const warmOnce = () => {
|
|
22653
|
+
if (pending.size === 0) warmSemanticAsync(rt);
|
|
20996
22654
|
};
|
|
20997
|
-
|
|
20998
|
-
|
|
20999
|
-
|
|
21000
|
-
|
|
22655
|
+
if (pending.size === 0 && semantic.backend === "local_bge") warmSemanticAsync(rt);
|
|
22656
|
+
env.provisionResources(plan, (entry, handleDir) => {
|
|
22657
|
+
if (entry.id === "mem:model" || entry.id === "mem:semantic-deps") {
|
|
22658
|
+
if (entry.id === "mem:semantic-deps" && handleDir !== void 0) {
|
|
22659
|
+
rt.memory.useSemanticModuleDir(handleDir);
|
|
22660
|
+
logger.info(`envinit: semantic deps root ${handleDir}`);
|
|
22661
|
+
}
|
|
22662
|
+
pending.delete(entry.id);
|
|
22663
|
+
warmOnce();
|
|
22664
|
+
}
|
|
22665
|
+
if (entry.id === "mem:native-deps") {
|
|
22666
|
+
if (handleDir !== void 0) {
|
|
22667
|
+
useTokenizerModuleDir(handleDir);
|
|
22668
|
+
rt.memory.useNativeModuleDir(handleDir);
|
|
22669
|
+
logger.info(`envinit: native deps root ${handleDir}`);
|
|
22670
|
+
}
|
|
22671
|
+
warmTokenizer();
|
|
22672
|
+
}
|
|
21001
22673
|
if (entry.action === "failed" || entry.action === "skipped") {
|
|
21002
22674
|
logger.warn(`envinit: ${entry.id} is ${entry.action} (${String(entry.code)}${entry.reason === void 0 ? "" : ` — ${entry.reason}`})`);
|
|
22675
|
+
if (entry.id === "mem:semantic-deps") logger.warn("envinit: semantic self-heal did not complete — the runtime keeps its own optional-dependency resolution, and retrieval degrades to FTS+entity if that is absent too");
|
|
22676
|
+
if (entry.id === "mem:native-deps") logger.warn("envinit: native self-heal did not complete — the runtime keeps its own optional-dependency resolution; entity/triple extraction falls back to the regex path and the vector store stays brute-force");
|
|
21003
22677
|
return;
|
|
21004
22678
|
}
|
|
21005
22679
|
logger.info(`envinit: ${entry.id} ${entry.action} (${entry.source}${entry.version === void 0 ? "" : ` ${entry.version}`})`);
|
|
22680
|
+
}, (error) => {
|
|
22681
|
+
logger.warn(`envinit: dispatching the background resources failed (${describeError$1(error)}); warming from the runtime's own paths`);
|
|
22682
|
+
pending.clear();
|
|
22683
|
+
warmOnce();
|
|
22684
|
+
warmTokenizer();
|
|
21006
22685
|
});
|
|
21007
22686
|
}
|
|
21008
22687
|
async function apply(ctx, config) {
|
|
@@ -21130,7 +22809,7 @@ async function apply(ctx, config) {
|
|
|
21130
22809
|
}
|
|
21131
22810
|
for (const warning of promptTextWarnings(promptSections)) logger.warn(`prompt text: ${warning}`);
|
|
21132
22811
|
logger.info(`prompt files: ${promptDir} (${loadedPrompts.map((entry) => `${entry.file}:${entry.source}`).join(", ")})`);
|
|
21133
|
-
ctx.on("agent/pre-step", async ({ signal }, next) => {
|
|
22812
|
+
ctx.on("agent/pre-step", async ({ agent, signal }, next) => {
|
|
21134
22813
|
const decision = await next();
|
|
21135
22814
|
if (decision.kind === "reject" || signal.aborted) return decision;
|
|
21136
22815
|
const text = freshUserText(decision.messages);
|
|
@@ -21141,18 +22820,22 @@ async function apply(ctx, config) {
|
|
|
21141
22820
|
} catch {
|
|
21142
22821
|
hit = false;
|
|
21143
22822
|
}
|
|
21144
|
-
|
|
22823
|
+
const sessionId = String(agent.id);
|
|
22824
|
+
const owner = isOwnerSession(agentRegistry(ctx), sessionId);
|
|
22825
|
+
const notices = noticeLines(env?.notices(), sessionId, owner);
|
|
22826
|
+
const injection = stepInjection(hit, notices);
|
|
22827
|
+
if (injection === "") return decision;
|
|
21145
22828
|
return {
|
|
21146
22829
|
...decision,
|
|
21147
22830
|
messages: [...decision.messages, createUserMessage({
|
|
21148
22831
|
content: [{
|
|
21149
22832
|
type: "text",
|
|
21150
|
-
text:
|
|
22833
|
+
text: injection
|
|
21151
22834
|
}],
|
|
21152
22835
|
source: {
|
|
21153
22836
|
kind: "mem-hint",
|
|
21154
22837
|
form: "notice",
|
|
21155
|
-
summary:
|
|
22838
|
+
summary: injection
|
|
21156
22839
|
}
|
|
21157
22840
|
})]
|
|
21158
22841
|
};
|
|
@@ -21179,6 +22862,7 @@ async function apply(ctx, config) {
|
|
|
21179
22862
|
log: logger,
|
|
21180
22863
|
compat
|
|
21181
22864
|
});
|
|
22865
|
+
env?.startDependencyAudit();
|
|
21182
22866
|
ctx.effect(() => async () => {
|
|
21183
22867
|
logger.info("plugin unmount: disposing runtime (closing databases)");
|
|
21184
22868
|
await rt.shutdown();
|