@avantf/dsh-mem 0.9.0 → 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 +103 -34
- 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/facts_io.d.ts +4 -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/runtime.d.ts +4 -8
- package/lib/engine/mem/store/facts_restore.d.ts +8 -6
- package/lib/engine/mem/store/memory.d.ts +51 -37
- 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 +9 -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 +62 -12
- package/lib/index.js +738 -557
- package/lib/interface-version.json +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/command.d.ts.map +1 -1
- package/lib/types/envinit-bootstrap.d.ts +1 -1
- package/lib/types/remote.d.ts.map +1 -1
- package/package.json +25 -24
package/lib/index.js
CHANGED
|
@@ -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
|
|
@@ -528,40 +668,83 @@ const presenceSchema = z.object({
|
|
|
528
668
|
/** Long-lived surfaces (plugin / MCP) tick this often; 0 = startup pass only. */
|
|
529
669
|
heartbeat_minutes: z.number().int().nonnegative().default(60)
|
|
530
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
|
+
*/
|
|
531
679
|
const trustSchema = z.object({
|
|
532
|
-
/** 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). */
|
|
533
681
|
enabled: z.boolean().default(true),
|
|
534
|
-
/**
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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),
|
|
538
707
|
/** Trust ≤ this ⇒ forgotten (archived 'forgot'). 0 = "reaching zero is forgetting". */
|
|
539
708
|
forget_threshold: z.number().min(0).max(1).default(0),
|
|
540
|
-
/**
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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),
|
|
546
717
|
/** Effective reinforcement events per fact per 24h (R10: zero-gain ones don't count). */
|
|
547
718
|
recall_daily_cap: z.number().int().nonnegative().default(3),
|
|
548
|
-
/** 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 … */
|
|
549
720
|
recall_marginal_decay: z.number().min(0).max(1).default(1),
|
|
550
|
-
/** Recall alone can never exceed this (and never pins — see D11). */
|
|
551
|
-
recall_ceiling: z.number().min(0).max(1).default(.85),
|
|
552
|
-
/** Explicit `helpful` / `unhelpful` step. */
|
|
553
|
-
feedback_delta: z.number().min(0).max(1).default(.05),
|
|
554
|
-
/** 0 = unlimited; >0 also caps feedback reinforcement per fact per 24h. */
|
|
555
|
-
feedback_daily_cap: z.number().int().nonnegative().default(0),
|
|
556
721
|
/** A revised fact inherits the settled trust (and pin) of the row it replaces. */
|
|
557
722
|
inherit_trust_on_update: z.boolean().default(true),
|
|
558
723
|
/** Pinned (permanent) facts are never physically purged. */
|
|
559
724
|
purge_skips_pinned: z.boolean().default(true),
|
|
560
|
-
/** Calendar fallback: unused for this many days ⇒ archived 'idle'. */
|
|
561
|
-
idle_calendar_days: z.number().int().positive().default(365),
|
|
562
725
|
/** Per-tick budget for the settle sweep (the other sweeps only touch critical rows). */
|
|
563
726
|
tick_max_facts: z.number().int().positive().default(5e3),
|
|
564
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
|
+
}
|
|
565
748
|
});
|
|
566
749
|
const lifecycleSchema = z.object({
|
|
567
750
|
/** Purge window, measured in ACTIVE days since `archived_clock`. */
|
|
@@ -758,8 +941,6 @@ const KIND_FILTER_DESCRIPTION = `限定记忆层;可选(取值:${MEMORY_KI
|
|
|
758
941
|
const SOURCE_REF_DESCRIPTION = "这条事实的来源(可选):未给 = 未知来源,引擎不会臆造。用 `kind:` 前缀标明来源类型——`session:<会话 id>`(会话供述)、`tool:<工具名>`(工具产出)、无前缀则记为 `manual`(调用方手工给出),前缀之后的文本原样保存。可用 `mem_admin` 的 `list`(`source=`)反查同一来源产生了哪些事实。";
|
|
759
942
|
/** P-13 `event_date`: the event time, written into `valid_from`. */
|
|
760
943
|
const EVENT_DATE_DESCRIPTION = "这件事发生的时间(可选,ISO 日期或日期时间,如 2026-10-06 或 2026-10-06T09:30:00+08:00):写入事实的事件时间,供时间窗检索与展示使用;不参与打分。未给 = 未知(不写)。";
|
|
761
|
-
/** P-13 `valid_until`: a known end, written into `valid_to` WITHOUT archiving the row. */
|
|
762
|
-
const VALID_UNTIL_DESCRIPTION = "这条事实有效到什么时候(可选,ISO 日期或日期时间):写入有效期终点,**不会归档该事实**——「仍然有效、但已知截止于某时」的事实照常参与检索。未给 = 未知(不写)。";
|
|
763
944
|
/**
|
|
764
945
|
* `title`: the short, human-readable handle of a journal entry (JOURNAL_TITLE_SEARCH §2.4/§11.1).
|
|
765
946
|
*
|
|
@@ -883,9 +1064,12 @@ function modelFacingToolResult(toolKey, action, value) {
|
|
|
883
1064
|
* - `source_ref` — the citation the answer must carry;
|
|
884
1065
|
* - `created_at` / `updated_at` — memory MAINTENANCE: when it was written and when it was last
|
|
885
1066
|
* changed is what tells the model whether a fact is fresh enough to act on (and `updated_at`
|
|
886
|
-
* differing from `created_at` is the only visible sign that it was revised)
|
|
887
|
-
*
|
|
888
|
-
*
|
|
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;
|
|
889
1073
|
* - `title` — the journal entry's short handle, for recognition and for the follow-up write flow
|
|
890
1074
|
* (an update inherits it). Display only: it never moved this hit's `score`;
|
|
891
1075
|
* - `scores` / `final` — only ever present when the caller explicitly asked for the evidence.
|
|
@@ -905,7 +1089,6 @@ const MODEL_HIT_FIELDS = [
|
|
|
905
1089
|
"created_at",
|
|
906
1090
|
"updated_at",
|
|
907
1091
|
"valid_from",
|
|
908
|
-
"valid_to",
|
|
909
1092
|
"scores",
|
|
910
1093
|
"final"
|
|
911
1094
|
];
|
|
@@ -913,7 +1096,6 @@ const MODEL_HIT_FIELDS = [
|
|
|
913
1096
|
const NULLABLE_HIT_FIELDS = /* @__PURE__ */ new Set([
|
|
914
1097
|
"updated_at",
|
|
915
1098
|
"valid_from",
|
|
916
|
-
"valid_to",
|
|
917
1099
|
"title"
|
|
918
1100
|
]);
|
|
919
1101
|
/**
|
|
@@ -1167,10 +1349,8 @@ const RememberUnion = z.discriminatedUnion("action", [
|
|
|
1167
1349
|
content: z.string().min(1).describe("要写入或替换的事实内容:一句自包含的陈述。action=add 时必填。"),
|
|
1168
1350
|
kind: z.enum(MEMORY_KINDS).optional().describe(KIND_WRITE_DESCRIPTION),
|
|
1169
1351
|
title: z.string().min(1).max(120).optional().describe(TITLE_WRITE_DESCRIPTION),
|
|
1170
|
-
ttl_days: z.number().int().nonnegative().optional().describe("有效期天数(0 或省略 = 不设有效期;正整数 = 自写入起该天数后自动归档)。update 未给则继承被改写事实的 TTL。"),
|
|
1171
1352
|
source_ref: z.string().min(1).optional().describe(SOURCE_REF_DESCRIPTION),
|
|
1172
1353
|
event_date: z.string().min(1).optional().describe(EVENT_DATE_DESCRIPTION),
|
|
1173
|
-
valid_until: z.string().min(1).optional().describe(VALID_UNTIL_DESCRIPTION),
|
|
1174
1354
|
relations: z.array(RelationInputSchema).max(20).optional().describe(RELATIONS_DESCRIPTION)
|
|
1175
1355
|
}),
|
|
1176
1356
|
z.object({
|
|
@@ -1180,24 +1360,14 @@ const RememberUnion = z.discriminatedUnion("action", [
|
|
|
1180
1360
|
kind: z.enum(MEMORY_KINDS).optional().describe(`改写后的${KIND_WRITE_DESCRIPTION}`),
|
|
1181
1361
|
title: z.string().min(1).max(120).optional().describe(`改写后的${TITLE_WRITE_DESCRIPTION}`),
|
|
1182
1362
|
retitle: z.literal(true).optional().describe(RETITLE_DESCRIPTION),
|
|
1183
|
-
ttl_days: z.number().int().nonnegative().optional().describe("新的有效期天数(0 = 取消有效期);可选,未给则继承被改写事实的 TTL。"),
|
|
1184
1363
|
source_ref: z.string().min(1).optional().describe(SOURCE_REF_DESCRIPTION),
|
|
1185
1364
|
event_date: z.string().min(1).optional().describe(EVENT_DATE_DESCRIPTION),
|
|
1186
|
-
valid_until: z.string().min(1).optional().describe(VALID_UNTIL_DESCRIPTION),
|
|
1187
1365
|
relations: z.array(RelationInputSchema).max(20).optional().describe(`改写后的${RELATIONS_DESCRIPTION}`)
|
|
1188
1366
|
}),
|
|
1189
1367
|
z.object({
|
|
1190
1368
|
action: z.literal("remove"),
|
|
1191
1369
|
fact_id: z.number().int().positive().describe("要删除的事实 ID。action=remove 时必填。"),
|
|
1192
1370
|
reason: z.string().optional().describe("删除原因;可选。")
|
|
1193
|
-
}),
|
|
1194
|
-
z.object({
|
|
1195
|
-
action: z.literal("helpful"),
|
|
1196
|
-
fact_id: z.number().int().positive().describe("对回答有帮助的事实 ID。action=helpful 时必填。")
|
|
1197
|
-
}),
|
|
1198
|
-
z.object({
|
|
1199
|
-
action: z.literal("unhelpful"),
|
|
1200
|
-
fact_id: z.number().int().positive().describe("对回答无帮助的事实 ID。action=unhelpful 时必填。")
|
|
1201
1371
|
})
|
|
1202
1372
|
]);
|
|
1203
1373
|
/**
|
|
@@ -1220,9 +1390,13 @@ function modelSubset(union, actions) {
|
|
|
1220
1390
|
return z.discriminatedUnion("action", options);
|
|
1221
1391
|
}
|
|
1222
1392
|
/**
|
|
1223
|
-
* The writes the model may make
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
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.
|
|
1226
1400
|
*/
|
|
1227
1401
|
const MODEL_REMEMBER_ACTIONS = [
|
|
1228
1402
|
"add",
|
|
@@ -1393,7 +1567,7 @@ const ModelAdminUnion = modelSubset(AdminUnion, [
|
|
|
1393
1567
|
const REMEMBER_TOOL = {
|
|
1394
1568
|
key: "remember",
|
|
1395
1569
|
name: "mem_remember",
|
|
1396
|
-
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 纠正一次——旧标题保留为别名,用旧标题检索仍能命中。",
|
|
1397
1571
|
input: ModelRememberUnion
|
|
1398
1572
|
};
|
|
1399
1573
|
const RECALL_TOOL = {
|
|
@@ -1419,7 +1593,7 @@ Number.MAX_SAFE_INTEGER, -Number.MAX_SAFE_INTEGER;
|
|
|
1419
1593
|
*
|
|
1420
1594
|
* Descriptions are authored per action ("action=update 时必填"), so taking the
|
|
1421
1595
|
* first branch's text verbatim told a model that `fact_id` only matters for
|
|
1422
|
-
* `update` although `remove
|
|
1596
|
+
* `update` although `remove` requires it too. The action-clause
|
|
1423
1597
|
* is stripped and replaced by the derived set of actions that make the field
|
|
1424
1598
|
* required.
|
|
1425
1599
|
*
|
|
@@ -1733,6 +1907,96 @@ function adoptWellFormed(kit) {
|
|
|
1733
1907
|
};
|
|
1734
1908
|
}
|
|
1735
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
|
|
1736
2000
|
//#region lib/types/prompt.js
|
|
1737
2001
|
/**
|
|
1738
2002
|
* The one system-prompt section this plugin contributes (DESIGN §10).
|
|
@@ -2299,8 +2563,13 @@ const DEFAULT_COMMON_CONFIG = `# avantf-mem 公共配置(分层第②层)。
|
|
|
2299
2563
|
# purge_after_archived_days: 365 # 归档事实的物理清理窗口(活跃日)
|
|
2300
2564
|
# contradiction_threshold: 0.6
|
|
2301
2565
|
|
|
2302
|
-
# trust:
|
|
2303
|
-
#
|
|
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)
|
|
2304
2573
|
# presence:
|
|
2305
2574
|
# heartbeat_minutes: 60 # 常驻进程的心跳(活跃日时钟与生命周期扫描)
|
|
2306
2575
|
|
|
@@ -2403,16 +2672,60 @@ function zodShape(schema) {
|
|
|
2403
2672
|
}
|
|
2404
2673
|
}
|
|
2405
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
|
+
/**
|
|
2406
2715
|
* Warn once about unknown config keys — at the ROOT level and inside known sections
|
|
2407
2716
|
* (TRUST_MODEL.md §7 / review R15+R23). This is generic typo protection, not legacy
|
|
2408
2717
|
* compatibility: `vector_store` or `semantics` are exactly the mistakes it catches.
|
|
2409
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
|
+
*
|
|
2410
2722
|
* Goes to the injected logger, not the process-wide default: this is the one message
|
|
2411
2723
|
* that tells an operator their config file is being partly ignored, and the DSH host
|
|
2412
2724
|
* only surfaces what the plugin's logger carries.
|
|
2413
2725
|
*/
|
|
2414
2726
|
function warnUnknownKeys(source, raw, rootShape, logger) {
|
|
2415
2727
|
const unknown = [];
|
|
2728
|
+
const explained = [];
|
|
2416
2729
|
for (const [key, value] of Object.entries(raw)) {
|
|
2417
2730
|
const section = rootShape[key];
|
|
2418
2731
|
if (section === void 0) {
|
|
@@ -2421,8 +2734,15 @@ function warnUnknownKeys(source, raw, rootShape, logger) {
|
|
|
2421
2734
|
}
|
|
2422
2735
|
const childShape = zodShape(section);
|
|
2423
2736
|
if (childShape === void 0 || !isPlainObject(value)) continue;
|
|
2424
|
-
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
|
+
}
|
|
2425
2744
|
}
|
|
2745
|
+
for (const message of explained) logger.warn(`${source}: ${message}`);
|
|
2426
2746
|
if (unknown.length > 0) logger.warn(`${source}: unknown config key(s) ignored: ${unknown.join(", ")} — check the spelling against docs/DESIGN.md §3`);
|
|
2427
2747
|
}
|
|
2428
2748
|
/** Section-aware merge: top-level sections merge one level deep, scalars/arrays replace. */
|
|
@@ -3503,7 +3823,7 @@ async function runToolSpec$1(rt, spec, args) {
|
|
|
3503
3823
|
//#region ../core/lib/trustHeartbeat.js
|
|
3504
3824
|
/**
|
|
3505
3825
|
* The trust heartbeat: the interval beat that advances the active-day clock and performs the
|
|
3506
|
-
* settle / forget /
|
|
3826
|
+
* settle / forget / purge sweeps (TRUST_MODEL.md §5), shared by the DSH plugin and the MCP
|
|
3507
3827
|
* server.
|
|
3508
3828
|
*
|
|
3509
3829
|
* The two surfaces used to install it separately, and the copies drifted in exactly the places a
|
|
@@ -5492,12 +5812,18 @@ const DDL = [
|
|
|
5492
5812
|
conflict_checked INTEGER NOT NULL DEFAULT 0,
|
|
5493
5813
|
archived_at TIMESTAMP,
|
|
5494
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).
|
|
5495
5818
|
ttl_days INTEGER DEFAULT 0,
|
|
5496
5819
|
-- P-07/P-13: when the fact became true and when it stopped being true. NULL = unknown, and no
|
|
5497
|
-
-- retrieval leg reads either column (the envelope only carries
|
|
5498
|
-
--
|
|
5499
|
-
--
|
|
5500
|
-
--
|
|
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.
|
|
5501
5827
|
valid_from TIMESTAMP,
|
|
5502
5828
|
valid_to TIMESTAMP,
|
|
5503
5829
|
-- The INSTANTS those two bounds name, in epoch milliseconds (migration 12).
|
|
@@ -5524,10 +5850,8 @@ const DDL = [
|
|
|
5524
5850
|
`CREATE INDEX IF NOT EXISTS idx_facts_supersedes ON facts(supersedes_id)`,
|
|
5525
5851
|
`CREATE INDEX IF NOT EXISTS idx_facts_trust ON facts(status, pinned, settle_clock)`,
|
|
5526
5852
|
`CREATE INDEX IF NOT EXISTS idx_facts_updated ON facts(status, updated_at)`,
|
|
5527
|
-
`CREATE INDEX IF NOT EXISTS idx_facts_ttl ON facts(status, ttl_days)`,
|
|
5528
5853
|
`CREATE INDEX IF NOT EXISTS idx_facts_purge ON facts(status, pinned, archived_clock)`,
|
|
5529
5854
|
`CREATE INDEX IF NOT EXISTS idx_facts_forget ON facts(status, pinned, trust_score)`,
|
|
5530
|
-
`CREATE INDEX IF NOT EXISTS idx_facts_idle_cutoff ON facts(status, pinned, julianday(COALESCE(last_retrieved_at, created_at)))`,
|
|
5531
5855
|
`CREATE INDEX IF NOT EXISTS idx_facts_bonus_window ON facts(bonus_window_at)`,
|
|
5532
5856
|
`CREATE TABLE IF NOT EXISTS fact_sources (
|
|
5533
5857
|
fact_id INTEGER NOT NULL REFERENCES facts(fact_id) ON DELETE CASCADE,
|
|
@@ -7868,7 +8192,7 @@ function timeParams(range) {
|
|
|
7868
8192
|
return timeSql(range)?.params ?? [];
|
|
7869
8193
|
}
|
|
7870
8194
|
/**
|
|
7871
|
-
* One stored `valid_from
|
|
8195
|
+
* One stored event-time (`valid_from`) string → epoch ms, using the SAME grammar as the filters.
|
|
7872
8196
|
*
|
|
7873
8197
|
* This is what the write path stamps into `valid_from_ms` and what migration 12's backfill applies to
|
|
7874
8198
|
* existing rows, so "the event time a caller wrote" and "the event time a window compares" can never
|
|
@@ -8762,6 +9086,14 @@ const MEMORY_SCHEMA = {
|
|
|
8762
9086
|
db.exec(TITLE_ALIASES_TABLE_DDL);
|
|
8763
9087
|
for (const sql of TITLE_ALIASES_INDEXES) db.exec(sql);
|
|
8764
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
|
+
}
|
|
8765
9097
|
}
|
|
8766
9098
|
]
|
|
8767
9099
|
};
|
|
@@ -9014,6 +9346,102 @@ function entityBags(db, ids, q) {
|
|
|
9014
9346
|
return out;
|
|
9015
9347
|
}
|
|
9016
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
|
|
9017
9445
|
//#region ../core/lib/db/dao/facts.js
|
|
9018
9446
|
/**
|
|
9019
9447
|
* The column a pure time window orders by — the axis the caller filtered on.
|
|
@@ -9051,7 +9479,7 @@ const FACT_COLUMNS_NO_BLOB = `fact_id, content, kind, title, tags, trust_score,
|
|
|
9051
9479
|
pinned, pinned_at, bonus_count, bonus_window_at, last_reinforced_at, archived_clock,
|
|
9052
9480
|
retrieval_count, helpful_count, last_retrieved_at, embedding_model, vector_store, status,
|
|
9053
9481
|
supersedes_id, entities_version, conflict_checked, archived_at, archive_reason, ttl_days,
|
|
9054
|
-
valid_from,
|
|
9482
|
+
valid_from, valid_from_ms, assert_count, created_at, updated_at`;
|
|
9055
9483
|
/**
|
|
9056
9484
|
* The FTS leg's exact SQL, exported so the plan guard (`test/fts_plan.spec.ts`) and the bench
|
|
9057
9485
|
* control group EXPLAIN the production text itself instead of a copy that can silently drift.
|
|
@@ -9369,12 +9797,11 @@ var FactsDao = class FactsDao {
|
|
|
9369
9797
|
const out = /* @__PURE__ */ new Map();
|
|
9370
9798
|
if (!ids.length) return out;
|
|
9371
9799
|
const { placeholders, values } = inList(ids);
|
|
9372
|
-
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);
|
|
9373
9801
|
for (const row of rows) out.set(row.fact_id, {
|
|
9374
9802
|
created_at: row.created_at === null ? "" : String(row.created_at),
|
|
9375
9803
|
updated_at: row.updated_at === null ? null : String(row.updated_at),
|
|
9376
9804
|
valid_from: row.valid_from === null || row.valid_from === void 0 ? null : String(row.valid_from),
|
|
9377
|
-
valid_to: row.valid_to === null || row.valid_to === void 0 ? null : String(row.valid_to),
|
|
9378
9805
|
title: row.title === null || row.title === void 0 || row.title === "" ? null : String(row.title)
|
|
9379
9806
|
});
|
|
9380
9807
|
return out;
|
|
@@ -9414,31 +9841,33 @@ var FactsDao = class FactsDao {
|
|
|
9414
9841
|
* `createdAt` is named EXPLICITLY rather than left to the column DEFAULT, because a
|
|
9415
9842
|
* superseding revision must INHERIT the archived row's `created_at` (the memory's first
|
|
9416
9843
|
* assertion date, distinct from when its text was rewritten). `updated_at` and
|
|
9417
|
-
* `last_retrieved_at` are stamped `CURRENT_TIMESTAMP` in the same statement
|
|
9418
|
-
*
|
|
9419
|
-
*
|
|
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.
|
|
9420
9850
|
*/
|
|
9421
9851
|
insertRevision(v) {
|
|
9422
9852
|
return this.db.prepare(`INSERT OR IGNORE INTO facts
|
|
9423
|
-
(content, kind, title,
|
|
9853
|
+
(content, kind, title, supersedes_id, hrr_vector,
|
|
9424
9854
|
trust_score, settle_clock, pinned, pinned_at, bonus_count, bonus_window_at,
|
|
9425
|
-
entities_version, conflict_checked, valid_from,
|
|
9855
|
+
entities_version, conflict_checked, valid_from, valid_from_ms,
|
|
9426
9856
|
created_at, updated_at, last_retrieved_at)
|
|
9427
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
9428
|
-
?, 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);
|
|
9429
9859
|
}
|
|
9430
9860
|
/**
|
|
9431
|
-
* P-10: land an explicitly given
|
|
9432
|
-
* caller named `event_date`
|
|
9433
|
-
*
|
|
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).
|
|
9434
9864
|
*/
|
|
9435
|
-
applyValidity(factId, validFrom
|
|
9436
|
-
if (validFrom === void 0
|
|
9865
|
+
applyValidity(factId, validFrom) {
|
|
9866
|
+
if (validFrom === void 0) return;
|
|
9437
9867
|
this.db.prepare(`UPDATE facts
|
|
9438
9868
|
SET valid_from = COALESCE(?, valid_from), valid_from_ms = COALESCE(?, valid_from_ms),
|
|
9439
|
-
valid_to = COALESCE(?, valid_to), valid_to_ms = COALESCE(?, valid_to_ms),
|
|
9440
9869
|
updated_at = CURRENT_TIMESTAMP
|
|
9441
|
-
WHERE fact_id = ?`).run(validFrom
|
|
9870
|
+
WHERE fact_id = ?`).run(validFrom.text, validFrom.ms, factId);
|
|
9442
9871
|
}
|
|
9443
9872
|
/** P-10: the row's assertion counter (`1` when the column is somehow NULL on a legacy row). */
|
|
9444
9873
|
assertCount(factId) {
|
|
@@ -9458,17 +9887,6 @@ var FactsDao = class FactsDao {
|
|
|
9458
9887
|
return Number(row?.n ?? 1);
|
|
9459
9888
|
}
|
|
9460
9889
|
/**
|
|
9461
|
-
* P-07 audit: stamp `valid_to` on a row that is leaving the active corpus (supersede / a
|
|
9462
|
-
* contradiction verdict). `COALESCE` keeps an end the caller already declared (`valid_until`):
|
|
9463
|
-
* overwriting it with the archival instant would replace a real answer with an administrative one.
|
|
9464
|
-
*/
|
|
9465
|
-
markValidTo(factId, at, atMs = null) {
|
|
9466
|
-
this.db.prepare(`UPDATE facts
|
|
9467
|
-
SET valid_to = COALESCE(valid_to, ?),
|
|
9468
|
-
valid_to_ms = CASE WHEN valid_to IS NULL THEN ? ELSE valid_to_ms END
|
|
9469
|
-
WHERE fact_id = ?`).run(at, atMs, factId);
|
|
9470
|
-
}
|
|
9471
|
-
/**
|
|
9472
9890
|
* P-07: "who superseded this fact", derived from the existing `supersedes_id` link — no
|
|
9473
9891
|
* redundant column. `MIN` is deterministic when several revisions named the same predecessor
|
|
9474
9892
|
* (which a merge can produce); the caller renders it as a single direct successor.
|
|
@@ -9629,17 +10047,22 @@ var FactsDao = class FactsDao {
|
|
|
9629
10047
|
}
|
|
9630
10048
|
return changes;
|
|
9631
10049
|
}
|
|
9632
|
-
/**
|
|
9633
|
-
|
|
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) {
|
|
9634
10057
|
return this.db.prepare(`UPDATE facts
|
|
9635
10058
|
SET status = 'active', archived_at = NULL, archive_reason = NULL, archived_clock = NULL,
|
|
9636
|
-
kind = ?,
|
|
10059
|
+
kind = ?, trust_score = ?, settle_clock = ?,
|
|
9637
10060
|
last_retrieved_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
|
9638
|
-
WHERE fact_id = ? AND status = 'archived'`).run(kind,
|
|
10061
|
+
WHERE fact_id = ? AND status = 'archived'`).run(kind, trust, clock, factId).changes;
|
|
9639
10062
|
}
|
|
9640
|
-
/** Land explicit kind
|
|
9641
|
-
applyValues(factId, kind
|
|
9642
|
-
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);
|
|
9643
10066
|
}
|
|
9644
10067
|
/**
|
|
9645
10068
|
* Land a title on a row that already exists — the ONE write behind every "backfill" path
|
|
@@ -9716,13 +10139,12 @@ var FactsDao = class FactsDao {
|
|
|
9716
10139
|
this.db.prepare("UPDATE facts SET supersedes_id = ? WHERE fact_id = ? AND supersedes_id IS NULL").run(supersedesId, factId);
|
|
9717
10140
|
}
|
|
9718
10141
|
/**
|
|
9719
|
-
* 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).
|
|
9720
10143
|
*
|
|
9721
10144
|
* `AND status = 'archived'` is load-bearing, like the guard on every sibling above: this UPDATE
|
|
9722
10145
|
* clears the archive bookkeeping and refreshes `last_retrieved_at`, so ungated it would take an
|
|
9723
|
-
* ACTIVE row and
|
|
9724
|
-
*
|
|
9725
|
-
* 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.
|
|
9726
10148
|
*/
|
|
9727
10149
|
restore(factId, trust, clock) {
|
|
9728
10150
|
return this.db.prepare(`UPDATE facts
|
|
@@ -9730,32 +10152,6 @@ var FactsDao = class FactsDao {
|
|
|
9730
10152
|
trust_score = ?, settle_clock = ?, last_retrieved_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
|
9731
10153
|
WHERE fact_id = ? AND status = 'archived'`).run(trust, clock, factId).changes;
|
|
9732
10154
|
}
|
|
9733
|
-
bumpHelpful(factId, delta) {
|
|
9734
|
-
this.db.prepare("UPDATE facts SET helpful_count = MAX(0, helpful_count + ?) WHERE fact_id = ?").run(delta, factId);
|
|
9735
|
-
}
|
|
9736
|
-
/** Feedback outcome: pin at the permanent threshold (trust snaps to 1). */
|
|
9737
|
-
pinOutcome(factId, clock, bonusCount, windowAt) {
|
|
9738
|
-
this.db.prepare(`UPDATE facts
|
|
9739
|
-
SET trust_score = 1, pinned = 1, pinned_at = COALESCE(pinned_at, CURRENT_TIMESTAMP),
|
|
9740
|
-
settle_clock = ?, bonus_count = ?, bonus_window_at = ?, archived_clock = NULL,
|
|
9741
|
-
last_reinforced_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
|
9742
|
-
WHERE fact_id = ?`).run(clock, bonusCount, windowAt, factId);
|
|
9743
|
-
}
|
|
9744
|
-
/** Feedback outcome: the forget line was reached — archive in the same statement. */
|
|
9745
|
-
forgetOutcome(factId, trust, clock, bonusCount, windowAt) {
|
|
9746
|
-
this.db.prepare(`UPDATE facts
|
|
9747
|
-
SET trust_score = ?, settle_clock = ?, bonus_count = ?, bonus_window_at = ?,
|
|
9748
|
-
status = 'archived', archived_at = CURRENT_TIMESTAMP, archived_clock = ?,
|
|
9749
|
-
archive_reason = 'forgot', updated_at = CURRENT_TIMESTAMP
|
|
9750
|
-
WHERE fact_id = ?`).run(trust, clock, bonusCount, windowAt, clock, factId);
|
|
9751
|
-
}
|
|
9752
|
-
/** Feedback outcome: reinforced (or decayed) but still active. */
|
|
9753
|
-
reinforceOutcome(factId, trust, clock, bonusCount, windowAt) {
|
|
9754
|
-
this.db.prepare(`UPDATE facts
|
|
9755
|
-
SET trust_score = ?, settle_clock = ?, bonus_count = ?, bonus_window_at = ?,
|
|
9756
|
-
last_reinforced_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
|
9757
|
-
WHERE fact_id = ?`).run(trust, clock, bonusCount, windowAt, factId);
|
|
9758
|
-
}
|
|
9759
10155
|
pin(factId, clock) {
|
|
9760
10156
|
return this.db.prepare(`UPDATE facts
|
|
9761
10157
|
SET pinned = 1, pinned_at = COALESCE(pinned_at, CURRENT_TIMESTAMP), trust_score = 1,
|
|
@@ -9844,17 +10240,25 @@ var FactsDao = class FactsDao {
|
|
|
9844
10240
|
return Number(this.db.prepare("SELECT COUNT(*) AS n FROM facts WHERE status = 'active' AND pinned = 1").get()?.n ?? 0);
|
|
9845
10241
|
}
|
|
9846
10242
|
/**
|
|
9847
|
-
* ACTIVE facts whose decayed trust
|
|
9848
|
-
*
|
|
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.
|
|
9849
10256
|
*/
|
|
9850
10257
|
countForgettingWithin(p) {
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
clock: p.clock,
|
|
9856
|
-
soon: p.withinDays
|
|
9857
|
-
})?.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;
|
|
9858
10262
|
}
|
|
9859
10263
|
/** Facts with a still-live (calendar 24h) reinforcement window — the per-fact quota. */
|
|
9860
10264
|
/**
|
|
@@ -9874,31 +10278,15 @@ var FactsDao = class FactsDao {
|
|
|
9874
10278
|
granted: Number(row?.granted ?? 0)
|
|
9875
10279
|
};
|
|
9876
10280
|
}
|
|
9877
|
-
/** ACTIVE, unpinned facts unused for `days` CALENDAR days (the idle-archival fallback). */
|
|
9878
|
-
countIdleCandidates(days) {
|
|
9879
|
-
return Number(this.db.prepare(`SELECT COUNT(*) AS n FROM facts
|
|
9880
|
-
WHERE status = 'active' AND pinned = 0
|
|
9881
|
-
AND julianday(COALESCE(last_retrieved_at, created_at)) < julianday('now', :modifier)`).get({ modifier: `-${days} days` })?.n ?? 0);
|
|
9882
|
-
}
|
|
9883
10281
|
/**
|
|
9884
|
-
* Rows still due for
|
|
10282
|
+
* Rows still due for ② after a budgeted tick — what the NEXT pass will archive.
|
|
9885
10283
|
*
|
|
9886
|
-
*
|
|
9887
|
-
*
|
|
9888
|
-
*
|
|
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.
|
|
9889
10287
|
*/
|
|
9890
10288
|
countArchiveBacklog(p) {
|
|
9891
|
-
|
|
9892
|
-
forgetThreshold: p.forgetThreshold,
|
|
9893
|
-
idleModifier: `-${p.idleCalendarDays} days`
|
|
9894
|
-
};
|
|
9895
|
-
return Number(this.db.prepare(`SELECT COUNT(*) AS n FROM (
|
|
9896
|
-
SELECT fact_id FROM facts WHERE status = 'active' AND ttl_days > 0
|
|
9897
|
-
AND julianday('now') - julianday(created_at) > ttl_days
|
|
9898
|
-
UNION SELECT fact_id FROM facts WHERE status = 'active' AND pinned = 0 AND trust_score <= :forgetThreshold
|
|
9899
|
-
UNION SELECT fact_id FROM facts WHERE status = 'active' AND pinned = 0
|
|
9900
|
-
AND julianday(COALESCE(last_retrieved_at, created_at)) < julianday('now', :idleModifier)
|
|
9901
|
-
)`).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);
|
|
9902
10290
|
}
|
|
9903
10291
|
/**
|
|
9904
10292
|
* Drop unusable (stale-dim) vectors so a re-encode can replace them — and re-queue the same
|
|
@@ -10237,48 +10625,60 @@ var FactsDao = class FactsDao {
|
|
|
10237
10625
|
* row, and `settle_clock < c` is a seek + range scan of exactly the pending rows.
|
|
10238
10626
|
*/
|
|
10239
10627
|
static SETTLE_PREDICATE = "status = 'active' AND pinned = 0 AND settle_clock < CAST(:clock AS INTEGER)";
|
|
10240
|
-
/**
|
|
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
|
+
*/
|
|
10241
10654
|
settleBudgeted(p) {
|
|
10242
|
-
|
|
10243
|
-
SET trust_score = MAX(0, MIN(1, trust_score - :step * (:clock - settle_clock))),
|
|
10244
|
-
settle_clock = :clock
|
|
10655
|
+
const rows = this.db.prepare(`SELECT fact_id, trust_score, settle_clock FROM facts
|
|
10245
10656
|
WHERE ${FactsDao.SETTLE_PREDICATE}
|
|
10246
|
-
|
|
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);
|
|
10247
10662
|
}
|
|
10248
10663
|
/** ① materialize decay for every pending row (no budget). */
|
|
10249
10664
|
settleAll(p) {
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
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;
|
|
10254
10675
|
}
|
|
10255
10676
|
/** Rows still awaiting ① (the budget ran out). */
|
|
10256
10677
|
countPendingSettle(p) {
|
|
10257
10678
|
return Number(this.db.prepare(`SELECT COUNT(*) AS n FROM facts WHERE ${FactsDao.SETTLE_PREDICATE}`).get(p)?.n ?? 0);
|
|
10258
10679
|
}
|
|
10259
10680
|
/**
|
|
10260
|
-
* ②
|
|
10261
|
-
*
|
|
10262
|
-
* `budget` (0 = none) caps how many rows ONE tick may archive: the tick is a single IMMEDIATE
|
|
10263
|
-
* transaction shared by several processes, so an unbudgeted pass over a large backlog held the
|
|
10264
|
-
* write lock for as long as it took (measured 14400 rows in one tick). The remainder stays due
|
|
10265
|
-
* and is reported as a deferred count, then archived by the next tick.
|
|
10266
|
-
*/
|
|
10267
|
-
archiveExpiredByTtl(p) {
|
|
10268
|
-
return this.db.prepare(`UPDATE facts
|
|
10269
|
-
SET status = 'archived', archived_at = CURRENT_TIMESTAMP, archived_clock = :clock, archive_reason = 'ttl'
|
|
10270
|
-
WHERE status = 'active' AND ttl_days > 0
|
|
10271
|
-
AND julianday('now') - julianday(created_at) > ttl_days
|
|
10272
|
-
AND (:budget = 0 OR fact_id IN (
|
|
10273
|
-
SELECT fact_id FROM facts WHERE status = 'active' AND ttl_days > 0
|
|
10274
|
-
AND julianday('now') - julianday(created_at) > ttl_days LIMIT :budget))
|
|
10275
|
-
RETURNING fact_id`).all({
|
|
10276
|
-
clock: p.clock,
|
|
10277
|
-
budget: p.budget ?? 0
|
|
10278
|
-
}).map((row) => row.fact_id);
|
|
10279
|
-
}
|
|
10280
|
-
/**
|
|
10281
|
-
* ③ trust-driven forgetting (① already clamped trust to the threshold).
|
|
10681
|
+
* ② trust-driven forgetting (① already clamped trust to the threshold).
|
|
10282
10682
|
*
|
|
10283
10683
|
* Sargable as written (`status`/`pinned` equality + `trust_score <= ?`), and served by
|
|
10284
10684
|
* `idx_facts_forget(status, pinned, trust_score)`: before that index this statement read every
|
|
@@ -10287,9 +10687,9 @@ var FactsDao = class FactsDao {
|
|
|
10287
10687
|
archiveForgotten(p) {
|
|
10288
10688
|
return this.db.prepare(`UPDATE facts
|
|
10289
10689
|
SET status = 'archived', archived_at = CURRENT_TIMESTAMP, archived_clock = :clock, archive_reason = 'forgot'
|
|
10290
|
-
WHERE
|
|
10690
|
+
WHERE ${FactsDao.FORGET_PREDICATE}
|
|
10291
10691
|
AND (:budget = 0 OR fact_id IN (
|
|
10292
|
-
SELECT fact_id FROM facts WHERE
|
|
10692
|
+
SELECT fact_id FROM facts WHERE ${FactsDao.FORGET_PREDICATE} LIMIT :budget))
|
|
10293
10693
|
RETURNING fact_id`).all({
|
|
10294
10694
|
clock: p.clock,
|
|
10295
10695
|
forgetThreshold: p.forgetThreshold,
|
|
@@ -10297,29 +10697,7 @@ var FactsDao = class FactsDao {
|
|
|
10297
10697
|
}).map((row) => row.fact_id);
|
|
10298
10698
|
}
|
|
10299
10699
|
/**
|
|
10300
|
-
*
|
|
10301
|
-
*
|
|
10302
|
-
* The comparison is written as `<expression> < julianday('now', :modifier)` — the same
|
|
10303
|
-
* expression `idx_facts_idle_cutoff` indexes — so it is a range seek. The old form
|
|
10304
|
-
* (`julianday('now') - <expression> > :days`) called `julianday` on EVERY active row and could
|
|
10305
|
-
* not use any index (measured 33–212 ms at 180k).
|
|
10306
|
-
*/
|
|
10307
|
-
archiveIdle(p) {
|
|
10308
|
-
return this.db.prepare(`UPDATE facts
|
|
10309
|
-
SET status = 'archived', archived_at = CURRENT_TIMESTAMP, archived_clock = :clock, archive_reason = 'idle'
|
|
10310
|
-
WHERE status = 'active' AND pinned = 0
|
|
10311
|
-
AND julianday(COALESCE(last_retrieved_at, created_at)) < julianday('now', :idleModifier)
|
|
10312
|
-
AND (:budget = 0 OR fact_id IN (
|
|
10313
|
-
SELECT fact_id FROM facts WHERE status = 'active' AND pinned = 0
|
|
10314
|
-
AND julianday(COALESCE(last_retrieved_at, created_at)) < julianday('now', :idleModifier) LIMIT :budget))
|
|
10315
|
-
RETURNING fact_id`).all({
|
|
10316
|
-
clock: p.clock,
|
|
10317
|
-
idleModifier: `-${p.idleCalendarDays} days`,
|
|
10318
|
-
budget: p.budget ?? 0
|
|
10319
|
-
}).map((row) => row.fact_id);
|
|
10320
|
-
}
|
|
10321
|
-
/**
|
|
10322
|
-
* ⑤ physical delete of archived rows past the retention window.
|
|
10700
|
+
* ③ physical delete of archived rows past the retention window.
|
|
10323
10701
|
*
|
|
10324
10702
|
* TWO statements in this order, sharing one predicate:
|
|
10325
10703
|
*
|
|
@@ -10328,7 +10706,7 @@ var FactsDao = class FactsDao {
|
|
|
10328
10706
|
* DELETE below failed with `SQLITE_CONSTRAINT_FOREIGNKEY` whenever a revision chain
|
|
10329
10707
|
* crossed `purge_after_archived_days`. Because the whole tick is ONE transaction and it
|
|
10330
10708
|
* runs from the store's constructor, that failure did not merely skip the purge: it rolled
|
|
10331
|
-
* back settle/
|
|
10709
|
+
* back settle/forget with it and left the process unable to start, permanently.
|
|
10332
10710
|
* 2. delete them.
|
|
10333
10711
|
*
|
|
10334
10712
|
* The predicate is built once and interpolated into both statements, so "which rows are
|
|
@@ -10373,7 +10751,7 @@ var FactsDao = class FactsDao {
|
|
|
10373
10751
|
purgeModifier: `-${p.purgeAfterDays} days`
|
|
10374
10752
|
};
|
|
10375
10753
|
}
|
|
10376
|
-
/** Archived rows still due for
|
|
10754
|
+
/** Archived rows still due for ③ after a budgeted pass (0 when the pass finished them). */
|
|
10377
10755
|
countPurgeBacklog(p) {
|
|
10378
10756
|
const [a, b] = FactsDao.purgeBranches(p);
|
|
10379
10757
|
return Number(this.db.prepare(`SELECT COUNT(*) AS n FROM (
|
|
@@ -11470,156 +11848,6 @@ var FactSourcesDao = class {
|
|
|
11470
11848
|
}
|
|
11471
11849
|
};
|
|
11472
11850
|
//#endregion
|
|
11473
|
-
//#region ../core/lib/lifecycle/trust.js
|
|
11474
|
-
const DAY_MS$1 = 864e5;
|
|
11475
|
-
/** The reinforcement quota window is a CALENDAR day ("每天" is a calendar notion, §2.3). */
|
|
11476
|
-
const QUOTA_WINDOW_MS = DAY_MS$1;
|
|
11477
|
-
function clamp01(v) {
|
|
11478
|
-
if (!Number.isFinite(v)) return 0;
|
|
11479
|
-
return Math.min(1, Math.max(0, v));
|
|
11480
|
-
}
|
|
11481
|
-
function isPinned(row) {
|
|
11482
|
-
return Number(row.pinned) === 1;
|
|
11483
|
-
}
|
|
11484
|
-
/**
|
|
11485
|
-
* `'YYYY-MM-DD HH:MM:SS'` (SQLite UTC) or ISO → epoch ms; null/garbage → null.
|
|
11486
|
-
*
|
|
11487
|
-
* A stamp with NO zone designator is read as UTC, not as local time. `Date.parse` treats a zone-less
|
|
11488
|
-
* ISO string as LOCAL, and everything in this store is written in UTC (`CURRENT_TIMESTAMP`, and
|
|
11489
|
-
* {@link formatUtcTs}), so handing one over as-is would shift every quota window and every decay
|
|
11490
|
-
* measurement by the host's UTC offset — a fact reinforced at 23:00 UTC+8 would look reinforced 8
|
|
11491
|
-
* hours earlier, and the 24h cap would open or close at the wrong moment. An explicit `Z` or
|
|
11492
|
-
* `±HH:MM` is left alone.
|
|
11493
|
-
*/
|
|
11494
|
-
function parseUtcTs(ts) {
|
|
11495
|
-
if (ts === null || ts === void 0 || ts === "") return null;
|
|
11496
|
-
const iso = ts.includes("T") ? ts : ts.replace(" ", "T");
|
|
11497
|
-
const zoned = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(iso) ? iso : `${iso}Z`;
|
|
11498
|
-
const ms = Date.parse(zoned);
|
|
11499
|
-
return Number.isFinite(ms) ? ms : null;
|
|
11500
|
-
}
|
|
11501
|
-
/** Epoch ms → `'YYYY-MM-DD HH:MM:SS'` (the SQLite `CURRENT_TIMESTAMP` shape, UTC). */
|
|
11502
|
-
function formatUtcTs(ms) {
|
|
11503
|
-
return new Date(ms).toISOString().slice(0, 19).replace("T", " ");
|
|
11504
|
-
}
|
|
11505
|
-
/** eff(f) — the trust a caller should act on right now (spec §2.2). */
|
|
11506
|
-
function effectiveTrust(row, clock, cfg, enabled = cfg.enabled) {
|
|
11507
|
-
if (!enabled) return clamp01(row.trust_score);
|
|
11508
|
-
if (isPinned(row)) return 1;
|
|
11509
|
-
return clamp01(row.trust_score - cfg.decay_per_day * (clock - row.settle_clock));
|
|
11510
|
-
}
|
|
11511
|
-
/**
|
|
11512
|
-
* What a caller should SEE for a row — the R11 display rule (spec §6):
|
|
11513
|
-
* `active` → `eff()`; **non-active → the stored value verbatim** (the decay
|
|
11514
|
-
* formula keeps running mathematically for archived rows, so the projection must
|
|
11515
|
-
* not use it), and `enabled=false` → stored value (D12).
|
|
11516
|
-
*/
|
|
11517
|
-
function displayTrust(row, clock, cfg, enabled = cfg.enabled) {
|
|
11518
|
-
if (row.status !== "active") return clamp01(row.trust_score);
|
|
11519
|
-
return effectiveTrust(row, clock, cfg, enabled);
|
|
11520
|
-
}
|
|
11521
|
-
/**
|
|
11522
|
-
* Remaining active days: `eff / step`. `null` for pinned rows, non-active rows and
|
|
11523
|
-
* when trust is disabled — the contract's `remaining_days` (spec §6 / R11).
|
|
11524
|
-
*/
|
|
11525
|
-
function remainingDays(row, clock, cfg, enabled = cfg.enabled) {
|
|
11526
|
-
if (row.status !== "active" || isPinned(row) || !enabled || cfg.decay_per_day <= 0) return null;
|
|
11527
|
-
return effectiveTrust(row, clock, cfg, enabled) / cfg.decay_per_day;
|
|
11528
|
-
}
|
|
11529
|
-
function rollWindow(row, nowMs) {
|
|
11530
|
-
const at = row.bonus_window_at;
|
|
11531
|
-
const started = parseUtcTs(at);
|
|
11532
|
-
if (at === null || started === null || nowMs - started >= QUOTA_WINDOW_MS) return {
|
|
11533
|
-
count: 0,
|
|
11534
|
-
at: formatUtcTs(nowMs),
|
|
11535
|
-
reset: true
|
|
11536
|
-
};
|
|
11537
|
-
return {
|
|
11538
|
-
count: row.bonus_count ?? 0,
|
|
11539
|
-
at,
|
|
11540
|
-
reset: false
|
|
11541
|
-
};
|
|
11542
|
-
}
|
|
11543
|
-
/**
|
|
11544
|
-
* Apply one recall to a fact: settle, then grant a bonus inside the daily quota.
|
|
11545
|
-
* Returns `next === settled` when the quota is exhausted, the ceiling is reached,
|
|
11546
|
-
* the fact is pinned, or the gain would be zero (R2/R10/D11).
|
|
11547
|
-
*/
|
|
11548
|
-
function grantRecallBonus(row, clock, nowMs, cfg, enabled = cfg.enabled) {
|
|
11549
|
-
const settled = effectiveTrust(row, clock, cfg, enabled);
|
|
11550
|
-
const window = rollWindow(row, nowMs);
|
|
11551
|
-
if (row.status !== "active" || isPinned(row) || !enabled || window.count >= cfg.recall_daily_cap) return {
|
|
11552
|
-
settled,
|
|
11553
|
-
next: settled,
|
|
11554
|
-
granted: false,
|
|
11555
|
-
bonusCount: window.count,
|
|
11556
|
-
windowAt: window.at
|
|
11557
|
-
};
|
|
11558
|
-
const gain = cfg.recall_delta * Math.pow(cfg.recall_marginal_decay, window.count);
|
|
11559
|
-
const next = settled <= cfg.recall_floor ? Math.max(settled, cfg.recall_floor) : Math.max(settled, Math.min(cfg.recall_ceiling, settled + gain));
|
|
11560
|
-
if (next <= settled + 1e-9) return {
|
|
11561
|
-
settled,
|
|
11562
|
-
next: settled,
|
|
11563
|
-
granted: false,
|
|
11564
|
-
bonusCount: window.count,
|
|
11565
|
-
windowAt: window.at
|
|
11566
|
-
};
|
|
11567
|
-
return {
|
|
11568
|
-
settled,
|
|
11569
|
-
next,
|
|
11570
|
-
granted: true,
|
|
11571
|
-
bonusCount: window.count + 1,
|
|
11572
|
-
windowAt: window.at
|
|
11573
|
-
};
|
|
11574
|
-
}
|
|
11575
|
-
/**
|
|
11576
|
-
* Apply one explicit `helpful` (+1) / `unhelpful` (-1) step.
|
|
11577
|
-
*
|
|
11578
|
-
* Pinned and archived rows are untouched (R7): only `helpful_count` changes for
|
|
11579
|
-
* them, so a pinned fact can never end up storing 0.95 while displaying 1.0.
|
|
11580
|
-
* When `feedback_daily_cap > 0` feedback shares the per-fact 24h window counter.
|
|
11581
|
-
*/
|
|
11582
|
-
function applyFeedbackDelta(row, clock, delta, nowMs, cfg, enabled = cfg.enabled) {
|
|
11583
|
-
const settled = effectiveTrust(row, clock, cfg, enabled);
|
|
11584
|
-
const shared = cfg.feedback_daily_cap > 0;
|
|
11585
|
-
const window = shared ? rollWindow(row, nowMs) : {
|
|
11586
|
-
count: row.bonus_count ?? 0,
|
|
11587
|
-
at: row.bonus_window_at ?? formatUtcTs(nowMs),
|
|
11588
|
-
reset: false
|
|
11589
|
-
};
|
|
11590
|
-
if (row.status !== "active" || isPinned(row) || !enabled) return {
|
|
11591
|
-
settled,
|
|
11592
|
-
next: row.trust_score,
|
|
11593
|
-
pin: false,
|
|
11594
|
-
forget: false,
|
|
11595
|
-
untouched: true,
|
|
11596
|
-
bonusCount: window.count,
|
|
11597
|
-
windowAt: window.at
|
|
11598
|
-
};
|
|
11599
|
-
if (shared && window.count >= cfg.feedback_daily_cap) return {
|
|
11600
|
-
settled,
|
|
11601
|
-
next: settled,
|
|
11602
|
-
pin: false,
|
|
11603
|
-
forget: false,
|
|
11604
|
-
untouched: false,
|
|
11605
|
-
bonusCount: window.count,
|
|
11606
|
-
windowAt: window.at
|
|
11607
|
-
};
|
|
11608
|
-
let next = clamp01(settled + delta * cfg.feedback_delta);
|
|
11609
|
-
const pin = next >= cfg.permanent_threshold;
|
|
11610
|
-
const forget = !pin && next <= cfg.forget_threshold + 1e-9;
|
|
11611
|
-
if (forget) next = cfg.forget_threshold;
|
|
11612
|
-
return {
|
|
11613
|
-
settled,
|
|
11614
|
-
next,
|
|
11615
|
-
pin,
|
|
11616
|
-
forget,
|
|
11617
|
-
untouched: false,
|
|
11618
|
-
bonusCount: shared ? window.count + 1 : window.count,
|
|
11619
|
-
windowAt: window.at
|
|
11620
|
-
};
|
|
11621
|
-
}
|
|
11622
|
-
//#endregion
|
|
11623
11851
|
//#region ../core/lib/lifecycle/presence.js
|
|
11624
11852
|
const CLOCK_KEY = "trust_clock";
|
|
11625
11853
|
const LAST_SEEN_KEY = "trust_last_seen";
|
|
@@ -11667,39 +11895,27 @@ function advancePresence(db, opts) {
|
|
|
11667
11895
|
function runTrustTick(db, cfg, opts) {
|
|
11668
11896
|
const clock = opts.clock;
|
|
11669
11897
|
const budget = opts.budget ?? cfg.trust.tick_max_facts;
|
|
11670
|
-
const
|
|
11898
|
+
const curve = trustCurveOf(cfg.trust);
|
|
11671
11899
|
const enabled = cfg.trust.enabled ? 1 : 0;
|
|
11672
11900
|
const trustEnabled = cfg.trust.enabled;
|
|
11673
11901
|
const facts = new FactsDao(db);
|
|
11674
11902
|
return db.transaction(() => {
|
|
11675
11903
|
const settled = !trustEnabled ? 0 : budget > 0 ? facts.settleBudgeted({
|
|
11676
|
-
|
|
11904
|
+
curve,
|
|
11677
11905
|
clock,
|
|
11678
11906
|
budget
|
|
11679
11907
|
}) : facts.settleAll({
|
|
11680
|
-
|
|
11908
|
+
curve,
|
|
11681
11909
|
clock
|
|
11682
11910
|
});
|
|
11683
11911
|
const skipped = trustEnabled && budget > 0 ? facts.countPendingSettle({ clock }) : 0;
|
|
11684
11912
|
const archiveBudget = budget > 0 ? budget : 0;
|
|
11685
|
-
const ttlIds = facts.archiveExpiredByTtl({
|
|
11686
|
-
clock,
|
|
11687
|
-
budget: archiveBudget
|
|
11688
|
-
});
|
|
11689
11913
|
const forgotIds = trustEnabled ? facts.archiveForgotten({
|
|
11690
11914
|
clock,
|
|
11691
11915
|
forgetThreshold: cfg.trust.forget_threshold,
|
|
11692
11916
|
budget: archiveBudget
|
|
11693
11917
|
}) : [];
|
|
11694
|
-
const
|
|
11695
|
-
clock,
|
|
11696
|
-
idleCalendarDays: cfg.trust.idle_calendar_days,
|
|
11697
|
-
budget: archiveBudget
|
|
11698
|
-
});
|
|
11699
|
-
const deferred = archiveBudget > 0 && (ttlIds.length === archiveBudget || forgotIds.length === archiveBudget || idleIds.length === archiveBudget) ? facts.countArchiveBacklog({
|
|
11700
|
-
forgetThreshold: cfg.trust.forget_threshold,
|
|
11701
|
-
idleCalendarDays: cfg.trust.idle_calendar_days
|
|
11702
|
-
}) : 0;
|
|
11918
|
+
const deferred = archiveBudget > 0 && forgotIds.length === archiveBudget ? facts.countArchiveBacklog({ forgetThreshold: cfg.trust.forget_threshold }) : 0;
|
|
11703
11919
|
const purgeArgs = {
|
|
11704
11920
|
clock,
|
|
11705
11921
|
enabled,
|
|
@@ -11714,16 +11930,10 @@ function runTrustTick(db, cfg, opts) {
|
|
|
11714
11930
|
return {
|
|
11715
11931
|
clock,
|
|
11716
11932
|
settled,
|
|
11717
|
-
archived_ttl: ttlIds.length,
|
|
11718
11933
|
archived_forgot: forgotIds.length,
|
|
11719
|
-
archived_idle: idleIds.length,
|
|
11720
11934
|
purged: purgedIds.length,
|
|
11721
11935
|
purged_ids: purgedIds,
|
|
11722
|
-
archived_ids: [
|
|
11723
|
-
...ttlIds,
|
|
11724
|
-
...forgotIds,
|
|
11725
|
-
...idleIds
|
|
11726
|
-
],
|
|
11936
|
+
archived_ids: [...forgotIds],
|
|
11727
11937
|
skipped,
|
|
11728
11938
|
archived_deferred: deferred,
|
|
11729
11939
|
purged_deferred: purgedDeferred
|
|
@@ -11747,15 +11957,15 @@ function runMaintenance(db, cfg, opts) {
|
|
|
11747
11957
|
});
|
|
11748
11958
|
return {
|
|
11749
11959
|
decayed: tick.settled,
|
|
11750
|
-
archived_ttl:
|
|
11751
|
-
archived_age: tick.archived_forgot
|
|
11960
|
+
archived_ttl: 0,
|
|
11961
|
+
archived_age: tick.archived_forgot,
|
|
11752
11962
|
purged: tick.purged,
|
|
11753
11963
|
purged_ids: tick.purged_ids,
|
|
11754
11964
|
archived_ids: tick.archived_ids,
|
|
11755
11965
|
settled: tick.settled,
|
|
11756
11966
|
clock: tick.clock,
|
|
11757
11967
|
archived_forgot: tick.archived_forgot,
|
|
11758
|
-
archived_idle:
|
|
11968
|
+
archived_idle: 0,
|
|
11759
11969
|
skipped: tick.skipped,
|
|
11760
11970
|
archived_deferred: tick.archived_deferred,
|
|
11761
11971
|
purged_deferred: tick.purged_deferred
|
|
@@ -12154,7 +12364,6 @@ const RESTORE_METADATA_FIELDS = [
|
|
|
12154
12364
|
"last_reinforced_at",
|
|
12155
12365
|
"ttl_days",
|
|
12156
12366
|
"valid_from",
|
|
12157
|
-
"valid_to",
|
|
12158
12367
|
"supersedes_source_id",
|
|
12159
12368
|
"archive",
|
|
12160
12369
|
"sources",
|
|
@@ -12300,9 +12509,7 @@ function rowOf(raw, metadataMissing, nowSql) {
|
|
|
12300
12509
|
lastRetrievedAt: textOrNull$1(row["last_retrieved_at"]),
|
|
12301
12510
|
ttlDays: Math.max(0, integerOrNull(row["ttl_days"]) ?? 0),
|
|
12302
12511
|
validFrom: textOrNull$1(row["valid_from"]),
|
|
12303
|
-
validTo: textOrNull$1(row["valid_to"]),
|
|
12304
12512
|
validFromMs: eventTimeMs(textOrNull$1(row["valid_from"])) ?? null,
|
|
12305
|
-
validToMs: eventTimeMs(textOrNull$1(row["valid_to"])) ?? null,
|
|
12306
12513
|
supersedesId: integerOrNull(row["supersedes_source_id"]),
|
|
12307
12514
|
mirrorSource: textOrNull$1(row["mirror_source"]),
|
|
12308
12515
|
mirrorTarget: textOrNull$1(row["mirror_target"]),
|
|
@@ -12345,10 +12552,11 @@ const INSERT_FACT_SQL = `INSERT INTO facts (
|
|
|
12345
12552
|
fact_id, content, kind, title, legacy_category, status, archived_at, archive_reason,
|
|
12346
12553
|
created_at, updated_at, trust_score, settle_clock, assert_count, helpful_count,
|
|
12347
12554
|
pinned, pinned_at, last_reinforced_at, bonus_count, bonus_window_at,
|
|
12348
|
-
retrieval_count, last_retrieved_at, ttl_days, valid_from,
|
|
12349
|
-
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,
|
|
12350
12557
|
hrr_vector, semantic_vector, embedding_model, entities_version, conflict_checked
|
|
12351
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
12558
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?,
|
|
12559
|
+
NULL, NULL, NULL, 0, 1)`;
|
|
12352
12560
|
function countOf(db, sql) {
|
|
12353
12561
|
return Number(db.prepare(sql).get()?.n ?? 0);
|
|
12354
12562
|
}
|
|
@@ -12399,7 +12607,7 @@ async function restoreFactsDocument(target, doc, options = {}) {
|
|
|
12399
12607
|
const insertSource = target.db.prepare("INSERT INTO fact_sources (fact_id, kind, ref) VALUES (?, ?, ?)");
|
|
12400
12608
|
for (const row of rows) {
|
|
12401
12609
|
try {
|
|
12402
|
-
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);
|
|
12403
12611
|
for (const source of row.sources) insertSource.run(row.factId, source.kind, source.ref);
|
|
12404
12612
|
if (row.relations.length > 0) {
|
|
12405
12613
|
const report = writeExplicitEdgesForFact(target.db, row.factId, row.relations);
|
|
@@ -12497,9 +12705,6 @@ var SqliteMemoryRepository = class {
|
|
|
12497
12705
|
countForgettingWithin(p) {
|
|
12498
12706
|
return this.dao.countForgettingWithin(p);
|
|
12499
12707
|
}
|
|
12500
|
-
countIdleCandidates(days) {
|
|
12501
|
-
return this.dao.countIdleCandidates(days);
|
|
12502
|
-
}
|
|
12503
12708
|
countArchiveBacklog(p) {
|
|
12504
12709
|
return this.dao.countArchiveBacklog(p);
|
|
12505
12710
|
}
|
|
@@ -12582,11 +12787,11 @@ var SqliteMemoryRepository = class {
|
|
|
12582
12787
|
factId: Number(existing.fact_id)
|
|
12583
12788
|
};
|
|
12584
12789
|
}
|
|
12585
|
-
applyValidity(factId, validFrom
|
|
12586
|
-
this.dao.applyValidity(factId, validFrom
|
|
12790
|
+
applyValidity(factId, validFrom) {
|
|
12791
|
+
this.dao.applyValidity(factId, validFrom);
|
|
12587
12792
|
}
|
|
12588
|
-
applyValues(factId, kind
|
|
12589
|
-
this.dao.applyValues(factId, kind
|
|
12793
|
+
applyValues(factId, kind) {
|
|
12794
|
+
this.dao.applyValues(factId, kind);
|
|
12590
12795
|
}
|
|
12591
12796
|
setTitle(factId, title) {
|
|
12592
12797
|
this.dao.setTitle(factId, title);
|
|
@@ -12603,17 +12808,14 @@ var SqliteMemoryRepository = class {
|
|
|
12603
12808
|
incrementAssertCount(factId) {
|
|
12604
12809
|
return this.dao.incrementAssertCount(factId);
|
|
12605
12810
|
}
|
|
12606
|
-
markValidTo(factId, at, atMs) {
|
|
12607
|
-
this.dao.markValidTo(factId, at, atMs);
|
|
12608
|
-
}
|
|
12609
12811
|
linkSupersede(factId, supersedesId) {
|
|
12610
12812
|
this.dao.linkSupersede(factId, supersedesId);
|
|
12611
12813
|
}
|
|
12612
12814
|
archiveSuperseded(factId, clock) {
|
|
12613
12815
|
return this.dao.archiveSuperseded(factId, clock);
|
|
12614
12816
|
}
|
|
12615
|
-
revive(factId, kind,
|
|
12616
|
-
return this.dao.revive(factId, kind,
|
|
12817
|
+
revive(factId, kind, trust, clock) {
|
|
12818
|
+
return this.dao.revive(factId, kind, trust, clock);
|
|
12617
12819
|
}
|
|
12618
12820
|
archive(factId, clock, reason) {
|
|
12619
12821
|
return this.dao.archive(factId, clock, reason);
|
|
@@ -12621,24 +12823,12 @@ var SqliteMemoryRepository = class {
|
|
|
12621
12823
|
restore(factId, trust, clock) {
|
|
12622
12824
|
return this.dao.restore(factId, trust, clock);
|
|
12623
12825
|
}
|
|
12624
|
-
bumpHelpful(factId, delta) {
|
|
12625
|
-
this.dao.bumpHelpful(factId, delta);
|
|
12626
|
-
}
|
|
12627
12826
|
pin(factId, clock) {
|
|
12628
12827
|
return this.dao.pin(factId, clock);
|
|
12629
12828
|
}
|
|
12630
12829
|
unpin(factId, clock) {
|
|
12631
12830
|
return this.dao.unpin(factId, clock);
|
|
12632
12831
|
}
|
|
12633
|
-
pinOutcome(factId, clock, bonusCount, windowAt) {
|
|
12634
|
-
this.dao.pinOutcome(factId, clock, bonusCount, windowAt);
|
|
12635
|
-
}
|
|
12636
|
-
forgetOutcome(factId, trust, clock, bonusCount, windowAt) {
|
|
12637
|
-
this.dao.forgetOutcome(factId, trust, clock, bonusCount, windowAt);
|
|
12638
|
-
}
|
|
12639
|
-
reinforceOutcome(factId, trust, clock, bonusCount, windowAt) {
|
|
12640
|
-
this.dao.reinforceOutcome(factId, trust, clock, bonusCount, windowAt);
|
|
12641
|
-
}
|
|
12642
12832
|
touchUsage(ids) {
|
|
12643
12833
|
this.dao.touchUsage(ids);
|
|
12644
12834
|
}
|
|
@@ -16289,6 +16479,22 @@ const NO_CHECK = {
|
|
|
16289
16479
|
*/
|
|
16290
16480
|
const RETRIEVAL_HEALTH_KEY = "retrieval_health";
|
|
16291
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
|
+
/**
|
|
16292
16498
|
* The ONE stored-title-row → `TitleCandidate` mapper, shared by the exact page and the fuzzy
|
|
16293
16499
|
* shortlist so a candidate cannot describe the same row two ways.
|
|
16294
16500
|
*
|
|
@@ -16306,8 +16512,8 @@ function titleCandidatesOf(rows, includeExcerpt) {
|
|
|
16306
16512
|
matched_title: row.matched_title === null || row.matched_title === void 0 ? String(row.title) : String(row.matched_title),
|
|
16307
16513
|
memory_kind: mapLegacyKind(row.kind),
|
|
16308
16514
|
status: row.status === "archived" ? "archived" : "active",
|
|
16309
|
-
created_at: row.created_at
|
|
16310
|
-
updated_at:
|
|
16515
|
+
created_at: modelInstant(row.created_at) ?? "",
|
|
16516
|
+
updated_at: modelInstant(row.updated_at),
|
|
16311
16517
|
...row.valid_from === null || row.valid_from === void 0 ? {} : { event_date: String(row.valid_from) },
|
|
16312
16518
|
...includeExcerpt && row.excerpt !== null ? { excerpt: String(row.excerpt) } : {}
|
|
16313
16519
|
}));
|
|
@@ -16334,8 +16540,8 @@ function factEvidenceOf(row) {
|
|
|
16334
16540
|
status: row["status"] === "archived" ? "archived" : "active",
|
|
16335
16541
|
...title === null || title === void 0 || title === "" ? {} : { title: String(title) },
|
|
16336
16542
|
...row["valid_from"] === null || row["valid_from"] === void 0 ? {} : { event_date: String(row["valid_from"]) },
|
|
16337
|
-
created_at: row["created_at"]
|
|
16338
|
-
updated_at:
|
|
16543
|
+
created_at: modelInstant(row["created_at"]) ?? "",
|
|
16544
|
+
updated_at: modelInstant(row["updated_at"])
|
|
16339
16545
|
};
|
|
16340
16546
|
}
|
|
16341
16547
|
var MemoryStore = class {
|
|
@@ -16487,7 +16693,7 @@ var MemoryStore = class {
|
|
|
16487
16693
|
dim: this.vstore.dim
|
|
16488
16694
|
});
|
|
16489
16695
|
}
|
|
16490
|
-
async add(content, kind,
|
|
16696
|
+
async add(content, kind, opts) {
|
|
16491
16697
|
const normalized = normalizeWrite(content).trim();
|
|
16492
16698
|
if (!normalized) throw new Error("内容不能为空");
|
|
16493
16699
|
const secret = detectSecretShape(normalized);
|
|
@@ -16500,7 +16706,7 @@ var MemoryStore = class {
|
|
|
16500
16706
|
if (checkedKind !== void 0 && requiresEventDate(checkedKind) && opts?.eventDate === void 0) throw new Error(JOURNAL_NEEDS_EVENT_DATE);
|
|
16501
16707
|
const explicitTriples = opts?.relations === void 0 ? void 0 : await this.relations.explicitRelationTriples(checkedKind ?? "note", opts.relations);
|
|
16502
16708
|
const title = this.resolveWriteTitle(opts?.title, checkedKind);
|
|
16503
|
-
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, {
|
|
16504
16710
|
...opts,
|
|
16505
16711
|
title,
|
|
16506
16712
|
relations: void 0
|
|
@@ -16591,7 +16797,7 @@ var MemoryStore = class {
|
|
|
16591
16797
|
}], this.vectorWriteTarget(this.vectorSpace()));
|
|
16592
16798
|
return encoded === 1;
|
|
16593
16799
|
}
|
|
16594
|
-
async update(fact_id, content, kind,
|
|
16800
|
+
async update(fact_id, content, kind, opts) {
|
|
16595
16801
|
const row = await this.getRow(fact_id);
|
|
16596
16802
|
if (!row) throw new Error(`找不到 fact_id=${fact_id}`);
|
|
16597
16803
|
const normalized = normalizeWrite(content).trim();
|
|
@@ -16607,7 +16813,7 @@ var MemoryStore = class {
|
|
|
16607
16813
|
if (requiresEventDate(effectiveKind) && opts?.eventDate === void 0 && row.valid_from == null) throw new Error(JOURNAL_NEEDS_EVENT_DATE);
|
|
16608
16814
|
const title = this.resolveWriteTitle(opts?.title, effectiveKind);
|
|
16609
16815
|
const explicitTriples = opts?.relations === void 0 ? void 0 : await this.relations.explicitRelationTriples(effectiveKind, opts.relations);
|
|
16610
|
-
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, {
|
|
16611
16817
|
...opts,
|
|
16612
16818
|
supersedesId: fact_id,
|
|
16613
16819
|
archiveOldIfActive: true,
|
|
@@ -16641,9 +16847,9 @@ var MemoryStore = class {
|
|
|
16641
16847
|
return changed > 0;
|
|
16642
16848
|
}
|
|
16643
16849
|
/**
|
|
16644
|
-
* Bring an archived fact back (spec §2.6): a `forgot` fact is lifted to
|
|
16645
|
-
* `
|
|
16646
|
-
*
|
|
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),
|
|
16647
16853
|
* and `settle_clock` moves to now so the downtime is not charged.
|
|
16648
16854
|
*/
|
|
16649
16855
|
async restore(fact_id) {
|
|
@@ -16652,7 +16858,7 @@ var MemoryStore = class {
|
|
|
16652
16858
|
const trust = this.config.trust;
|
|
16653
16859
|
const clock = await this.lifecycle.clock();
|
|
16654
16860
|
const stored = Number(row.trust_score);
|
|
16655
|
-
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;
|
|
16656
16862
|
if (await this.facts.restore(fact_id, next, clock) === 0) return false;
|
|
16657
16863
|
const vec = bytesToFloat32(row.semantic_vector ?? null);
|
|
16658
16864
|
if (vec && vec.length === this.vstore.dim) this.vstore.add(fact_id, vec);
|
|
@@ -16660,42 +16866,6 @@ var MemoryStore = class {
|
|
|
16660
16866
|
if ((await this.detectContradictions(fact_id)).complete) await this.facts.markConflictChecked([fact_id]);
|
|
16661
16867
|
return true;
|
|
16662
16868
|
}
|
|
16663
|
-
async helpful(fact_id) {
|
|
16664
|
-
return await this.applyFeedback(fact_id, 1);
|
|
16665
|
-
}
|
|
16666
|
-
async unhelpful(fact_id) {
|
|
16667
|
-
return await this.applyFeedback(fact_id, -1);
|
|
16668
|
-
}
|
|
16669
|
-
/**
|
|
16670
|
-
* Explicit feedback (spec §2.4). Pinned/archived rows only move `helpful_count`
|
|
16671
|
-
* (R7); otherwise the decay is settled first, then `±feedback_delta`, pin at the
|
|
16672
|
-
* permanent threshold (snap to 1.0) or archive immediately at the forget line.
|
|
16673
|
-
*/
|
|
16674
|
-
async applyFeedback(fact_id, delta) {
|
|
16675
|
-
if (await this.getRow(fact_id) === void 0) return null;
|
|
16676
|
-
const trust = this.config.trust;
|
|
16677
|
-
const evict = await this.lifecycle.immediateTransaction(async () => {
|
|
16678
|
-
const row = await this.getRow(fact_id);
|
|
16679
|
-
if (!row) return null;
|
|
16680
|
-
const clock = await this.lifecycle.clock();
|
|
16681
|
-
const outcome = applyFeedbackDelta(asTrustRow(row), clock, delta, Date.now(), trust);
|
|
16682
|
-
await this.facts.bumpHelpful(fact_id, delta);
|
|
16683
|
-
if (outcome.untouched) return null;
|
|
16684
|
-
if (outcome.pin) {
|
|
16685
|
-
await this.facts.pinOutcome(fact_id, clock, outcome.bonusCount, outcome.windowAt);
|
|
16686
|
-
return null;
|
|
16687
|
-
}
|
|
16688
|
-
if (outcome.forget) {
|
|
16689
|
-
await this.facts.forgetOutcome(fact_id, outcome.next, clock, outcome.bonusCount, outcome.windowAt);
|
|
16690
|
-
await this.contradictions.resolveForFact(fact_id);
|
|
16691
|
-
return fact_id;
|
|
16692
|
-
}
|
|
16693
|
-
await this.facts.reinforceOutcome(fact_id, outcome.next, clock, outcome.bonusCount, outcome.windowAt);
|
|
16694
|
-
return null;
|
|
16695
|
-
});
|
|
16696
|
-
if (evict !== null) evictVectors(this.vstore, [evict]);
|
|
16697
|
-
return (await this.getRow(fact_id))?.helpful_count ?? 0;
|
|
16698
|
-
}
|
|
16699
16869
|
/** Permanent-memory administration (spec §6). */
|
|
16700
16870
|
async pin(fact_id) {
|
|
16701
16871
|
return await this.facts.pin(fact_id, await this.lifecycle.clock()) > 0;
|
|
@@ -16713,15 +16883,15 @@ var MemoryStore = class {
|
|
|
16713
16883
|
const clock = await this.lifecycle.clock();
|
|
16714
16884
|
const active = await this.facts.countActive();
|
|
16715
16885
|
const pinned = await this.facts.countPinnedActive();
|
|
16716
|
-
const
|
|
16717
|
-
|
|
16886
|
+
const curve = curveOf(trust);
|
|
16887
|
+
const forgettingSoon = await this.facts.countForgettingWithin({
|
|
16888
|
+
threshold: ceilingForDays(FORGETTING_SOON_DAYS, curve),
|
|
16718
16889
|
clock,
|
|
16719
|
-
|
|
16720
|
-
})
|
|
16890
|
+
curve
|
|
16891
|
+
});
|
|
16721
16892
|
const bonus = await this.facts.bonusStatedToday();
|
|
16722
16893
|
const reinforcedToday = bonus.count;
|
|
16723
16894
|
const grantedToday = bonus.granted;
|
|
16724
|
-
const idleCandidates = await this.facts.countIdleCandidates(trust.idle_calendar_days);
|
|
16725
16895
|
const reasons = await this.facts.archivedByReason();
|
|
16726
16896
|
const oldest = await this.facts.minSettleClock();
|
|
16727
16897
|
return {
|
|
@@ -16732,7 +16902,6 @@ var MemoryStore = class {
|
|
|
16732
16902
|
forgetting_soon: forgettingSoon,
|
|
16733
16903
|
reinforced_today: reinforcedToday,
|
|
16734
16904
|
bonus_granted_today: grantedToday,
|
|
16735
|
-
idle_candidates: idleCandidates,
|
|
16736
16905
|
conflict_pending: await this.facts.countPendingConflicts(),
|
|
16737
16906
|
entities_stale: await this.facts.countStaleEntities(3),
|
|
16738
16907
|
archived_by_reason: Object.fromEntries(reasons.map((r) => [r.reason ?? "unknown", r.n])),
|
|
@@ -16895,9 +17064,18 @@ var MemoryStore = class {
|
|
|
16895
17064
|
liveVectorCount() {
|
|
16896
17065
|
return this.vstore.count();
|
|
16897
17066
|
}
|
|
16898
|
-
/**
|
|
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
|
+
*/
|
|
16899
17074
|
async listContradictions(opts) {
|
|
16900
|
-
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
|
+
}));
|
|
16901
17079
|
}
|
|
16902
17080
|
/**
|
|
16903
17081
|
* Adjudicate one logged conflict — the outlet the detector never had.
|
|
@@ -16943,7 +17121,6 @@ var MemoryStore = class {
|
|
|
16943
17121
|
reason: "loser_not_in_pair"
|
|
16944
17122
|
};
|
|
16945
17123
|
await this.archive(loserFactId, "contradiction");
|
|
16946
|
-
await this.facts.markValidTo(loserFactId, formatUtcTs(Date.now()), Date.now());
|
|
16947
17124
|
await this.contradictions.resolve(id, resolution, loserFactId);
|
|
16948
17125
|
return {
|
|
16949
17126
|
resolved: true,
|
|
@@ -17330,13 +17507,21 @@ var MemoryStore = class {
|
|
|
17330
17507
|
* ({@link timePage}, `mode:"time"`): the window is the whole question, so there is nothing to
|
|
17331
17508
|
* match, plan or rank.
|
|
17332
17509
|
*
|
|
17333
|
-
*
|
|
17334
|
-
*
|
|
17335
|
-
*
|
|
17336
|
-
*
|
|
17337
|
-
*
|
|
17338
|
-
*
|
|
17339
|
-
*
|
|
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}).
|
|
17340
17525
|
*
|
|
17341
17526
|
* It does NOT emit a retrieval-health event. Those counters describe the retrieval PIPELINE
|
|
17342
17527
|
* (legs, fusion, semantic-live rate); none of an exact, facts, relation or time answer ran a leg,
|
|
@@ -17394,8 +17579,9 @@ var MemoryStore = class {
|
|
|
17394
17579
|
* list", and the budget stays a knob for a RANKED result (exact/facts/relation/hybrid unchanged).
|
|
17395
17580
|
* `max_tokens` (including the contract's `0` = unlimited) is passed through unchanged.
|
|
17396
17581
|
*
|
|
17397
|
-
* It honours `kind`/`source`, and
|
|
17398
|
-
* the caller
|
|
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.
|
|
17399
17585
|
*/
|
|
17400
17586
|
async timePage(input) {
|
|
17401
17587
|
if (input.time === void 0) throw new Error("mem_recall.search 需要 `query`,或只给 `time`(纯时间召回);两者都没有时无法检索。");
|
|
@@ -17415,7 +17601,7 @@ var MemoryStore = class {
|
|
|
17415
17601
|
offset
|
|
17416
17602
|
});
|
|
17417
17603
|
const { kept } = applyOutputBudget(this.config, await this.factHits(ids, () => 1), input.maxTokens ?? 0);
|
|
17418
|
-
if (input.track !== false) await this.
|
|
17604
|
+
if (input.track !== false) await this.touchOnly(kept.map((hit) => hit.ref_id));
|
|
17419
17605
|
return {
|
|
17420
17606
|
mode: "time",
|
|
17421
17607
|
total,
|
|
@@ -17453,7 +17639,7 @@ var MemoryStore = class {
|
|
|
17453
17639
|
offset
|
|
17454
17640
|
});
|
|
17455
17641
|
const { kept } = applyOutputBudget(this.config, await this.factHits(ids, () => 1), input.maxTokens);
|
|
17456
|
-
if (input.track !== false) await this.
|
|
17642
|
+
if (input.track !== false) await this.touchOnly(kept.map((hit) => hit.ref_id));
|
|
17457
17643
|
return {
|
|
17458
17644
|
mode: "exact",
|
|
17459
17645
|
total,
|
|
@@ -17490,8 +17676,8 @@ var MemoryStore = class {
|
|
|
17490
17676
|
* malformed window is the caller's error exactly as it is in the exact page;
|
|
17491
17677
|
* - the page is FACT-level (`total`/`offset`/`limit`/`truncated`), and the caller's output budget
|
|
17492
17678
|
* only shortens `content` — dropping a row would make `total` disagree with the page;
|
|
17493
|
-
* - the returned facts
|
|
17494
|
-
*
|
|
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}).
|
|
17495
17681
|
*/
|
|
17496
17682
|
async factPage(input, rawQuery) {
|
|
17497
17683
|
const query = rawQuery.trim();
|
|
@@ -17510,7 +17696,7 @@ var MemoryStore = class {
|
|
|
17510
17696
|
});
|
|
17511
17697
|
if (page.total === 0) return null;
|
|
17512
17698
|
const facts = this.budgetFactEvidence(page.rows.map(factEvidenceOf), input.maxTokens);
|
|
17513
|
-
if (input.track !== false) await this.
|
|
17699
|
+
if (input.track !== false) await this.touchOnly(facts.map((entry) => entry.fact_id));
|
|
17514
17700
|
return {
|
|
17515
17701
|
mode: "facts",
|
|
17516
17702
|
total: page.total,
|
|
@@ -17607,8 +17793,8 @@ var MemoryStore = class {
|
|
|
17607
17793
|
text: String(row["content"] ?? ""),
|
|
17608
17794
|
memory_kind: mapLegacyKind(String(row["kind"] ?? "note")),
|
|
17609
17795
|
...title === void 0 ? {} : { title },
|
|
17610
|
-
created_at: row["created_at"]
|
|
17611
|
-
updated_at:
|
|
17796
|
+
created_at: modelInstant(row["created_at"]) ?? "",
|
|
17797
|
+
updated_at: modelInstant(row["updated_at"])
|
|
17612
17798
|
});
|
|
17613
17799
|
}
|
|
17614
17800
|
return evidence;
|
|
@@ -17836,10 +18022,9 @@ var MemoryStore = class {
|
|
|
17836
18022
|
score: scoreOf(id),
|
|
17837
18023
|
source_ref: factSourceRef(id),
|
|
17838
18024
|
entities: bags.get(id) ?? [],
|
|
17839
|
-
created_at: time?.created_at ?? "",
|
|
17840
|
-
updated_at: time?.updated_at
|
|
18025
|
+
created_at: modelInstant(time?.created_at) ?? "",
|
|
18026
|
+
updated_at: modelInstant(time?.updated_at),
|
|
17841
18027
|
...time?.valid_from === null || time?.valid_from === void 0 ? {} : { valid_from: time.valid_from },
|
|
17842
|
-
...time?.valid_to === null || time?.valid_to === void 0 ? {} : { valid_to: time.valid_to },
|
|
17843
18028
|
...time?.title === null || time?.title === void 0 ? {} : { title: time.title }
|
|
17844
18029
|
};
|
|
17845
18030
|
});
|
|
@@ -17904,6 +18089,19 @@ var MemoryStore = class {
|
|
|
17904
18089
|
} catch {}
|
|
17905
18090
|
}
|
|
17906
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
|
+
/**
|
|
17907
18105
|
* Semantic leg. `queryVector` short-circuits the encode when the caller already
|
|
17908
18106
|
* encoded this exact query (the cross-store router does, for both stores at once);
|
|
17909
18107
|
* `onVector` publishes the vector this leg actually used, so the relaxed retry pass can reuse it
|
|
@@ -18012,25 +18210,22 @@ var MemoryStore = class {
|
|
|
18012
18210
|
/**
|
|
18013
18211
|
* Insert (or resolve to) a fact row.
|
|
18014
18212
|
*
|
|
18015
|
-
* `kind`
|
|
18016
|
-
*
|
|
18017
|
-
*
|
|
18018
|
-
*
|
|
18019
|
-
*
|
|
18020
|
-
* omitted one keeps what the row already has, and a brand-new row falls back to the
|
|
18021
|
-
* 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`).
|
|
18022
18218
|
*/
|
|
18023
|
-
async persistFact(content, kind,
|
|
18219
|
+
async persistFact(content, kind, entities, triples, opts, explicitEdges = void 0) {
|
|
18024
18220
|
const hrr = hrrToBytes(encodeHrrEntityVector(entities.map((e) => e.name)));
|
|
18025
18221
|
const trust = this.config.trust;
|
|
18026
18222
|
const clock = await this.lifecycle.clock();
|
|
18027
18223
|
const nowIso = formatUtcTs(Date.now());
|
|
18028
18224
|
const replaced = opts?.supersedesId === void 0 ? void 0 : await this.getRow(opts.supersedesId);
|
|
18029
18225
|
const superseded = trust.inherit_trust_on_update ? replaced : void 0;
|
|
18030
|
-
const startTrust = superseded === void 0 ?
|
|
18226
|
+
const startTrust = superseded === void 0 ? BASE_TRUST : effectiveTrust(asTrustRow(superseded), clock, trust);
|
|
18031
18227
|
const startPinned = superseded !== void 0 && isPinned(asTrustRow(superseded)) ? 1 : 0;
|
|
18032
18228
|
const newKind = kind ?? (replaced === void 0 ? "note" : mapLegacyKind(replaced.kind));
|
|
18033
|
-
const newTtl = ttlDays ?? (replaced === void 0 ? 0 : Number(replaced.ttl_days ?? 0));
|
|
18034
18229
|
/**
|
|
18035
18230
|
* The title this revision would carry (JOURNAL_TITLE_SEARCH §2.2/§11.2, v17's correction).
|
|
18036
18231
|
*
|
|
@@ -18062,13 +18257,11 @@ var MemoryStore = class {
|
|
|
18062
18257
|
}
|
|
18063
18258
|
const createdAt = replaced === void 0 || replaced.created_at === null ? nowIso : String(replaced.created_at);
|
|
18064
18259
|
const validFrom = opts?.eventDate === void 0 ? replaced === void 0 ? null : nullableEventTime(replaced.valid_from, replaced.valid_from_ms) : normalizeEventTime(opts.eventDate, "event_date");
|
|
18065
|
-
const validTo = opts?.validUntil === void 0 ? replaced === void 0 ? null : nullableEventTime(replaced.valid_to, replaced.valid_to_ms) : normalizeEventTime(opts.validUntil, "valid_until");
|
|
18066
18260
|
const { supersededId, ...result } = await this.lifecycle.transaction(async () => {
|
|
18067
18261
|
const cur = await this.facts.insertRevision({
|
|
18068
18262
|
content,
|
|
18069
18263
|
kind: newKind,
|
|
18070
18264
|
title: desiredTitle,
|
|
18071
|
-
ttlDays: newTtl,
|
|
18072
18265
|
supersedesId: opts?.supersedesId ?? null,
|
|
18073
18266
|
hrr,
|
|
18074
18267
|
trust: clamp01(startTrust),
|
|
@@ -18078,9 +18271,7 @@ var MemoryStore = class {
|
|
|
18078
18271
|
windowAt: nowIso,
|
|
18079
18272
|
createdAt,
|
|
18080
18273
|
validFrom: validFrom?.text ?? null,
|
|
18081
|
-
|
|
18082
|
-
validFromMs: validFrom?.ms ?? null,
|
|
18083
|
-
validToMs: validTo?.ms ?? null
|
|
18274
|
+
validFromMs: validFrom?.ms ?? null
|
|
18084
18275
|
});
|
|
18085
18276
|
let fact_id;
|
|
18086
18277
|
let is_new;
|
|
@@ -18090,18 +18281,16 @@ var MemoryStore = class {
|
|
|
18090
18281
|
fact_id = Number(row.fact_id);
|
|
18091
18282
|
is_new = false;
|
|
18092
18283
|
const rowKind = mapLegacyKind(row.kind);
|
|
18093
|
-
const rowTtl = Number(row.ttl_days ?? 0);
|
|
18094
18284
|
const keepKind = kind ?? rowKind;
|
|
18095
|
-
const keepTtl = ttlDays ?? rowTtl;
|
|
18096
18285
|
const rowTitle = storedTitle(row.title);
|
|
18097
18286
|
const selfRetitle = retitle && opts?.supersedesId !== void 0 && Number(opts.supersedesId) === fact_id;
|
|
18098
18287
|
if (opts?.title !== void 0 && opts.title !== null && rowTitle !== null && opts.title !== rowTitle && !selfRetitle) throw new Error(TITLE_CONFLICT);
|
|
18099
18288
|
const keepTitle = selfRetitle ? desiredTitle : rowTitle ?? desiredTitle;
|
|
18100
18289
|
if (opts?.requireJournalTitle === true && keepKind === "journal" && keepTitle === null) throw new Error(TITLE_REQUIRED);
|
|
18101
|
-
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);
|
|
18102
18291
|
const settled = effectiveTrust(asTrustRow(row), clock, trust);
|
|
18103
|
-
const revived = await this.facts.revive(fact_id, keepKind,
|
|
18104
|
-
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);
|
|
18105
18294
|
if (keepTitle !== null && keepTitle !== rowTitle) {
|
|
18106
18295
|
await this.facts.setTitle(fact_id, keepTitle);
|
|
18107
18296
|
if (selfRetitle) {
|
|
@@ -18189,10 +18378,7 @@ var MemoryStore = class {
|
|
|
18189
18378
|
if (!opts?.supersedesId || opts.supersedesId === fact_id) return null;
|
|
18190
18379
|
await this.facts.linkSupersede(fact_id, opts.supersedesId);
|
|
18191
18380
|
if (opts.archiveOldIfActive) {
|
|
18192
|
-
if (await this.facts.archiveSuperseded(opts.supersedesId, await this.lifecycle.clock()) > 0)
|
|
18193
|
-
await this.contradictions.resolveForFact(opts.supersedesId);
|
|
18194
|
-
await this.facts.markValidTo(opts.supersedesId, formatUtcTs(Date.now()), Date.now());
|
|
18195
|
-
}
|
|
18381
|
+
if (await this.facts.archiveSuperseded(opts.supersedesId, await this.lifecycle.clock()) > 0) await this.contradictions.resolveForFact(opts.supersedesId);
|
|
18196
18382
|
}
|
|
18197
18383
|
await this.triples.deleteForFact(opts.supersedesId);
|
|
18198
18384
|
await this.relations.deleteEdgesForFact(opts.supersedesId);
|
|
@@ -18349,8 +18535,8 @@ var MemoryStore = class {
|
|
|
18349
18535
|
pinned: isPinned(row),
|
|
18350
18536
|
remaining_days: remainingDays(row, clock, this.config.trust),
|
|
18351
18537
|
helpful_count: Number(r.helpful_count),
|
|
18352
|
-
created_at:
|
|
18353
|
-
archived_at:
|
|
18538
|
+
created_at: modelInstant(r.created_at) ?? "",
|
|
18539
|
+
archived_at: modelInstant(r.archived_at),
|
|
18354
18540
|
archive_reason: r.archive_reason ? String(r.archive_reason) : null
|
|
18355
18541
|
};
|
|
18356
18542
|
}
|
|
@@ -18363,14 +18549,13 @@ var MemoryStore = class {
|
|
|
18363
18549
|
entities,
|
|
18364
18550
|
triples,
|
|
18365
18551
|
settle_clock: Number(r.settle_clock),
|
|
18366
|
-
pinned_at:
|
|
18367
|
-
last_reinforced_at:
|
|
18368
|
-
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),
|
|
18369
18555
|
bonus_count: Number(r.bonus_count ?? 0),
|
|
18370
|
-
bonus_window_at:
|
|
18556
|
+
bonus_window_at: modelInstant(r.bonus_window_at),
|
|
18371
18557
|
sources: await this.sources.listForFact(factId),
|
|
18372
18558
|
valid_from: nullableString(r.valid_from),
|
|
18373
|
-
valid_to: nullableString(r.valid_to),
|
|
18374
18559
|
superseded_by: await this.facts.supersededBy(factId),
|
|
18375
18560
|
assert_count: Number(r.assert_count ?? 1),
|
|
18376
18561
|
relations: await this.relations.explicitRelationsForFact(factId)
|
|
@@ -18476,10 +18661,11 @@ function nullableEventTime(text, ms) {
|
|
|
18476
18661
|
* inserts SQL rows.
|
|
18477
18662
|
*
|
|
18478
18663
|
* TWO CONSUMERS, TWO SEMANTICS. `import`/`merge` remain a CONTENT-IDENTITY UNION and consume only
|
|
18479
|
-
* a subset: kind,
|
|
18480
|
-
*
|
|
18481
|
-
*
|
|
18482
|
-
*
|
|
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`):
|
|
18483
18669
|
* only into an EMPTY facts table, keeping fact_id, status, timestamps, trust, pin, counters, every
|
|
18484
18670
|
* provenance row and the supersedes chain, then re-deriving entities. The union path never revives
|
|
18485
18671
|
* an archived row; restore is the only path that brings archived rows back. The vector, entity,
|
|
@@ -18575,7 +18761,6 @@ const FACT_COLUMNS = [
|
|
|
18575
18761
|
"helpful_count",
|
|
18576
18762
|
"last_reinforced_at",
|
|
18577
18763
|
"valid_from",
|
|
18578
|
-
"valid_to",
|
|
18579
18764
|
"supersedes_id",
|
|
18580
18765
|
"archived_at",
|
|
18581
18766
|
"archive_reason",
|
|
@@ -18725,7 +18910,7 @@ function readFactsDocument(dbPath, options) {
|
|
|
18725
18910
|
last_reinforced_at: textOrNull(cell(row, "last_reinforced_at")),
|
|
18726
18911
|
sources: sources.get(sourceId) ?? [],
|
|
18727
18912
|
valid_from: textOrNull(cell(row, "valid_from")),
|
|
18728
|
-
valid_to:
|
|
18913
|
+
valid_to: null,
|
|
18729
18914
|
supersedes_source_id: supersedes === null || supersedes === void 0 ? null : numberOf(supersedes, 0),
|
|
18730
18915
|
archive: status === "archived" ? {
|
|
18731
18916
|
at: textOrNull(cell(row, "archived_at")),
|
|
@@ -18921,7 +19106,9 @@ function emptyMetadataDropped() {
|
|
|
18921
19106
|
assert_count: 0,
|
|
18922
19107
|
helpful_count: 0,
|
|
18923
19108
|
pinned: 0,
|
|
18924
|
-
supersedes: 0
|
|
19109
|
+
supersedes: 0,
|
|
19110
|
+
ttl_days: 0,
|
|
19111
|
+
valid_to: 0
|
|
18925
19112
|
};
|
|
18926
19113
|
}
|
|
18927
19114
|
/** What a Phase-2 restore WOULD have carried but v1 does not. */
|
|
@@ -18932,7 +19119,9 @@ function metadataDroppedOf(row) {
|
|
|
18932
19119
|
assert_count: numberOf(cell(row, "assert_count"), 1) > 1 ? 1 : 0,
|
|
18933
19120
|
helpful_count: numberOf(cell(row, "helpful_count"), 0) > 0 ? 1 : 0,
|
|
18934
19121
|
pinned: boolOf(cell(row, "pinned")) ? 1 : 0,
|
|
18935
|
-
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
|
|
18936
19125
|
};
|
|
18937
19126
|
}
|
|
18938
19127
|
function accumulateMetadataDropped(total, one) {
|
|
@@ -18942,6 +19131,8 @@ function accumulateMetadataDropped(total, one) {
|
|
|
18942
19131
|
total.helpful_count += one.helpful_count;
|
|
18943
19132
|
total.pinned += one.pinned;
|
|
18944
19133
|
total.supersedes += one.supersedes;
|
|
19134
|
+
total.ttl_days += one.ttl_days;
|
|
19135
|
+
total.valid_to += one.valid_to;
|
|
18945
19136
|
}
|
|
18946
19137
|
function sourceIdOf(row) {
|
|
18947
19138
|
const value = cell(row, "source_id");
|
|
@@ -18953,12 +19144,6 @@ function optionalString(row, key, label) {
|
|
|
18953
19144
|
if (typeof value !== "string") throw new Error(`${label} 不是字符串`);
|
|
18954
19145
|
return value === "" ? void 0 : value;
|
|
18955
19146
|
}
|
|
18956
|
-
function optionalTtlDays(row) {
|
|
18957
|
-
const value = cell(row, "ttl_days");
|
|
18958
|
-
if (value === null || value === void 0) return void 0;
|
|
18959
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new Error("ttl_days 无效(需要非负整数)");
|
|
18960
|
-
return value;
|
|
18961
|
-
}
|
|
18962
19147
|
function optionalDate(row, key, label) {
|
|
18963
19148
|
const value = cell(row, key);
|
|
18964
19149
|
if (value === null || value === void 0) return void 0;
|
|
@@ -19020,19 +19205,15 @@ function addRequestFor(identity, row, fallbackSourceRef, preferEmbeddedSources)
|
|
|
19020
19205
|
const sourceRef = (preferEmbeddedSources ? embeddedSourceRef(row) : void 0) ?? fallbackSourceRef;
|
|
19021
19206
|
const kind = mapLegacyKind(optionalString(row, "kind", "kind") ?? optionalString(row, "category", "category"));
|
|
19022
19207
|
const title = optionalString(row, "title", "title");
|
|
19023
|
-
const ttlDays = optionalTtlDays(row);
|
|
19024
19208
|
const eventDate = optionalDate(row, "valid_from", "valid_from");
|
|
19025
|
-
const validUntil = optionalDate(row, "valid_to", "valid_to");
|
|
19026
19209
|
const relations = relationsOf(row, kind);
|
|
19027
19210
|
return {
|
|
19028
19211
|
action: "add",
|
|
19029
19212
|
content: identity,
|
|
19030
19213
|
kind,
|
|
19031
19214
|
...title === void 0 ? {} : { title },
|
|
19032
|
-
...ttlDays === void 0 ? {} : { ttl_days: ttlDays },
|
|
19033
19215
|
...sourceRef === void 0 ? {} : { source_ref: sourceRef },
|
|
19034
19216
|
...eventDate === void 0 ? {} : { event_date: eventDate },
|
|
19035
|
-
...validUntil === void 0 ? {} : { valid_until: validUntil },
|
|
19036
19217
|
...relations === void 0 ? {} : { relations }
|
|
19037
19218
|
};
|
|
19038
19219
|
}
|
|
@@ -19427,13 +19608,12 @@ function walHealth(dbPath) {
|
|
|
19427
19608
|
* P-08 / P-13 / JOURNAL_TITLE_SEARCH: the `mem_remember` extras, lifted off the request and onto the
|
|
19428
19609
|
* store's write options. Only the keys actually present are carried, so an omitted argument stays
|
|
19429
19610
|
* "omitted" all the way down (the store's "unset keeps what the row has" rule — an update inherits
|
|
19430
|
-
* an omitted title
|
|
19611
|
+
* an omitted title or event time — depends on that distinction).
|
|
19431
19612
|
*/
|
|
19432
19613
|
function writeOptions(req, rules) {
|
|
19433
19614
|
return {
|
|
19434
19615
|
...req.source_ref === void 0 ? {} : { sourceRef: req.source_ref },
|
|
19435
19616
|
...req.event_date === void 0 ? {} : { eventDate: req.event_date },
|
|
19436
|
-
...req.valid_until === void 0 ? {} : { validUntil: req.valid_until },
|
|
19437
19617
|
...req.title === void 0 ? {} : { title: req.title },
|
|
19438
19618
|
...req.retitle === true ? { retitle: true } : {},
|
|
19439
19619
|
...req.relations === void 0 ? {} : { relations: req.relations },
|
|
@@ -19557,11 +19737,9 @@ async function buildRuntime(opts) {
|
|
|
19557
19737
|
},
|
|
19558
19738
|
async remember(req, rules) {
|
|
19559
19739
|
switch (req.action) {
|
|
19560
|
-
case "add": return this.memory.add(req.content, req.kind,
|
|
19561
|
-
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));
|
|
19562
19742
|
case "remove": return this.memory.remove(req.fact_id, req.reason);
|
|
19563
|
-
case "helpful": return this.memory.helpful(req.fact_id);
|
|
19564
|
-
case "unhelpful": return this.memory.unhelpful(req.fact_id);
|
|
19565
19743
|
}
|
|
19566
19744
|
},
|
|
19567
19745
|
async recall(req) {
|
|
@@ -19591,14 +19769,19 @@ async function buildRuntime(opts) {
|
|
|
19591
19769
|
},
|
|
19592
19770
|
async admin(req) {
|
|
19593
19771
|
switch (req.action) {
|
|
19594
|
-
case "stats":
|
|
19595
|
-
|
|
19596
|
-
|
|
19597
|
-
|
|
19598
|
-
|
|
19599
|
-
|
|
19600
|
-
|
|
19601
|
-
|
|
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
|
+
};
|
|
19602
19785
|
case "list": return await this.memory.list(req.kind, req.status ?? "active", req.limit ?? 50, req.offset ?? 0, req.source, req.time);
|
|
19603
19786
|
case "detail": return await this.memory.get(req.fact_id) ?? { error: `fact_id=${req.fact_id} not found` };
|
|
19604
19787
|
case "archive": return await this.memory.archive(req.fact_id, req.reason);
|
|
@@ -19749,11 +19932,9 @@ const descriptors = [
|
|
|
19749
19932
|
content: optionalText.optional(),
|
|
19750
19933
|
fact_id: count.optional(),
|
|
19751
19934
|
kind: optionalText.optional(),
|
|
19752
|
-
ttl_days: count.optional(),
|
|
19753
19935
|
reason: optionalText.optional(),
|
|
19754
19936
|
source_ref: optionalText.optional(),
|
|
19755
19937
|
event_date: optionalText.optional(),
|
|
19756
|
-
valid_until: optionalText.optional(),
|
|
19757
19938
|
title: optionalText.optional(),
|
|
19758
19939
|
retitle: bool.optional(),
|
|
19759
19940
|
relations: z.array(z.object({
|
|
@@ -21577,7 +21758,7 @@ function tokenize(rawInput) {
|
|
|
21577
21758
|
return tokens;
|
|
21578
21759
|
}
|
|
21579
21760
|
function metadataDroppedLine(dropped) {
|
|
21580
|
-
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);
|
|
21581
21762
|
}
|
|
21582
21763
|
/** Failures are rendered in full up to {@link FAILURES_SHOWN}; the rest is a count, never silence. */
|
|
21583
21764
|
const FAILURES_SHOWN = 5;
|