@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
|
@@ -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
|
|
127
|
-
* written fact, `remove` a flag
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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/
|
|
8
|
-
* library back exactly as it was" — which is only
|
|
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,
|
|
12
|
-
* the mirror bookkeeping. A non-empty target is REFUSED (never merged
|
|
13
|
-
* that silently unions two histories is a different, unreviewable
|
|
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", "
|
|
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>;
|
|
@@ -164,17 +164,15 @@ export interface SearchInput {
|
|
|
164
164
|
/**
|
|
165
165
|
* P-08 / P-13 write-side extras on `add` / `update`.
|
|
166
166
|
*
|
|
167
|
-
*
|
|
168
|
-
* NULL/1 default, or the existing row's value on a duplicate) — the store never invents a source
|
|
169
|
-
* an event time
|
|
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.
|
|
170
170
|
*/
|
|
171
171
|
export interface FactWriteOptions {
|
|
172
172
|
/** P-08: `fact_sources` provenance. `kind:` prefix selects the kind, else `manual`. */
|
|
173
173
|
sourceRef?: string;
|
|
174
174
|
/** P-13: event time → `valid_from`. */
|
|
175
175
|
eventDate?: string;
|
|
176
|
-
/** P-13: known end → `valid_to`, WITHOUT archiving the row. */
|
|
177
|
-
validUntil?: string;
|
|
178
176
|
/**
|
|
179
177
|
* The journal title (JOURNAL_TITLE_SEARCH §2.2/§11.1). `undefined` = not given, `null` = "no
|
|
180
178
|
* title", a string = the (already normalized) title. On a revision an omitted title INHERITS the
|
|
@@ -340,7 +338,7 @@ export declare class MemoryStore {
|
|
|
340
338
|
* and every existing assertion on it — is unchanged; only the warning's wording uses it.
|
|
341
339
|
*/
|
|
342
340
|
private vectorSpaceCounts;
|
|
343
|
-
add(content: string, kind?: MemoryKind,
|
|
341
|
+
add(content: string, kind?: MemoryKind, opts?: FactWriteOptions): Promise<RememberResult>;
|
|
344
342
|
/**
|
|
345
343
|
* Write-path contradiction detection (DESIGN §11): check the fact that was just
|
|
346
344
|
* written against the corpus, then report its FULL open-conflict set.
|
|
@@ -367,24 +365,16 @@ export declare class MemoryStore {
|
|
|
367
365
|
private warnIndexFailure;
|
|
368
366
|
/** Best-effort semantic indexing: encode the fact and add it to the vstore. */
|
|
369
367
|
private maybeIndexSemantic;
|
|
370
|
-
update(fact_id: number, content: string, kind?: MemoryKind,
|
|
368
|
+
update(fact_id: number, content: string, kind?: MemoryKind, opts?: FactWriteOptions): Promise<RememberResult>;
|
|
371
369
|
remove(fact_id: number, reason?: string): Promise<boolean>;
|
|
372
370
|
archive(fact_id: number, reason?: string): Promise<boolean>;
|
|
373
371
|
/**
|
|
374
|
-
* Bring an archived fact back (spec §2.6): a `forgot` fact is lifted to
|
|
375
|
-
* `
|
|
376
|
-
*
|
|
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),
|
|
377
375
|
* and `settle_clock` moves to now so the downtime is not charged.
|
|
378
376
|
*/
|
|
379
377
|
restore(fact_id: number): Promise<boolean>;
|
|
380
|
-
helpful(fact_id: number): Promise<number | null>;
|
|
381
|
-
unhelpful(fact_id: number): Promise<number | null>;
|
|
382
|
-
/**
|
|
383
|
-
* Explicit feedback (spec §2.4). Pinned/archived rows only move `helpful_count`
|
|
384
|
-
* (R7); otherwise the decay is settled first, then `±feedback_delta`, pin at the
|
|
385
|
-
* permanent threshold (snap to 1.0) or archive immediately at the forget line.
|
|
386
|
-
*/
|
|
387
|
-
private applyFeedback;
|
|
388
378
|
/** Permanent-memory administration (spec §6). */
|
|
389
379
|
pin(fact_id: number): Promise<boolean>;
|
|
390
380
|
unpin(fact_id: number): Promise<boolean>;
|
|
@@ -488,7 +478,13 @@ export declare class MemoryStore {
|
|
|
488
478
|
* asks "how big is the store right now" must not run a database query to answer.
|
|
489
479
|
*/
|
|
490
480
|
liveVectorCount(): number;
|
|
491
|
-
/**
|
|
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
|
+
*/
|
|
492
488
|
listContradictions(opts?: {
|
|
493
489
|
resolved?: number;
|
|
494
490
|
kind?: MemoryKind;
|
|
@@ -728,13 +724,21 @@ export declare class MemoryStore {
|
|
|
728
724
|
* ({@link timePage}, `mode:"time"`): the window is the whole question, so there is nothing to
|
|
729
725
|
* match, plan or rank.
|
|
730
726
|
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
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}).
|
|
738
742
|
*
|
|
739
743
|
* It does NOT emit a retrieval-health event. Those counters describe the retrieval PIPELINE
|
|
740
744
|
* (legs, fusion, semantic-live rate); none of an exact, facts, relation or time answer ran a leg,
|
|
@@ -762,8 +766,9 @@ export declare class MemoryStore {
|
|
|
762
766
|
* list", and the budget stays a knob for a RANKED result (exact/facts/relation/hybrid unchanged).
|
|
763
767
|
* `max_tokens` (including the contract's `0` = unlimited) is passed through unchanged.
|
|
764
768
|
*
|
|
765
|
-
* It honours `kind`/`source`, and
|
|
766
|
-
* the caller
|
|
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.
|
|
767
772
|
*/
|
|
768
773
|
private timePage;
|
|
769
774
|
/**
|
|
@@ -800,8 +805,8 @@ export declare class MemoryStore {
|
|
|
800
805
|
* malformed window is the caller's error exactly as it is in the exact page;
|
|
801
806
|
* - the page is FACT-level (`total`/`offset`/`limit`/`truncated`), and the caller's output budget
|
|
802
807
|
* only shortens `content` — dropping a row would make `total` disagree with the page;
|
|
803
|
-
* - the returned facts
|
|
804
|
-
*
|
|
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}).
|
|
805
810
|
*/
|
|
806
811
|
private factPage;
|
|
807
812
|
/**
|
|
@@ -975,6 +980,17 @@ export declare class MemoryStore {
|
|
|
975
980
|
flushHealth(): Promise<void>;
|
|
976
981
|
/** Retrieval bookkeeping: `retrieval_count` + the "last used" clock (spec §2.5). */
|
|
977
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;
|
|
978
994
|
/**
|
|
979
995
|
* Semantic leg. `queryVector` short-circuits the encode when the caller already
|
|
980
996
|
* encoded this exact query (the cross-store router does, for both stores at once);
|
|
@@ -1022,13 +1038,11 @@ export declare class MemoryStore {
|
|
|
1022
1038
|
/**
|
|
1023
1039
|
* Insert (or resolve to) a fact row.
|
|
1024
1040
|
*
|
|
1025
|
-
* `kind`
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1030
|
-
* omitted one keeps what the row already has, and a brand-new row falls back to the
|
|
1031
|
-
* 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`).
|
|
1032
1046
|
*/
|
|
1033
1047
|
private persistFact;
|
|
1034
1048
|
/**
|
|
@@ -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
|
|
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 /
|
|
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
|
|
@@ -182,20 +182,20 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
182
182
|
}, z.core.$strip>>;
|
|
183
183
|
trust: z.ZodPrefault<z.ZodObject<{
|
|
184
184
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
185
|
-
|
|
186
|
-
|
|
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>;
|
|
187
192
|
forget_threshold: z.ZodDefault<z.ZodNumber>;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
recall_delta: z.ZodDefault<z.ZodNumber>;
|
|
193
|
+
recall_gain_below_base: z.ZodDefault<z.ZodNumber>;
|
|
194
|
+
recall_gain_above_base: z.ZodDefault<z.ZodNumber>;
|
|
191
195
|
recall_daily_cap: z.ZodDefault<z.ZodNumber>;
|
|
192
196
|
recall_marginal_decay: z.ZodDefault<z.ZodNumber>;
|
|
193
|
-
recall_ceiling: z.ZodDefault<z.ZodNumber>;
|
|
194
|
-
feedback_delta: z.ZodDefault<z.ZodNumber>;
|
|
195
|
-
feedback_daily_cap: z.ZodDefault<z.ZodNumber>;
|
|
196
197
|
inherit_trust_on_update: z.ZodDefault<z.ZodBoolean>;
|
|
197
198
|
purge_skips_pinned: z.ZodDefault<z.ZodBoolean>;
|
|
198
|
-
idle_calendar_days: z.ZodDefault<z.ZodNumber>;
|
|
199
199
|
tick_max_facts: z.ZodDefault<z.ZodNumber>;
|
|
200
200
|
presence: z.ZodPrefault<z.ZodObject<{
|
|
201
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"
|
|
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
|
|
150
|
-
*
|
|
151
|
-
*
|
|
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
|
|
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;
|