@avantf/dsh-mem 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +42 -43
  2. package/lib/client.js +1181 -327
  3. package/lib/engine/graph-recall/index.d.ts +2 -2
  4. package/lib/engine/graph-recall/traverse.d.ts +2 -2
  5. package/lib/engine/graph-recall/types.d.ts +1 -1
  6. package/lib/engine/mem/config/config_files.d.ts +1 -1
  7. package/lib/engine/mem/db/dao/facts.d.ts +80 -82
  8. package/lib/engine/mem/dbdrivers/sqlite/facts.d.ts +6 -13
  9. package/lib/engine/mem/defaults.d.ts +4 -1
  10. package/lib/engine/mem/entities/extract.d.ts +34 -14
  11. package/lib/engine/mem/facts_io.d.ts +4 -0
  12. package/lib/engine/mem/index.d.ts +1 -0
  13. package/lib/engine/mem/interfaces/repositories.d.ts +5 -19
  14. package/lib/engine/mem/lifecycle/maintenance.d.ts +8 -4
  15. package/lib/engine/mem/lifecycle/tick.d.ts +11 -10
  16. package/lib/engine/mem/lifecycle/trust.d.ts +24 -37
  17. package/lib/engine/mem/modelBootstrap.d.ts +29 -0
  18. package/lib/engine/mem/runtime.d.ts +4 -8
  19. package/lib/engine/mem/store/facts_restore.d.ts +8 -6
  20. package/lib/engine/mem/store/graph_reader.d.ts +2 -2
  21. package/lib/engine/mem/store/memory.d.ts +98 -37
  22. package/lib/engine/mem/store/recall_policy.d.ts +10 -0
  23. package/lib/engine/mem/store/time_filter.d.ts +1 -1
  24. package/lib/engine/mem/trustHeartbeat.d.ts +1 -1
  25. package/lib/engine/mem-contract/config.d.ts +144 -9
  26. package/lib/engine/mem-contract/index.d.ts +2 -0
  27. package/lib/engine/mem-contract/time.d.ts +75 -0
  28. package/lib/engine/mem-contract/tools.d.ts +9 -15
  29. package/lib/engine/mem-contract/trust_curve.d.ts +119 -0
  30. package/lib/engine/mem-contract/types.d.ts +73 -12
  31. package/lib/engine/mem-retrieval/adapters/auto_vstore.d.ts +10 -0
  32. package/lib/engine/mem-retrieval/adapters/hnswlib.d.ts +28 -0
  33. package/lib/engine/mem-retrieval/adapters/local_bge.d.ts +48 -6
  34. package/lib/engine/mem-retrieval/interfaces.d.ts +22 -2
  35. package/lib/engine/mem-retrieval/text_budget.d.ts +31 -0
  36. package/lib/index.js +2349 -665
  37. package/lib/interface-version.json +2 -2
  38. package/lib/types/client/index.d.ts.map +1 -1
  39. package/lib/types/client/paging.d.ts +58 -8
  40. package/lib/types/client/paging.d.ts.map +1 -1
  41. package/lib/types/client/time_filter.d.ts +54 -0
  42. package/lib/types/client/time_filter.d.ts.map +1 -0
  43. package/lib/types/command.d.ts.map +1 -1
  44. package/lib/types/deps_audit.d.ts +88 -0
  45. package/lib/types/deps_audit.d.ts.map +1 -0
  46. package/lib/types/envinit-bootstrap.d.ts +1 -1
  47. package/lib/types/envinit.d.ts +224 -4
  48. package/lib/types/envinit.d.ts.map +1 -1
  49. package/lib/types/hints.d.ts +58 -0
  50. package/lib/types/hints.d.ts.map +1 -1
  51. package/lib/types/index.d.ts.map +1 -1
  52. package/lib/types/remote.d.ts.map +1 -1
  53. package/lib/types/render.d.ts.map +1 -1
  54. package/lib/types/runtime_deps_resource.d.ts +29 -0
  55. package/lib/types/runtime_deps_resource.d.ts.map +1 -0
  56. package/package.json +26 -24
  57. package/res/runtime-deps.yaml +99 -0
@@ -4,8 +4,10 @@
4
4
  *
5
5
  * Surface stability (D10): the original report keys are all preserved
6
6
  * (`decayed / archived_ttl / archived_age / purged / purged_ids / archived_ids`)
7
- * and the new detail is purely additive. `archived_age` now means "non-TTL
8
- * automatic archival" = `forgot + idle`.
7
+ * and the new detail is purely additive. `archived_ttl` / `archived_idle` are kept as
8
+ * CONSTANT 0 and marked `@deprecated`: TTL and idle archival are gone (v2), but a report key
9
+ * is a surface scripts read — deleting it would break them for nothing. `archived_age` now
10
+ * means "the automatic archival that still exists" = `forgot`.
9
11
  *
10
12
  * The `maintenance` action forces a FULL pass (no settle budget); the automatic
11
13
  * startup/heartbeat passes use `tick_max_facts`.
@@ -15,8 +17,9 @@ import type { Db } from '../db/conn.js';
15
17
  export interface MaintenanceResult {
16
18
  /** Rows whose trust was materialized by the settle sweep (see tick.ts). */
17
19
  decayed: number;
20
+ /** @deprecated TTL archival is gone (v2); always 0, kept so existing report readers keep working. */
18
21
  archived_ttl: number;
19
- /** Non-TTL automatic archival = `forgot + idle` (kept name, widened meaning). */
22
+ /** The automatic archival this pass performed = `forgot` (name kept, D10). */
20
23
  archived_age: number;
21
24
  purged: number;
22
25
  /** Fact ids physically deleted by the purge — callers evict their vectors from the live index. */
@@ -26,10 +29,11 @@ export interface MaintenanceResult {
26
29
  settled: number;
27
30
  clock: number;
28
31
  archived_forgot: number;
32
+ /** @deprecated Idle archival is gone (v2); always 0, kept for the same reason as `archived_ttl`. */
29
33
  archived_idle: number;
30
34
  /** Rows still pending settle because the budget ran out (always 0 for a forced pass). */
31
35
  skipped: number;
32
- /** Rows still due for TTL/forgot/idle because the budget ran out (0 for a forced pass). */
36
+ /** Rows still due for forgot because the budget ran out (0 for a forced pass). */
33
37
  archived_deferred: number;
34
38
  /** Rows still due for purge when the budget ran out (0 for a forced pass). */
35
39
  purged_deferred: number;
@@ -1,16 +1,19 @@
1
1
  /**
2
- * One trust/lifecycle tick (spec docs/TRUST_MODEL.md §4) — five ordered statements:
2
+ * One trust/lifecycle tick (spec docs/TRUST_MODEL.md §4) — three ordered statements:
3
3
  *
4
4
  * ① settle — materialize decay for every row whose ACTIVE DAY changed
5
- * ② TTL — calendar expiry (explicit instruction; pinned is not exempt)
6
- * ③ forgot — trust ≤ forget_threshold after settling (pinned exempt)
7
- * ④ idle — calendar fallback: unused for `idle_calendar_days`
8
- * ⑤ purge — physical delete of archived rows past the window
5
+ * ② forgot — trust ≤ forget_threshold after settling (pinned exempt)
6
+ * ③ purge — physical delete of archived rows past the window
7
+ *
8
+ * The calendar clocks are GONE (v2): there is no TTL step and no idle fallback, so a fact's
9
+ * lifetime is decided by the active-day curve alone and "expired" is expressed by an explicit
10
+ * `update` / `archive` (docs/TRUST_MODEL.md §3.4). `pinned` is therefore never archived by an
11
+ * automatic pass.
9
12
  *
10
13
  * EVERY step is budgeted by the same `tick_max_facts`, not just ①.
11
14
  *
12
15
  * It used to say "only ① is budgeted; ②③④⑤ only ever touch critical rows", and that reading is
13
- * wrong in the direction that matters: ②③④⑤ each run inside the tick's ONE `IMMEDIATE` transaction,
16
+ * wrong in the direction that matters: every step runs inside the tick's ONE `IMMEDIATE` transaction,
14
17
  * which holds the write lock for its whole duration, so an unbounded archive or purge pass over a
15
18
  * large corpus blocks every other process (and the startup path, which is where the tick runs) for
16
19
  * as long as it takes. Bounding each step is what keeps that lock short; `archived_deferred` and
@@ -24,17 +27,15 @@ export interface TickResult {
24
27
  clock: number;
25
28
  /** Rows materialized by ① (equals "trust actually changed" — see the note in runTrustTick). */
26
29
  settled: number;
27
- archived_ttl: number;
28
30
  archived_forgot: number;
29
- archived_idle: number;
30
31
  purged: number;
31
32
  purged_ids: number[];
32
33
  archived_ids: number[];
33
34
  /** Rows still awaiting ① when the budget ran out (0 for a full pass). */
34
35
  skipped: number;
35
- /** Rows still due for ②③④ when the budget ran out (0 when the pass finished them). */
36
+ /** Rows still due for ② when the budget ran out (0 when the pass finished them). */
36
37
  archived_deferred: number;
37
- /** Rows still due for ⑤ when the budget ran out (0 when the pass finished them). */
38
+ /** Rows still due for ③ when the budget ran out (0 when the pass finished them). */
38
39
  purged_deferred: number;
39
40
  }
40
41
  export declare function runTrustTick(db: Db, cfg: Config, opts: {
@@ -3,13 +3,22 @@
3
3
  * (`clock` / `nowMs` are always injected, so every rule is unit-testable).
4
4
  *
5
5
  * Spec: docs/TRUST_MODEL.md §2. Key rules encoded here:
6
- * - `eff = pinned ? 1 : clamp(trust − step × (clock − settle_clock), 0, 1)` (§2.2)
6
+ * - `eff = pinned ? 1 : clamp(T(clock − settle_clock), 0, 1)` (§2.2), where `T(days)` is the decay
7
+ * curve of `contract/src/trust_curve.ts` — the ONE implementation of the formula
7
8
  * - recall only ever RAISES trust (`max(eff, …)`, R2) and never pins (D11)
8
9
  * - zero-gain recalls neither consume quota nor stamp `last_reinforced_at` (R10)
9
- * - explicit feedback can pin (snap to 1.0) or forget immediately (§2.4)
10
+ * - permanent is set ONLY by an explicit `pin`; no write path moves a fact across that line (§2.4)
10
11
  * - `enabled=false` degrades `eff()` to the stored value (D12)
11
12
  */
12
13
  import type { Config } from '../../mem-contract/index.js';
14
+ import { formatUtcTs, parseUtcTs, type TrustCurve } from '../../mem-contract/index.js';
15
+ /**
16
+ * The UTC stamp parse/render pair is `@avantf/mem-contract/time` — ONE implementation, shared with
17
+ * the two display faces (`formatOffsetIso` for the model, `formatLocalHuman` for the UI), so the
18
+ * "a zone-less stamp means UTC" rule cannot drift between the trust math and what a reader sees.
19
+ * Re-exported here because this module is where the trust code (and its specs) have always found it.
20
+ */
21
+ export { formatUtcTs, parseUtcTs };
13
22
  export declare const DAY_MS = 86400000;
14
23
  /** Guard for "did the number actually move" comparisons. */
15
24
  export declare const EPSILON = 1e-9;
@@ -24,20 +33,9 @@ export interface TrustRow {
24
33
  status: string;
25
34
  }
26
35
  export declare function clamp01(v: number): number;
36
+ /** The curve of one `trust` section — the single mapping from config keys to the math. */
37
+ export declare function trustCurveOf(cfg: TrustConfig): TrustCurve;
27
38
  export declare function isPinned(row: Pick<TrustRow, 'pinned'>): boolean;
28
- /**
29
- * `'YYYY-MM-DD HH:MM:SS'` (SQLite UTC) or ISO → epoch ms; null/garbage → null.
30
- *
31
- * A stamp with NO zone designator is read as UTC, not as local time. `Date.parse` treats a zone-less
32
- * ISO string as LOCAL, and everything in this store is written in UTC (`CURRENT_TIMESTAMP`, and
33
- * {@link formatUtcTs}), so handing one over as-is would shift every quota window and every decay
34
- * measurement by the host's UTC offset — a fact reinforced at 23:00 UTC+8 would look reinforced 8
35
- * hours earlier, and the 24h cap would open or close at the wrong moment. An explicit `Z` or
36
- * `±HH:MM` is left alone.
37
- */
38
- export declare function parseUtcTs(ts: string | null | undefined): number | null;
39
- /** Epoch ms → `'YYYY-MM-DD HH:MM:SS'` (the SQLite `CURRENT_TIMESTAMP` shape, UTC). */
40
- export declare function formatUtcTs(ms: number): string;
41
39
  /** eff(f) — the trust a caller should act on right now (spec §2.2). */
42
40
  export declare function effectiveTrust(row: TrustRow, clock: number, cfg: TrustConfig, enabled?: boolean): number;
43
41
  /**
@@ -48,8 +46,9 @@ export declare function effectiveTrust(row: TrustRow, clock: number, cfg: TrustC
48
46
  */
49
47
  export declare function displayTrust(row: TrustRow, clock: number, cfg: TrustConfig, enabled?: boolean): number;
50
48
  /**
51
- * Remaining active days: `eff / step`. `null` for pinned rows, non-active rows and
52
- * when trust is disabled — the contract's `remaining_days` (spec §6 / R11).
49
+ * Remaining active days: the inverse of the curve at the current eff (`daysToZero`).
50
+ * `null` for pinned rows, non-active rows and when trust is disabled — the contract's
51
+ * `remaining_days` (spec §6 / R11).
53
52
  */
54
53
  export declare function remainingDays(row: TrustRow, clock: number, cfg: TrustConfig, enabled?: boolean): number | null;
55
54
  /** Everything a recall write needs to persist (spec §2.3). */
@@ -66,25 +65,13 @@ export interface RecallOutcome {
66
65
  * Apply one recall to a fact: settle, then grant a bonus inside the daily quota.
67
66
  * Returns `next === settled` when the quota is exhausted, the ceiling is reached,
68
67
  * the fact is pinned, or the gain would be zero (R2/R10/D11).
69
- */
70
- export declare function grantRecallBonus(row: TrustRow, clock: number, nowMs: number, cfg: TrustConfig, enabled?: boolean): RecallOutcome;
71
- /** Everything a feedback write needs to persist (spec §2.4). */
72
- interface FeedbackOutcome {
73
- settled: number;
74
- next: number;
75
- pin: boolean;
76
- forget: boolean;
77
- /** true when the row was left untouched (pinned / archived). */
78
- untouched: boolean;
79
- bonusCount: number;
80
- windowAt: string;
81
- }
82
- /**
83
- * Apply one explicit `helpful` (+1) / `unhelpful` (-1) step.
84
68
  *
85
- * Pinned and archived rows are untouched (R7): only `helpful_count` changes for
86
- * them, so a pinned fact can never end up storing 0.95 while displaying 1.0.
87
- * When `feedback_daily_cap > 0` feedback shares the per-fact 24h window counter.
69
+ * The gain is SEGMENTED at the frozen anchor `BASE_TRUST` (D20):
70
+ * - strictly below the base: `min(BASE_TRUST, T + recall_gain_below_base)`, so the shipped
71
+ * `recall_gain_below_base: 0.1` takes 0.05 → 0.5 in five recalls and never past the base;
72
+ * - AT OR ABOVE the base: `recall_gain_above_base × marginal_decay^count`, capped at the ceiling
73
+ * INVERTED from `max_retention_days` (there is no `recall_ceiling` key any more, D16). The
74
+ * boundary is strictly `<` on purpose: a fact sitting exactly on the base (a fresh one, on the
75
+ * first recall after it has finished settling) takes the above-base branch and starts climbing.
88
76
  */
89
- export declare function applyFeedbackDelta(row: TrustRow, clock: number, delta: number, nowMs: number, cfg: TrustConfig, enabled?: boolean): FeedbackOutcome;
90
- export {};
77
+ export declare function grantRecallBonus(row: TrustRow, clock: number, nowMs: number, cfg: TrustConfig, enabled?: boolean): RecallOutcome;
@@ -1,3 +1,4 @@
1
+ import { type JiebaStatus } from './entities/extract.js';
1
2
  import type { AvantfRuntime } from './runtime.js';
2
3
  /** The statuses the base's verdict can carry (kept structural — see the note above). */
3
4
  export type DshCompatStatus = 'ok' | 'version-mismatch' | 'version-unknown' | 'probe-skipped' | 'probe-failed';
@@ -72,6 +73,34 @@ export declare function warmModels(rt: AvantfRuntime, options?: WarmOptions, rep
72
73
  * @returns how long the idle gate waited (ms), or `0` when it was not requested.
73
74
  */
74
75
  export declare function awaitStartupGate(options?: WarmOptions): Promise<number>;
76
+ /**
77
+ * The tokenizer states this module reports — one line per observation, in the order they happen.
78
+ *
79
+ * Process-wide on purpose, because the thing being described is: `entities/extract.ts` memoizes the
80
+ * load per process, and a provisioned module root that arrives later (`useTokenizerModuleDir`) can
81
+ * turn the same memo from "unavailable" into "ready". Without the previous verdict a slow repair
82
+ * prints a bare contradiction (`unavailable in 1ms` … `ready in 623ms`); with it the second line
83
+ * says it is a recovery, and the first one knows enough to say it was not final.
84
+ */
85
+ export type TokenizerVerdict = 'unknown' | 'ready' | 'degraded';
86
+ /** One line to log, and the verdict it leaves behind. */
87
+ export interface TokenizerReport {
88
+ level: 'info' | 'warn';
89
+ message: string;
90
+ next: TokenizerVerdict;
91
+ }
92
+ /**
93
+ * The line one tokenizer observation deserves.
94
+ *
95
+ * `previous` is what makes the pair readable: a `ready` after a `degraded` is a RECOVERY (the
96
+ * earlier probe ran before a loadable copy existed — the provisioned root had not been handed over,
97
+ * or the package was not installed yet), not a change of story. A `degraded` line never claims more
98
+ * than it knows: it names the reason and the two ways to fix it, and says that a loadable copy
99
+ * arriving later is picked up automatically.
100
+ *
101
+ * Exported for the test that pins those transitions; it has no side effects.
102
+ */
103
+ export declare function tokenizerReport(status: JiebaStatus, elapsedMs: number, previous: TokenizerVerdict): TokenizerReport;
75
104
  /**
76
105
  * Non-blocking embedding warm, for a caller that knows the model files are already on disk.
77
106
  *
@@ -123,11 +123,10 @@ export type RecallHitsResult = StoreResult<MemoryStore, 'searchMemory'>;
123
123
  /** The payload of `mem_recall.titles`: an exact page or a fuzzy shortlist of title candidates. */
124
124
  export type RecallTitlesResult = StoreResult<MemoryStore, 'titles'>;
125
125
  /**
126
- * `remember` dispatches to five store operations whose RESULTS differ: `add`/`update` return the
127
- * written fact, `remove` a flag, the feedback actions the new counter (or `null`). One plain
128
- * signature would either say `unknown` — leaving every caller to invent a cast, which is how a
129
- * caller ends up reading a field that does not exist — or a union that forces every caller to
130
- * narrow first. The callable type says what each ACTION returns.
126
+ * `remember` dispatches to three store operations whose RESULTS differ: `add`/`update` return the
127
+ * written fact, `remove` a flag. One plain signature would either say `unknown` — leaving every
128
+ * caller to invent a cast, which is how a caller ends up reading a field that does not exist — or a
129
+ * union that forces every caller to narrow first. The callable type says what each ACTION returns.
131
130
  */
132
131
  export type RememberDispatch = {
133
132
  (req: Extract<RememberRequest, {
@@ -139,9 +138,6 @@ export type RememberDispatch = {
139
138
  (req: Extract<RememberRequest, {
140
139
  action: 'remove';
141
140
  }>): Promise<StoreResult<MemoryStore, 'remove'>>;
142
- (req: Extract<RememberRequest, {
143
- action: 'helpful' | 'unhelpful';
144
- }>): Promise<StoreResult<MemoryStore, 'helpful'>>;
145
141
  (req: RememberRequest, rules?: RememberWriteRules): Promise<unknown>;
146
142
  };
147
143
  /**
@@ -4,13 +4,15 @@
4
4
  *
5
5
  * RESTORE IS NOT IMPORT. `import` / `merge` (see `facts_io.ts`) are CONTENT-IDENTITY UNIONS: new
6
6
  * rows get new ids, only ACTIVE facts travel, and every value the document carries beyond
7
- * kind/ttl/sources/validity is counted as dropped. Restore is the opposite promise — "put this
8
- * library back exactly as it was" — which is only answerable if the target is EMPTY:
7
+ * kind/sources/event time is counted as dropped (the retired `ttl_days` / `valid_to` are ignored).
8
+ * Restore is the opposite promise — "put this library back exactly as it was" — which is only
9
+ * answerable if the target is EMPTY:
9
10
  *
10
11
  * - `facts` is written DIRECTLY, one transaction, keeping `fact_id`, status, timestamps,
11
- * `trust_score`, `settle_clock`, counters, pin state, TTL, validity, the supersedes pointer and
12
- * the mirror bookkeeping. A non-empty target is REFUSED (never merged into), because "restore"
13
- * that silently unions two histories is a different, unreviewable operation.
12
+ * `trust_score`, `settle_clock`, counters, pin state, TTL (format only), event time, the
13
+ * supersedes pointer and the mirror bookkeeping. A non-empty target is REFUSED (never merged
14
+ * into), because "restore" that silently unions two histories is a different, unreviewable
15
+ * operation.
14
16
  * - Rows are inserted in `source_id` ASCENDING order, so the row a `supersedes` pointer names
15
17
  * already exists when the pointer is written. v1 documents carry the same pointer as
16
18
  * `supersedes_source_id`, so both versions resolve identically.
@@ -64,7 +66,7 @@ export declare const RESTORE_VECTOR_HINT: string;
64
66
  * rather than trust). A field counts as missing when the document does not carry the key at all —
65
67
  * `null` is a value the document states ("unknown"), not an absence.
66
68
  */
67
- export declare const RESTORE_METADATA_FIELDS: readonly ["kind", "title", "created_at", "updated_at", "trust_score", "pinned", "pinned_at", "assert_count", "helpful_count", "last_reinforced_at", "ttl_days", "valid_from", "valid_to", "supersedes_source_id", "archive", "sources", "settle_clock", "bonus_count", "bonus_window_at", "retrieval_count", "last_retrieved_at", "mirror_source", "mirror_target", "entities_version", "conflict_checked", "relations"];
69
+ export declare const RESTORE_METADATA_FIELDS: readonly ["kind", "title", "created_at", "updated_at", "trust_score", "pinned", "pinned_at", "assert_count", "helpful_count", "last_reinforced_at", "ttl_days", "valid_from", "supersedes_source_id", "archive", "sources", "settle_clock", "bonus_count", "bonus_window_at", "retrieval_count", "last_retrieved_at", "mirror_source", "mirror_target", "entities_version", "conflict_checked", "relations"];
68
70
  export type RestoreMetadataField = (typeof RESTORE_METADATA_FIELDS)[number];
69
71
  /** Per-field count of facts whose document did not carry that field. */
70
72
  export type RestoreMetadataMissing = Record<RestoreMetadataField, number>;
@@ -1,6 +1,6 @@
1
1
  /**
2
- * The memory adapter for the shared `@avantf/graph-recall` port
3
- * (docs/GRAPH_RECALL_LIBRARY.md §6, Phase 3b).
2
+ * The memory adapter for the shared `@avantf/graph-recall` port (Phase 3b). The port — and the
3
+ * obligations an adapter must honour — is documented in that package's README.
4
4
  *
5
5
  * WHY THIS FILE EXISTS. The library owns the traversal algorithms (one hop, reverse, two hops, AND,
6
6
  * neighbours) and knows nothing about SQLite; memory owns the storage and therefore must supply a
@@ -104,7 +104,13 @@ export interface SearchInput {
104
104
  * verify it, so it must come from the same backend.
105
105
  */
106
106
  queryVector?: Float32Array;
107
- /** Per-call output token budget; `0` = unlimited, omitted = `retriever.max_output_tokens`. */
107
+ /**
108
+ * Per-call output token budget; `0` = unlimited, omitted = `retriever.max_output_tokens`.
109
+ *
110
+ * A pure time recall (no `query`) is the one exception: omitting it there means NO budget at all.
111
+ * That page is already bounded by its own `limit` (50 by default), so a budget on top would only
112
+ * rewrite the text of rows the caller asked to see (see `timePage`).
113
+ */
108
114
  maxTokens?: number;
109
115
  /**
110
116
  * Relevance-floor profile for this query. Omitted = the default policy: the configured (strict)
@@ -158,17 +164,15 @@ export interface SearchInput {
158
164
  /**
159
165
  * P-08 / P-13 write-side extras on `add` / `update`.
160
166
  *
161
- * All three are optional and "omitted" has one meaning everywhere: do not write (a new row's
162
- * NULL/1 default, or the existing row's value on a duplicate) — the store never invents a source,
163
- * an event time or an end date.
167
+ * Both are optional and "omitted" has one meaning everywhere: do not write (a new row's
168
+ * NULL/1 default, or the existing row's value on a duplicate) — the store never invents a source
169
+ * or an event time.
164
170
  */
165
171
  export interface FactWriteOptions {
166
172
  /** P-08: `fact_sources` provenance. `kind:` prefix selects the kind, else `manual`. */
167
173
  sourceRef?: string;
168
174
  /** P-13: event time → `valid_from`. */
169
175
  eventDate?: string;
170
- /** P-13: known end → `valid_to`, WITHOUT archiving the row. */
171
- validUntil?: string;
172
176
  /**
173
177
  * The journal title (JOURNAL_TITLE_SEARCH §2.2/§11.1). `undefined` = not given, `null` = "no
174
178
  * title", a string = the (already normalized) title. On a revision an omitted title INHERITS the
@@ -242,7 +246,10 @@ export interface TitleInput {
242
246
  export interface MemorySearchInput extends Omit<SearchInput, 'query'> {
243
247
  /** The literal query. Omitted = a pure time window (then `time` is required). */
244
248
  query?: string;
245
- /** The exact page's first row (default 0). Only read when the literal pass matches. */
249
+ /**
250
+ * The page's first row (default 0). Read by the exact/facts passes and by the pure time window,
251
+ * where it walks the window one page at a time (see `timePage`).
252
+ */
246
253
  offset?: number;
247
254
  }
248
255
  export declare class MemoryStore {
@@ -331,7 +338,7 @@ export declare class MemoryStore {
331
338
  * and every existing assertion on it — is unchanged; only the warning's wording uses it.
332
339
  */
333
340
  private vectorSpaceCounts;
334
- add(content: string, kind?: MemoryKind, ttlDays?: number, opts?: FactWriteOptions): Promise<RememberResult>;
341
+ add(content: string, kind?: MemoryKind, opts?: FactWriteOptions): Promise<RememberResult>;
335
342
  /**
336
343
  * Write-path contradiction detection (DESIGN §11): check the fact that was just
337
344
  * written against the corpus, then report its FULL open-conflict set.
@@ -358,24 +365,16 @@ export declare class MemoryStore {
358
365
  private warnIndexFailure;
359
366
  /** Best-effort semantic indexing: encode the fact and add it to the vstore. */
360
367
  private maybeIndexSemantic;
361
- update(fact_id: number, content: string, kind?: MemoryKind, ttlDays?: number, opts?: FactWriteOptions): Promise<RememberResult>;
368
+ update(fact_id: number, content: string, kind?: MemoryKind, opts?: FactWriteOptions): Promise<RememberResult>;
362
369
  remove(fact_id: number, reason?: string): Promise<boolean>;
363
370
  archive(fact_id: number, reason?: string): Promise<boolean>;
364
371
  /**
365
- * Bring an archived fact back (spec §2.6): a `forgot` fact is lifted to
366
- * `recall_floor` (otherwise the very next tick would re-archive it, R4), the
367
- * idle clock is refreshed (otherwise an `idle`-archived fact dies again, R18),
372
+ * Bring an archived fact back (spec §2.6): a `forgot` fact is lifted to the frozen base
373
+ * `BASE_TRUST` (otherwise the very next tick would re-archive it, R4),
374
+ * `last_retrieved_at` is refreshed (observation only — no clock reads it any more),
368
375
  * and `settle_clock` moves to now so the downtime is not charged.
369
376
  */
370
377
  restore(fact_id: number): Promise<boolean>;
371
- helpful(fact_id: number): Promise<number | null>;
372
- unhelpful(fact_id: number): Promise<number | null>;
373
- /**
374
- * Explicit feedback (spec §2.4). Pinned/archived rows only move `helpful_count`
375
- * (R7); otherwise the decay is settled first, then `±feedback_delta`, pin at the
376
- * permanent threshold (snap to 1.0) or archive immediately at the forget line.
377
- */
378
- private applyFeedback;
379
378
  /** Permanent-memory administration (spec §6). */
380
379
  pin(fact_id: number): Promise<boolean>;
381
380
  unpin(fact_id: number): Promise<boolean>;
@@ -455,7 +454,37 @@ export declare class MemoryStore {
455
454
  private sweepConflicts;
456
455
  /** Async bootstrap the semantic backend (download/config model) without blocking. */
457
456
  warmupSemantic(): Promise<boolean>;
458
- /** List contradiction candidates (`resolved` 0 = open, 1 = closed with a recorded verdict). */
457
+ /**
458
+ * Point the semantic backend at a shared runtime-dependency root (the base provider's
459
+ * `runtime-deps` handle) before it warms. Best-effort by contract: a backend without external
460
+ * module resolution ignores it, and a missing root is reported by the backend itself.
461
+ */
462
+ useSemanticModuleDir(dir: string): void;
463
+ /**
464
+ * Point the vector store at a shared runtime-dependency root for its NATIVE binding
465
+ * (`hnswlib-node`), after the DSH plugin's native self-heal item is ready.
466
+ *
467
+ * Same best-effort contract as {@link useSemanticModuleDir}: a store with no native binding (the
468
+ * brute-force backend, or an `auto` store still on it) ignores it, and a store that already fell
469
+ * back to brute force rebuilds its tracked vectors under the new root. Nothing here decides whether
470
+ * a root should exist — the caller's dependency audit does.
471
+ */
472
+ useNativeModuleDir(dir: string): void;
473
+ /**
474
+ * Vectors currently in the LIVE index (sync, one map read).
475
+ *
476
+ * Exposed for the plugin's dependency audit: an `hnswlib-node` notice is conditioned on the corpus
477
+ * actually being large enough for an ANN index to matter (`min_vectors`), and a session line that
478
+ * asks "how big is the store right now" must not run a database query to answer.
479
+ */
480
+ liveVectorCount(): number;
481
+ /**
482
+ * List contradiction candidates (`resolved` 0 = open, 1 = closed with a recorded verdict).
483
+ *
484
+ * `detected_at` is mapped to the model face's instant shape here (the DAO hands back the raw UTC
485
+ * column): this payload is what the 记忆 tab's 未处理矛盾 table and the CLI both render, and it must
486
+ * not be the one surface still printing a zone-less stamp.
487
+ */
459
488
  listContradictions(opts?: {
460
489
  resolved?: number;
461
490
  kind?: MemoryKind;
@@ -695,10 +724,21 @@ export declare class MemoryStore {
695
724
  * ({@link timePage}, `mode:"time"`): the window is the whole question, so there is nothing to
696
725
  * match, plan or rank.
697
726
  *
698
- * The exact, facts and relation passes reinforce the facts they return, exactly like the fuzzy
699
- * pass does for its hits: all are recalls the caller actually reads. A caller that must stay
700
- * read-only passes `track: false` (the conditional hint's probe does). Exact, facts, relation and
701
- * time also honour the caller's output budget (their text is the caller's to bound).
727
+ * What a pass does to the rows it returns is one decision, and it is deliberately NOT uniform:
728
+ *
729
+ * - the RELATION and FUZZY passes REINFORCE them ({@link reinforce}): each answers a question
730
+ * the caller asked, so the rows are what it read;
731
+ * - the TIME, EXACT and FACTS passes record USAGE ONLY ({@link touchOnly}): they hand back a
732
+ * LIST the caller browses — page 2 of a listing is not a second recall — so "it was looked
733
+ * at" still moves `retrieval_count` / `last_retrieved_at` (the dormancy clock) while
734
+ * `trust_score` / `bonus_count` stay where they are.
735
+ *
736
+ * A caller that must stay read-only passes `track: false` (the conditional hint's probe does);
737
+ * that branch writes nothing at all, usage included. Exact, facts, relation and time also honour
738
+ * the caller's output budget (their text is the caller's to bound). The one difference is the
739
+ * DEFAULT: a time window applies no budget unless the caller names one — its page size (`limit`)
740
+ * is what bounds the list, and the budget would only rewrite rows the caller asked for (see
741
+ * {@link timePage}).
702
742
  *
703
743
  * It does NOT emit a retrieval-health event. Those counters describe the retrieval PIPELINE
704
744
  * (legs, fusion, semantic-live rate); none of an exact, facts, relation or time answer ran a leg,
@@ -714,9 +754,21 @@ export declare class MemoryStore {
714
754
  * happened that day" reads in the order it happened; `score` is the constant 1 exactly like the
715
755
  * exact page's, because there is no relevance number here for the model to compare.
716
756
  *
717
- * It honours `kind`/`source`, pages with the same `offset`/`limit` (default 10, cap 50) and the
718
- * same `max_tokens` budget as every other recall, and reinforces the rows it returns unless
719
- * `track: false` — a window the caller reads is a recall like any other.
757
+ * The PAGE is the contract's, not the caller's guess: `limit` omitted means one page of
758
+ * {@link TIME_DEFAULT_LIMIT} rows (the same 50 the contract caps an explicit page at), and an
759
+ * explicit `limit` (1-50) still wins. A window that holds more than one page is the normal case
760
+ * for a busy day, so the envelope does not leave the caller to infer the shortfall: it reports
761
+ * `remaining = total - (offset + hits.length)`, the count still unseen.
762
+ *
763
+ * The output budget is honoured only when the caller names it (`max_tokens`). Falling back to the
764
+ * configured `retrieval.max_output_tokens` here would rewrite the text of a window's rows behind a
765
+ * page the caller already bounded with `limit` — the page size is the knob for "how much of the
766
+ * list", and the budget stays a knob for a RANKED result (exact/facts/relation/hybrid unchanged).
767
+ * `max_tokens` (including the contract's `0` = unlimited) is passed through unchanged.
768
+ *
769
+ * It honours `kind`/`source`, and records usage ONLY for the rows it returns unless `track: false`
770
+ * ({@link touchOnly}). A window is listed so the caller can look through it, not recalled into an
771
+ * answer: the dormancy clock (`retrieval_count` / `last_retrieved_at`) moves, trust does not.
720
772
  */
721
773
  private timePage;
722
774
  /**
@@ -753,8 +805,8 @@ export declare class MemoryStore {
753
805
  * malformed window is the caller's error exactly as it is in the exact page;
754
806
  * - the page is FACT-level (`total`/`offset`/`limit`/`truncated`), and the caller's output budget
755
807
  * only shortens `content` — dropping a row would make `total` disagree with the page;
756
- * - the returned facts are reinforced unless `track: false`, like every other recall the caller
757
- * actually reads.
808
+ * - the returned facts get usage recorded but NO reinforcement unless `track: false`: a facts
809
+ * page is a list the caller browses, so only the dormancy clock moves ({@link touchOnly}).
758
810
  */
759
811
  private factPage;
760
812
  /**
@@ -928,6 +980,17 @@ export declare class MemoryStore {
928
980
  flushHealth(): Promise<void>;
929
981
  /** Retrieval bookkeeping: `retrieval_count` + the "last used" clock (spec §2.5). */
930
982
  private touchUsage;
983
+ /**
984
+ * Record the USAGE of these facts and nothing else — no trust bonus, no quota, no
985
+ * `last_reinforced_at`.
986
+ *
987
+ * This is the paginated passes' half of {@link reinforce}: `timePage` / `exactContentPage` /
988
+ * `factPage` return a list the caller browses rather than a recall it acted on, so "someone
989
+ * looked at this row" still moves `retrieval_count` / `last_retrieved_at` (observation and
990
+ * export only — no lifecycle step reads that clock) while `trust_score` / `bonus_count` stay
991
+ * put.
992
+ */
993
+ private touchOnly;
931
994
  /**
932
995
  * Semantic leg. `queryVector` short-circuits the encode when the caller already
933
996
  * encoded this exact query (the cross-store router does, for both stores at once);
@@ -975,13 +1038,11 @@ export declare class MemoryStore {
975
1038
  /**
976
1039
  * Insert (or resolve to) a fact row.
977
1040
  *
978
- * `kind` / `ttlDays` are `undefined` when the CALLER specified nothing — the
979
- * distinction is load-bearing. A duplicate-content hit must not clobber the existing
980
- * row's values with the insert defaults: re-adding a fact without a kind used to
981
- * reset it to `general`, and reviving an archived one silently cleared an explicit TTL.
982
- * The rule for all three branches is therefore: an explicitly provided value lands, an
983
- * omitted one keeps what the row already has, and a brand-new row falls back to the
984
- * defaults (`general` / `0`).
1041
+ * `kind` is `undefined` when the CALLER specified nothing — the distinction is load-bearing.
1042
+ * A duplicate-content hit must not clobber the existing row's value with the insert default:
1043
+ * re-adding a fact without a kind used to reset it to `general`. The rule for all three
1044
+ * branches is therefore: an explicitly provided value lands, an omitted one keeps what the row
1045
+ * already has, and a brand-new row falls back to the default (`note`).
985
1046
  */
986
1047
  private persistFact;
987
1048
  /**
@@ -5,6 +5,16 @@ export declare const EXACT_MIN_LATIN = 3;
5
5
  /** The default page of an exact result, and its hard cap. */
6
6
  export declare const EXACT_DEFAULT_LIMIT = 10;
7
7
  export declare const EXACT_MAX_LIMIT = 50;
8
+ /**
9
+ * The default page of a PURE TIME window (`mem_recall.search` with no `query`), and its cap.
10
+ *
11
+ * A window is a list, not a ranked shortlist: the caller asked "what happened then", and one page of
12
+ * ten would silently answer a tenth of the day. It is therefore answered with the widest page the
13
+ * contract allows (`EXACT_MAX_LIMIT`), and the envelope states how much is left
14
+ * (`remaining`) instead of leaving the caller to infer it from `truncated`. Omitting `limit` never
15
+ * means "everything": a window that has outgrown one page is exactly when a cap matters.
16
+ */
17
+ export declare const TIME_DEFAULT_LIMIT = 50;
8
18
  /**
9
19
  * The widest literal match set that still counts as an ANSWER (P1-2, REVIEW-2026-10-24).
10
20
  *
@@ -107,7 +107,7 @@ export declare function timeParams(range: TimeRange | undefined): (string | numb
107
107
  */
108
108
  export declare function dayInZone(ms: number, timeZone?: string): string;
109
109
  /**
110
- * One stored `valid_from`/`valid_to` string → epoch ms, using the SAME grammar as the filters.
110
+ * One stored event-time (`valid_from`) string → epoch ms, using the SAME grammar as the filters.
111
111
  *
112
112
  * This is what the write path stamps into `valid_from_ms` and what migration 12's backfill applies to
113
113
  * existing rows, so "the event time a caller wrote" and "the event time a window compares" can never
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The trust heartbeat: the interval beat that advances the active-day clock and performs the
3
- * settle / forget / idle / purge sweeps (TRUST_MODEL.md §5), shared by the DSH plugin and the MCP
3
+ * settle / forget / purge sweeps (TRUST_MODEL.md §5), shared by the DSH plugin and the MCP
4
4
  * server.
5
5
  *
6
6
  * The two surfaces used to install it separately, and the copies drifted in exactly the places a