@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
@@ -8,6 +8,135 @@ import { z } from 'zod';
8
8
  * past the window SILENTLY, so every derivation has to agree on the number.
9
9
  */
10
10
  export declare const DEFAULT_MODEL_WINDOW_TOKENS = 512;
11
+ /** One audit entry. `kind` decides which probe runs, and which of `specifier`/`item` is required. */
12
+ export declare const DependencyAuditEntrySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
13
+ id: z.ZodString;
14
+ severity: z.ZodEnum<{
15
+ info: "info";
16
+ warning: "warning";
17
+ degraded: "degraded";
18
+ }>;
19
+ when: z.ZodEnum<{
20
+ degraded: "degraded";
21
+ failed: "failed";
22
+ missing: "missing";
23
+ }>;
24
+ min_vectors: z.ZodOptional<z.ZodNumber>;
25
+ body: z.ZodString;
26
+ kind: z.ZodLiteral<"module">;
27
+ specifier: z.ZodString;
28
+ }, z.core.$strip>, z.ZodObject<{
29
+ id: z.ZodString;
30
+ severity: z.ZodEnum<{
31
+ info: "info";
32
+ warning: "warning";
33
+ degraded: "degraded";
34
+ }>;
35
+ when: z.ZodEnum<{
36
+ degraded: "degraded";
37
+ failed: "failed";
38
+ missing: "missing";
39
+ }>;
40
+ min_vectors: z.ZodOptional<z.ZodNumber>;
41
+ body: z.ZodString;
42
+ kind: z.ZodLiteral<"runtime-deps">;
43
+ item: z.ZodString;
44
+ }, z.core.$strip>], "kind">;
45
+ /** One configured audit entry, discriminated by `kind`. */
46
+ export type DependencyAuditEntry = z.infer<typeof DependencyAuditEntrySchema>;
47
+ /**
48
+ * One `runtime-deps` declaration as SHIPPED WITH THE PLUGIN (`packages/plugin/res/runtime-deps.yaml`).
49
+ *
50
+ * WHY IT IS A RESOURCE AND NOT USER CONFIG. The package set, the sources, the attempts, the verify
51
+ * list and the audit sentences are facts about THIS BUILD: which module the local backend imports,
52
+ * which native bindings this release looks for, what a session should be told when one of them is
53
+ * absent. Leaving them to `configs/common.yaml` made every deployment responsible for keeping that
54
+ * list correct — a stale copy would install a module no code loads, or silently drop the audit — and
55
+ * the shipped defaults were only ever the plugin's own declaration anyway. The operator keeps the one
56
+ * thing that really is a per-deployment decision (the two `enabled` switches); the DATA travels in the
57
+ * package, next to the code that reads it.
58
+ *
59
+ * Validation is strict on purpose: the file is read from the INSTALL directory at mount, and a
60
+ * malformed one is a packaging bug. The plugin logs it and degrades (no self-heal item, no audit)
61
+ * rather than throwing out of the mount — see `packages/plugin/src/runtime_deps_resource.ts`.
62
+ *
63
+ * The schema is the SINGLE definition of the file's shape: the loader validates with it, and the
64
+ * plugin maps it to the base provider's objects field by field (the base never reads YAML).
65
+ */
66
+ export declare const RuntimeDepsResourceSchema: z.ZodObject<{
67
+ version: z.ZodLiteral<1>;
68
+ semantic: z.ZodObject<{
69
+ packages: z.ZodRecord<z.ZodString, z.ZodString>;
70
+ sources: z.ZodArray<z.ZodObject<{
71
+ id: z.ZodString;
72
+ registry: z.ZodDefault<z.ZodString>;
73
+ cache: z.ZodDefault<z.ZodString>;
74
+ }, z.core.$strip>>;
75
+ attempts: z.ZodArray<z.ZodObject<{
76
+ source: z.ZodString;
77
+ manager: z.ZodOptional<z.ZodEnum<{
78
+ npm: "npm";
79
+ pnpm: "pnpm";
80
+ }>>;
81
+ env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
82
+ allowScripts: z.ZodOptional<z.ZodBoolean>;
83
+ }, z.core.$strip>>;
84
+ verify: z.ZodArray<z.ZodString>;
85
+ }, z.core.$strip>;
86
+ native: z.ZodObject<{
87
+ packages: z.ZodRecord<z.ZodString, z.ZodString>;
88
+ sources: z.ZodArray<z.ZodObject<{
89
+ id: z.ZodString;
90
+ registry: z.ZodDefault<z.ZodString>;
91
+ cache: z.ZodDefault<z.ZodString>;
92
+ }, z.core.$strip>>;
93
+ attempts: z.ZodArray<z.ZodObject<{
94
+ source: z.ZodString;
95
+ manager: z.ZodOptional<z.ZodEnum<{
96
+ npm: "npm";
97
+ pnpm: "pnpm";
98
+ }>>;
99
+ env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
100
+ allowScripts: z.ZodOptional<z.ZodBoolean>;
101
+ }, z.core.$strip>>;
102
+ verify: z.ZodArray<z.ZodString>;
103
+ }, z.core.$strip>;
104
+ audit: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
105
+ id: z.ZodString;
106
+ severity: z.ZodEnum<{
107
+ info: "info";
108
+ warning: "warning";
109
+ degraded: "degraded";
110
+ }>;
111
+ when: z.ZodEnum<{
112
+ degraded: "degraded";
113
+ failed: "failed";
114
+ missing: "missing";
115
+ }>;
116
+ min_vectors: z.ZodOptional<z.ZodNumber>;
117
+ body: z.ZodString;
118
+ kind: z.ZodLiteral<"module">;
119
+ specifier: z.ZodString;
120
+ }, z.core.$strip>, z.ZodObject<{
121
+ id: z.ZodString;
122
+ severity: z.ZodEnum<{
123
+ info: "info";
124
+ warning: "warning";
125
+ degraded: "degraded";
126
+ }>;
127
+ when: z.ZodEnum<{
128
+ degraded: "degraded";
129
+ failed: "failed";
130
+ missing: "missing";
131
+ }>;
132
+ min_vectors: z.ZodOptional<z.ZodNumber>;
133
+ body: z.ZodString;
134
+ kind: z.ZodLiteral<"runtime-deps">;
135
+ item: z.ZodString;
136
+ }, z.core.$strip>], "kind">>;
137
+ }, z.core.$strip>;
138
+ /** The parsed `res/runtime-deps.yaml`. */
139
+ export type RuntimeDepsResource = z.infer<typeof RuntimeDepsResourceSchema>;
11
140
  /** The merged runtime config shared by the memory store and the retrieval stack. */
12
141
  export declare const ConfigSchema: z.ZodObject<{
13
142
  dataHome: z.ZodDefault<z.ZodString>;
@@ -20,6 +149,12 @@ export declare const ConfigSchema: z.ZodObject<{
20
149
  cache_dir: z.ZodDefault<z.ZodString>;
21
150
  auto_download: z.ZodDefault<z.ZodBoolean>;
22
151
  auto_migrate: z.ZodDefault<z.ZodBoolean>;
152
+ self_heal: z.ZodPrefault<z.ZodObject<{
153
+ enabled: z.ZodDefault<z.ZodBoolean>;
154
+ }, z.core.$strip>>;
155
+ native_self_heal: z.ZodPrefault<z.ZodObject<{
156
+ enabled: z.ZodDefault<z.ZodBoolean>;
157
+ }, z.core.$strip>>;
23
158
  }, z.core.$strip>>;
24
159
  vectorStore: z.ZodPrefault<z.ZodObject<{
25
160
  backend: z.ZodDefault<z.ZodString>;
@@ -47,20 +182,20 @@ export declare const ConfigSchema: z.ZodObject<{
47
182
  }, z.core.$strip>>;
48
183
  trust: z.ZodPrefault<z.ZodObject<{
49
184
  enabled: z.ZodDefault<z.ZodBoolean>;
50
- start: z.ZodDefault<z.ZodNumber>;
51
- decay_per_day: z.ZodDefault<z.ZodNumber>;
185
+ base_lifetime_days: z.ZodDefault<z.ZodNumber>;
186
+ max_retention_days: z.ZodDefault<z.ZodNumber>;
187
+ decay_shape: z.ZodDefault<z.ZodEnum<{
188
+ linear: "linear";
189
+ accelerating: "accelerating";
190
+ }>>;
191
+ decay_curvature: z.ZodDefault<z.ZodNumber>;
52
192
  forget_threshold: z.ZodDefault<z.ZodNumber>;
53
- permanent_threshold: z.ZodDefault<z.ZodNumber>;
54
- recall_floor: z.ZodDefault<z.ZodNumber>;
55
- recall_delta: z.ZodDefault<z.ZodNumber>;
193
+ recall_gain_below_base: z.ZodDefault<z.ZodNumber>;
194
+ recall_gain_above_base: z.ZodDefault<z.ZodNumber>;
56
195
  recall_daily_cap: z.ZodDefault<z.ZodNumber>;
57
196
  recall_marginal_decay: z.ZodDefault<z.ZodNumber>;
58
- recall_ceiling: z.ZodDefault<z.ZodNumber>;
59
- feedback_delta: z.ZodDefault<z.ZodNumber>;
60
- feedback_daily_cap: z.ZodDefault<z.ZodNumber>;
61
197
  inherit_trust_on_update: z.ZodDefault<z.ZodBoolean>;
62
198
  purge_skips_pinned: z.ZodDefault<z.ZodBoolean>;
63
- idle_calendar_days: z.ZodDefault<z.ZodNumber>;
64
199
  tick_max_facts: z.ZodDefault<z.ZodNumber>;
65
200
  presence: z.ZodPrefault<z.ZodObject<{
66
201
  mode: z.ZodDefault<z.ZodEnum<{
@@ -1,8 +1,10 @@
1
1
  export * from './types.js';
2
2
  export * from './config.js';
3
+ export * from './trust_curve.js';
3
4
  export * from './family.js';
4
5
  export * from './tools.js';
5
6
  export * from './remote.js';
6
7
  export * from './log.js';
7
8
  export * from './env.js';
8
9
  export * from './text.js';
10
+ export * from './time.js';
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The ONE place a stored timestamp turns into a string a human (or a model) reads.
3
+ *
4
+ * ── THE BUG THIS MODULE EXISTS FOR ───────────────────────────────────────────────────────────────
5
+ *
6
+ * `facts.created_at` / `updated_at` / `archived_at` (and `contradiction_log.detected_at`, and the
7
+ * retrieval-health stamp) are written by SQLite's `DEFAULT CURRENT_TIMESTAMP`, which is **UTC**, and
8
+ * `store/memory.ts` used to hand the raw text up to every surface. Two consequences, both reported as
9
+ * "记忆时间显示不对":
10
+ *
11
+ * 1. the UI printed `2026-10-28 11:58:48` as if it were a local wall clock (Asia/Shanghai really
12
+ * meant `19:58:48`), because a zone-less string cannot say otherwise;
13
+ * 2. the MODEL received the same zone-less string next to `event_date`, which is a calendar DATE
14
+ * with no time and no zone. Two different kinds of value, one shape — the model had to guess
15
+ * which one it was looking at, and it cannot.
16
+ *
17
+ * ── THE SPLIT THIS MODULE ENFORCES ───────────────────────────────────────────────────────────────
18
+ *
19
+ * Storage stays UTC and stays byte-identical (`facts_io` export/import/snapshot is a compatibility
20
+ * surface and is deliberately NOT routed through here). The fix is in the two DISPLAY faces:
21
+ *
22
+ * - {@link formatOffsetIso} — the MODEL face (`mem_recall` hits, `mem_admin list/detail/
23
+ * contradictions`, `mem_admin stats`, the CLI and MCP that print those payloads): an ISO-8601
24
+ * instant **with an explicit offset**, e.g. `2026-10-28T19:58:48+08:00`, rendered in the host's
25
+ * local zone. The offset is part of the string, so the model needs no conversion and the value is
26
+ * never ambiguous again — while `event_date` stays `2026-10-28`, a calendar date with no zone.
27
+ * - {@link formatLocalHuman} — the HUMAN face (the 记忆 tab): `2026-10-28 19:58`, the same instant,
28
+ * local, to the minute.
29
+ *
30
+ * Both read the input through the SAME parser ({@link parseUtcTs}) and the same instant, so the two
31
+ * faces cannot disagree. The UI deliberately formats the model face's OWN string rather than
32
+ * re-deriving one from the raw column: the input it gets already carries an offset, `parseUtcTs`
33
+ * leaves an explicit offset alone, and there is therefore exactly ONE parse rule and ONE pair of
34
+ * renderers in the repository — not a second opinion in the browser.
35
+ *
36
+ * Dependency-free on purpose: this module is reachable from the DSH client bundle through the
37
+ * `@avantf/mem-contract/time` subpath (the bundle must not pull in the tool schemas / zod). It has no
38
+ * DSH host and no DB, so the engine, the CLI and the MCP face can all use it.
39
+ *
40
+ * @module contract/time
41
+ */
42
+ /**
43
+ * `'YYYY-MM-DD HH:MM:SS'` (SQLite UTC) or ISO → epoch ms; null/garbage → null.
44
+ *
45
+ * A stamp with NO zone designator is read as UTC, not as local time. `Date.parse` treats a zone-less
46
+ * ISO string as LOCAL, and everything in this store is written in UTC (`CURRENT_TIMESTAMP`, and
47
+ * {@link formatUtcTs}), so handing one over as-is would shift every quota window and every decay
48
+ * measurement by the host's UTC offset — a fact reinforced at 23:00 UTC+8 would look reinforced 8
49
+ * hours earlier, and the 24h cap would open or close at the wrong moment. An explicit `Z` or
50
+ * `±HH:MM` is left alone (that is what makes a `formatOffsetIso` output safe to feed back in).
51
+ */
52
+ export declare function parseUtcTs(ts: string | null | undefined): number | null;
53
+ /** Epoch ms → `'YYYY-MM-DD HH:MM:SS'` (the SQLite `CURRENT_TIMESTAMP` shape, UTC). */
54
+ export declare function formatUtcTs(ms: number): string;
55
+ /**
56
+ * The MODEL face: an instant as ISO-8601 with an explicit offset, in the host's local zone
57
+ * (`2026-10-28T19:58:48+08:00`).
58
+ *
59
+ * `null` — never `''` and never a fabricated stamp — when the value is absent or unreadable, so a
60
+ * caller keeps its own "unknown" spelling (`''` for `created_at`, `null` for `updated_at`) instead of
61
+ * rendering an empty time. `+00:00` (not `Z`) on a UTC host: the shape MUST carry a sign, which is
62
+ * what a caller regex-checks.
63
+ */
64
+ export declare function formatOffsetIso(ts: string | null | undefined): string | null;
65
+ /**
66
+ * The HUMAN face: the same instant as local wall-clock text to the minute (`2026-10-28 19:58`).
67
+ *
68
+ * Seconds are deliberately dropped: the list/detail lines and the health line are read at a glance,
69
+ * and `HH:MM` is the whole useful resolution there. `null` for an absent/unreadable value (same
70
+ * contract as {@link formatOffsetIso}), so a caller prints its own 「未知」/`—` placeholder.
71
+ *
72
+ * NOT for `event_date` / `valid_from`: those are calendar dates the CALLER wrote (`2026-10-28`), with
73
+ * no time and no zone. Running one through here would invent a midnight in the host's zone.
74
+ */
75
+ export declare function formatLocalHuman(ts: string | null | undefined): string | null;
@@ -91,7 +91,7 @@ export declare function modelFacingToolResult(toolKey: string, action: unknown,
91
91
  */
92
92
  export declare const EXPORT_STATUSES: readonly ["active", "archived", "all"];
93
93
  export type ExportStatus = StatusFilter;
94
- export declare const REMEMBER_ACTIONS: readonly ["add", "update", "remove", "helpful", "unhelpful"];
94
+ export declare const REMEMBER_ACTIONS: readonly ["add", "update", "remove"];
95
95
  export declare const RememberUnion: z.ZodDiscriminatedUnion<[z.ZodObject<{
96
96
  action: z.ZodLiteral<"add">;
97
97
  content: z.ZodString;
@@ -100,10 +100,8 @@ export declare const RememberUnion: z.ZodDiscriminatedUnion<[z.ZodObject<{
100
100
  note: "note";
101
101
  }>>;
102
102
  title: z.ZodOptional<z.ZodString>;
103
- ttl_days: z.ZodOptional<z.ZodNumber>;
104
103
  source_ref: z.ZodOptional<z.ZodString>;
105
104
  event_date: z.ZodOptional<z.ZodString>;
106
- valid_until: z.ZodOptional<z.ZodString>;
107
105
  relations: z.ZodOptional<z.ZodArray<z.ZodObject<{
108
106
  subject: z.ZodString;
109
107
  predicate: z.ZodString;
@@ -119,10 +117,8 @@ export declare const RememberUnion: z.ZodDiscriminatedUnion<[z.ZodObject<{
119
117
  }>>;
120
118
  title: z.ZodOptional<z.ZodString>;
121
119
  retitle: z.ZodOptional<z.ZodLiteral<true>>;
122
- ttl_days: z.ZodOptional<z.ZodNumber>;
123
120
  source_ref: z.ZodOptional<z.ZodString>;
124
121
  event_date: z.ZodOptional<z.ZodString>;
125
- valid_until: z.ZodOptional<z.ZodString>;
126
122
  relations: z.ZodOptional<z.ZodArray<z.ZodObject<{
127
123
  subject: z.ZodString;
128
124
  predicate: z.ZodString;
@@ -132,12 +128,6 @@ export declare const RememberUnion: z.ZodDiscriminatedUnion<[z.ZodObject<{
132
128
  action: z.ZodLiteral<"remove">;
133
129
  fact_id: z.ZodNumber;
134
130
  reason: z.ZodOptional<z.ZodString>;
135
- }, z.core.$strip>, z.ZodObject<{
136
- action: z.ZodLiteral<"helpful">;
137
- fact_id: z.ZodNumber;
138
- }, z.core.$strip>, z.ZodObject<{
139
- action: z.ZodLiteral<"unhelpful">;
140
- fact_id: z.ZodNumber;
141
131
  }, z.core.$strip>], "action">;
142
132
  /**
143
133
  * The runtime/CLI-visible write request. The base zod union keeps `kind` optional so the CLI's
@@ -146,9 +136,13 @@ export declare const RememberUnion: z.ZodDiscriminatedUnion<[z.ZodObject<{
146
136
  */
147
137
  export type RememberRequest = z.infer<typeof RememberUnion>;
148
138
  /**
149
- * The writes the model may make. `helpful` / `unhelpful` are operator feedback: the model cannot
150
- * observe the retention clocks they move, and a model that could would be able to "refresh" a fact by
151
- * voting on it — the exact behaviour the retention policy exists to prevent.
139
+ * The writes the model may make — `MODEL_REMEMBER_ACTIONS` and `REMEMBER_ACTIONS` are the same three
140
+ * again since the operator-side `helpful` / `unhelpful` actions were removed: a vote on a fact was a
141
+ * way to move its retention clock, and a model able to vote could "refresh" a fact by voting on it.
142
+ *
143
+ * Permanent is set by ONE route only: an explicit `pin` from a human (`mem_admin` / CLI / settings
144
+ * page). Do NOT re-add an action that promotes or demotes a fact from a write path — that is the
145
+ * behaviour the retention policy exists to prevent.
152
146
  */
153
147
  export declare const MODEL_REMEMBER_ACTIONS: readonly ["add", "update", "remove"];
154
148
  export declare const ModelRememberUnion: z.ZodDiscriminatedUnion<readonly z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>[], string>;
@@ -341,7 +335,7 @@ export declare const TOOL_SPECS: ToolSpec[];
341
335
  *
342
336
  * Descriptions are authored per action ("action=update 时必填"), so taking the
343
337
  * first branch's text verbatim told a model that `fact_id` only matters for
344
- * `update` although `remove/helpful/unhelpful` require it too. The action-clause
338
+ * `update` although `remove` requires it too. The action-clause
345
339
  * is stripped and replaced by the derived set of actions that make the field
346
340
  * required.
347
341
  *
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The trust decay curve — PURE functions, no IO, no config object of its own.
3
+ *
4
+ * This file is the ONE implementation of "how much trust is left after N active days" and of every
5
+ * inverse of it. `core/lifecycle/trust.ts` (read path, recall reinforcement), the tick's settle
6
+ * statement (`db/dao/facts.ts`) and the config's load-time validation (`config.ts`) all delegate
7
+ * here, so a change of shape cannot leave one of them behind (docs/TRUST_MODEL.md §2.2, DESIGN §18).
8
+ *
9
+ * It lives in the CONTRACT package because the load-time validation needs the same math, and the
10
+ * dependency direction is core → contract, never the reverse.
11
+ *
12
+ * ## The two shapes
13
+ *
14
+ * `linear` (TRUST_MODEL D7, the P3 equivalent tier): trust falls by a constant amount per active
15
+ * day, `slope = BASE_TRUST / base_lifetime_days` ⇒ a fact that starts at `BASE_TRUST` reaches 0
16
+ * after exactly `base_lifetime_days` active days.
17
+ *
18
+ * `accelerating` (the v2 target shape): `dT/dt = −λ·(a + 1 − T)` — the closer to 1, the slower the
19
+ * decay, so "almost permanent" facts really linger. Closed form (plan §3.1):
20
+ *
21
+ * T(t) = (1 + a) − ((1 + a) − T₀)·e^(λt) λ = ln((1 + a)/(1 + a − BASE_TRUST)) / B
22
+ *
23
+ * Note the POSITIVE exponent: `u = a + 1 − T` grows, so `e^(λt)` is the decay direction here (the
24
+ * plan writes it this way, and the closed-form test pins it). `a = decay_curvature`.
25
+ *
26
+ * ## Units
27
+ *
28
+ * `days` is ALWAYS the ACTIVE-DAY counter (`avantf_stats.trust_clock`), never wall time.
29
+ */
30
+ /**
31
+ * The frozen anchor (TRUST_MODEL D20): a new fact's initial trust, the trust value the lifetime
32
+ * anchor is defined at (`daysToZero(BASE_TRUST) === base_lifetime_days`), and the divider between
33
+ * the two recall-reinforcement segments. One constant on purpose — the three uses must never be
34
+ * configurable apart, or "a new fact lives 90 active days" stops being true.
35
+ */
36
+ export declare const BASE_TRUST = 0.5;
37
+ /** The decay shapes `decay_shape` accepts. */
38
+ export declare const DECAY_SHAPES: readonly ["linear", "accelerating"];
39
+ export type DecayShape = (typeof DECAY_SHAPES)[number];
40
+ /** The curve parameters, flattened from the `trust` config section (no config import: structural). */
41
+ export interface TrustCurve {
42
+ shape: DecayShape;
43
+ /** `decay_curvature` — the floor term `a` of the accelerating shape; unused by `linear`. */
44
+ curvature: number;
45
+ /** `base_lifetime_days` — the active days `BASE_TRUST → 0` takes. */
46
+ baseDays: number;
47
+ }
48
+ /** Read the curve out of the `trust` section (the single mapping from keys to math). */
49
+ export declare function curveOf(trust: {
50
+ decay_shape: DecayShape;
51
+ decay_curvature: number;
52
+ base_lifetime_days: number;
53
+ }): TrustCurve;
54
+ /**
55
+ * Trust units lost per active day — the LINEAR slope, `BASE_TRUST / base_lifetime_days`.
56
+ *
57
+ * `linear` is the only shape with a constant slope; for `accelerating` this returns the LOCAL slope
58
+ * at the anchor `T = BASE_TRUST` (`λ·(BASE_TRUST + a)`). It exists as the building block of the
59
+ * `linear` branch of {@link trustAfterDays} / {@link ceilingForDays} and for the tests that pin the
60
+ * anchor; it is deliberately NOT a scalar stand-in for the curve — anything that needs "how much
61
+ * trust is left after N days" uses {@link trustAfterDays}, and anything that needs "N days from a
62
+ * threshold" uses {@link ceilingForDays}. (An earlier caller — `trust_diagnose.forgetting_soon` —
63
+ * used it as a linear rate and was wrong once `accelerating` became the default.)
64
+ */
65
+ export declare function slopeOf(shape: DecayShape, curvature: number, baseDays: number): number;
66
+ /**
67
+ * `λ = ln((1 + a) / (1 + a − BASE_TRUST)) / baseDays` — pinned so the anchor holds:
68
+ * `λ·baseDays = ln((1 + a)/(0.5 + a))`, i.e. `daysToZero(BASE_TRUST) === baseDays` for ANY `a`.
69
+ */
70
+ export declare function lambdaFor(curvature: number, baseDays: number): number;
71
+ /**
72
+ * `T − days·slope` (linear) or the closed form `(1 + a) − ((1 + a) − T)·e^(λ·days)`.
73
+ *
74
+ * UNCLAMPED on purpose: callers clamp to `[0, 1]` themselves (`effectiveTrust`'s read path and the
75
+ * settle write both must clamp, and a shared clamp here would hide a caller that forgot).
76
+ */
77
+ export declare function trustAfterDays(T: number, days: number, cfg: TrustCurve): number;
78
+ /**
79
+ * Active days between two trust values, `t(T0 → T1)`.
80
+ *
81
+ * `linear`: `(T0 − T1) / slope`; `accelerating`: `ln((1 + a − T1)/(1 + a − T0)) / λ`. Both are
82
+ * signed the natural way (a RISING target yields a negative number of days), so the formula is
83
+ * usable as an inverse without special cases. Returns `NaN` only for an out-of-domain input
84
+ * (`T > 1 + a` on the accelerating branch), which the config validation makes unreachable.
85
+ */
86
+ export declare function daysBetween(T0: number, T1: number, cfg: TrustCurve): number;
87
+ /** Active days until the value reaches 0 — `remainingDays`'s whole body. */
88
+ export declare function daysToZero(T: number, cfg: TrustCurve): number;
89
+ /**
90
+ * The trust ceiling a `max_retention_days` promises: after being reinforced up to `ceiling`, a fact
91
+ * survives `days` more active days.
92
+ *
93
+ * `linear`: `days · slope` (= `days / (2·baseDays)`); `accelerating`: `(1 + a)(1 − e^(−λ·days))`.
94
+ * This is an INVERSION (`daysToZero(ceilingForDays(R)) === R`), not a configuration value — the
95
+ * config has no `recall_ceiling` key any more (D16).
96
+ */
97
+ export declare function ceilingForDays(days: number, cfg: TrustCurve): number;
98
+ /**
99
+ * The inverse of {@link ceilingForDays}: how many active days a given ceiling is worth. Used by the
100
+ * deprecated-key warning (`config/loader.ts`) to translate an old `recall_ceiling` value, and by the
101
+ * validation messages.
102
+ */
103
+ export declare function retentionDaysForCeiling(ceiling: number, cfg: TrustCurve): number;
104
+ /**
105
+ * `R_max` of the accelerating shape — the days at which the ceiling reaches 1 (`ceiling → ∞`).
106
+ *
107
+ * R_max = baseDays · ln((1 + a)/a) / ln((1 + a)/(1 + a − BASE_TRUST))
108
+ *
109
+ * `max_retention_days` must stay strictly below it, or a reinforced fact would stop aging like
110
+ * `pinned` does (D14: permanent is set only by a person).
111
+ */
112
+ export declare function RMAX(curvature: number, baseDays: number): number;
113
+ /**
114
+ * The shape-aware upper bound on `max_retention_days`: the days at which the ceiling hits 1.
115
+ *
116
+ * `linear`: `2·baseDays` (the ceiling is `days/(2·baseDays)`); `accelerating`: {@link RMAX}. Both
117
+ * are STRICT bounds — at the bound the ceiling is exactly 1, which `superRefine` rejects.
118
+ */
119
+ export declare function retentionDaysLimit(shape: DecayShape, curvature: number, baseDays: number): number;
@@ -74,6 +74,25 @@ export type StatusFilter = (typeof STATUS_FILTERS)[number];
74
74
  * SEARCH query shares the bound because it is compared against the stored title with `=`/`LIKE`.
75
75
  */
76
76
  export declare const TITLE_MAX_CHARS = 120;
77
+ /**
78
+ * ── TWO KINDS OF TIME, TWO SHAPES (the "记忆时间显示不对" fix) ────────────────────────────────────
79
+ *
80
+ * Every INSTANT in a payload (`created_at`, `updated_at`, `archived_at`, `pinned_at`,
81
+ * `last_reinforced_at`, `bonus_window_at`, `detected_at`, the health stamp) is an ISO-8601 instant
82
+ * **with an explicit offset in the host's local zone** — `2026-10-28T19:58:48+08:00` — produced by
83
+ * `@avantf/mem-contract/time`'s `formatOffsetIso`. Storage is unchanged (SQLite `CURRENT_TIMESTAMP`
84
+ * is UTC, and the `facts_io` export/import/snapshot document keeps the SQLite text), so this is a
85
+ * RENDERING rule, not a storage one: the model gets an unambiguous instant it never has to convert,
86
+ * and a UI can `Date.parse` it directly.
87
+ *
88
+ * `event_date` / `valid_from` is the OTHER kind: a calendar DATE the caller wrote (`2026-10-28`),
89
+ * with no time and no zone. It is never passed through the instant renderer, and the two must not be
90
+ * confused: `created_at` answers "when was this recorded", `event_date` answers "as of which day is
91
+ * this true".
92
+ *
93
+ * A UI that shows these to a human formats them through `formatLocalHuman` (the same module): local
94
+ * wall clock to the minute, `2026-10-28 19:58`.
95
+ */
77
96
  export interface FactSummary {
78
97
  fact_id: number;
79
98
  content: string;
@@ -95,8 +114,15 @@ export interface FactSummary {
95
114
  pinned: boolean;
96
115
  /** Remaining ACTIVE days before `forgot`; null when pinned / non-active / trust disabled. */
97
116
  remaining_days: number | null;
117
+ /**
118
+ * @deprecated No writer since `helpful` / `unhelpful` were removed (2026-10-28). The column stays
119
+ * because it is part of the export/import/snapshot format, so an old document still round-trips;
120
+ * the value is whatever the row last carried. Do not build new logic on it.
121
+ */
98
122
  helpful_count: number;
123
+ /** When the memory was first recorded — an INSTANT, ISO-8601 with the host's offset. */
99
124
  created_at: string;
125
+ /** When it was archived — an INSTANT (ISO-8601 with offset), `null` while active. */
100
126
  archived_at: string | null;
101
127
  archive_reason: string | null;
102
128
  }
@@ -152,15 +178,21 @@ export interface FactDetail extends FactSummary {
152
178
  relations: FactRelation[];
153
179
  /** Active-day clock value at the last settle. */
154
180
  settle_clock: number;
181
+ /** When it was pinned — an INSTANT (ISO-8601 with offset), `null` when never pinned. */
155
182
  pinned_at: string | null;
183
+ /** When trust was last raised by a recall — an INSTANT (ISO-8601 with offset). */
156
184
  last_reinforced_at: string | null;
157
185
  /**
158
186
  * When this row was last CHANGED (edit/pin/unpin/reinforce/archive/restore). Retrieval and the
159
187
  * daily settle do not write it, so it is not `last_retrieved_at`; `null` on a legacy row.
188
+ *
189
+ * An INSTANT (ISO-8601 with the host's offset) — compare it with `created_at`, never with
190
+ * `valid_from`, which is a calendar date.
160
191
  */
161
192
  updated_at: string | null;
162
193
  /** Effective reinforcement events in the current 24h window (recall + feedback). */
163
194
  bonus_count: number;
195
+ /** Start of that window — an INSTANT (ISO-8601 with offset), `null` when no window is open. */
164
196
  bonus_window_at: string | null;
165
197
  /**
166
198
  * P-08: where this fact came from, as recorded rows. Empty when the writer gave no `source_ref`
@@ -168,12 +200,13 @@ export interface FactDetail extends FactSummary {
168
200
  */
169
201
  sources: FactSourceView[];
170
202
  /**
171
- * P-07: when the fact became true / stopped being true. `null` = unknown; `valid_to` set while
172
- * the row is still `active` means "still valid, known to end at T" (the state
173
- * `mem_remember valid_until` creates), while an archived row's `valid_to` is an audit stamp.
203
+ * P-07/P-13: when the fact became true (the caller's `event_date`) — a CALENDAR DATE
204
+ * (`YYYY-MM-DD`), with no time and no zone, exactly as the caller wrote it. `null` = unknown. It is
205
+ * the `event` time axis — NOT a validity window: v2 removed the "known end" (`valid_until`)
206
+ * entirely, so "this is no longer true" is expressed by `update` or `archive`, never by a deadline
207
+ * field. Do not compare it with `created_at`/`updated_at`: those are instants.
174
208
  */
175
209
  valid_from: string | null;
176
- valid_to: string | null;
177
210
  /**
178
211
  * P-07: the fact that supersedes this one, DERIVED from the existing `supersedes_id` link
179
212
  * (reverse lookup) — no redundant column. `null` when nothing supersedes it.
@@ -203,12 +236,15 @@ export interface RecallHit {
203
236
  * When this hit was first recorded: a fact's revision chain keeps the ORIGINAL row's
204
237
  * `created_at` across a supersede (`mem_remember update`), so this is when the memory was
205
238
  * first asserted, not when its current text was written (see `updated_at`).
239
+ *
240
+ * An INSTANT: ISO-8601 with the host's explicit offset (`2026-10-28T19:58:48+08:00`), so no
241
+ * conversion is needed and it can never be mistaken for `valid_from`'s calendar date.
206
242
  */
207
243
  created_at: string;
208
244
  /**
209
245
  * When this row was last CHANGED — an edit, pin/unpin, reinforcement, archive or restore.
210
246
  * Retrieval and the daily settle deliberately do not write it, so it never means "last
211
- * retrieved"; a copy can carry `null` on a legacy row.
247
+ * retrieved"; a copy can carry `null` on a legacy row. An INSTANT (ISO-8601 with offset).
212
248
  */
213
249
  updated_at: string | null;
214
250
  /**
@@ -235,12 +271,16 @@ export interface RecallHit {
235
271
  */
236
272
  final?: number;
237
273
  /**
238
- * P-07/P-13: the fact's event time / known end, serialized ONLY when non-null (the same
239
- * convention as {@link RecallHit.truncated}). `null` means "unknown", and omitting the key keeps
240
- * every pre-P-07 envelope byte-identical. Both are display/audit only — no leg filters on them.
274
+ * P-07/P-13: the fact's event time, serialized ONLY when non-null (the same convention as
275
+ * {@link RecallHit.truncated}). `null` means "unknown", and omitting the key keeps every pre-P-07
276
+ * envelope byte-identical. Display only — no leg filters on it.
277
+ *
278
+ * A CALENDAR DATE (`YYYY-MM-DD`), not an instant: the caller wrote it, it has no time and no zone,
279
+ * and it is deliberately NOT rendered in the host's zone. `created_at`/`updated_at` on the same
280
+ * hit are instants with an offset — the two answer different questions ("as of which day is this
281
+ * true" vs "when was it recorded").
241
282
  */
242
283
  valid_from?: string;
243
- valid_to?: string;
244
284
  /**
245
285
  * The fact's title, when it has one (journal entries normally do, notes usually do not).
246
286
  *
@@ -277,9 +317,14 @@ export interface TitleCandidate {
277
317
  matched_title: string;
278
318
  memory_kind: MemoryKind;
279
319
  status: FactStatus;
320
+ /** When the memory was first recorded — an INSTANT (ISO-8601 with the host's offset). */
280
321
  created_at: string;
322
+ /** When it was last changed — an INSTANT (ISO-8601 with offset), `null` when never revised. */
281
323
  updated_at: string | null;
282
- /** The fact's event time (`valid_from`), when it has one — the journal axis that tells two entries apart. */
324
+ /**
325
+ * The fact's event time (`valid_from`), when it has one — the journal axis that tells two entries
326
+ * apart. A CALENDAR DATE (`YYYY-MM-DD`), never an instant: `created_at` is the instant.
327
+ */
283
328
  event_date?: string;
284
329
  /** The first characters of the content, for judging "same transaction or not"; absent when `include_excerpt` is false. */
285
330
  excerpt?: string;
@@ -306,9 +351,20 @@ export type ExactSearchResult = ExactPageEnvelope & {
306
351
  * The window IS the question: no literal pass, no relation plan, no fuzzy legs. `hits` are the
307
352
  * active facts inside the window, newest first along `time.field`, and `score` is the same constant
308
353
  * 1 the exact page reports — there is no relevance number here to compare with anything.
354
+ *
355
+ * A window is a LIST, so its page is bounded like every other page (default 50, cap 50) and the
356
+ * envelope has to say how much of the list is not on it: {@link TimeSearchResult.remaining} is that
357
+ * number, spelled out instead of left for the caller to derive from `truncated`.
309
358
  */
310
359
  export type TimeSearchResult = ExactPageEnvelope & {
311
360
  mode: 'time';
361
+ /**
362
+ * How many rows of the window this answer does NOT show: `total - (offset + hits.length)`, never
363
+ * negative. `0` means the window is exhausted (either it fits, or the caller reached its last
364
+ * page). It is the count a caller needs to continue — `truncated` alone says "there is more"
365
+ * without saying how much.
366
+ */
367
+ remaining: number;
312
368
  hits: RecallHit[];
313
369
  };
314
370
  /**
@@ -369,7 +425,9 @@ export interface RelationEvidence {
369
425
  /** The memory layer, so a caller knows whether it is reading a journal or a note. */
370
426
  memory_kind: MemoryKind;
371
427
  title?: string;
428
+ /** When the memory was first recorded — an INSTANT (ISO-8601 with the host's offset). */
372
429
  created_at: string;
430
+ /** When it was last changed — an INSTANT (ISO-8601 with offset), `null` when never revised. */
373
431
  updated_at: string | null;
374
432
  /** Set when the output budget shortened `text` (never when the evidence itself was dropped). */
375
433
  truncated?: boolean;
@@ -436,9 +494,11 @@ export interface FactEvidence {
436
494
  /** `active` for every row this stage returns; carried so the field is explicit, not implied. */
437
495
  status: FactStatus;
438
496
  title?: string;
439
- /** The fact's event time (`valid_from`), when it has one. */
497
+ /** The fact's event time (`valid_from`) — a CALENDAR DATE (`YYYY-MM-DD`), when it has one. */
440
498
  event_date?: string;
499
+ /** When the memory was first recorded — an INSTANT (ISO-8601 with the host's offset). */
441
500
  created_at: string;
501
+ /** When it was last changed — an INSTANT (ISO-8601 with offset), `null` when never revised. */
442
502
  updated_at: string | null;
443
503
  /** Set when the output budget shortened `content` (never when the fact itself was dropped). */
444
504
  truncated?: boolean;
@@ -591,6 +651,7 @@ export interface ContradictionRecord {
591
651
  fact_a: number;
592
652
  fact_b: number;
593
653
  score: number;
654
+ /** When the pair was detected — an INSTANT (ISO-8601 with the host's offset). */
594
655
  detected_at: string;
595
656
  content_a: string;
596
657
  content_b: string;
@@ -684,6 +745,7 @@ export interface RetrievalHealthSummary {
684
745
  legs_capped: number;
685
746
  /** Candidates removed by a leg's relevance floor (see `RetrievalHealth`). */
686
747
  candidates_dropped_by_floor: number;
748
+ /** When a counter last moved — an INSTANT (ISO-8601 with the host's offset), `null` before any. */
687
749
  updated_at: string | null;
688
750
  by_kind: Record<string, KindHealth>;
689
751
  }
@@ -777,7 +839,6 @@ export interface TrustDiagnostic {
777
839
  forgetting_soon: number;
778
840
  reinforced_today: number;
779
841
  bonus_granted_today: number;
780
- idle_candidates: number;
781
842
  archived_by_reason: Record<string, number>;
782
843
  oldest_settle_clock: number | null;
783
844
  /**