pi-memory-evolution 0.3.2 → 0.4.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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to pi-memory-evolution are documented here.
4
4
 
5
+ ## [0.4.0](https://github.com/btnalit/pi-memory-evolution/compare/v0.3.2...v0.4.0) (2026-09-10)
6
+
7
+
8
+ ### Features
9
+
10
+ * confirm memories from evidence already in hand, and let every kind go dormant ([#28](https://github.com/btnalit/pi-memory-evolution/issues/28)) ([150bdbd](https://github.com/btnalit/pi-memory-evolution/commit/150bdbd82a86b57eb43d1197251411d2bba35b3c))
11
+
5
12
  ## [0.3.2](https://github.com/btnalit/pi-memory-evolution/compare/v0.3.1...v0.3.2) (2026-09-10)
6
13
 
7
14
 
@@ -83,18 +83,38 @@ refresh is required. All times are numeric and negative ages clamp to zero.
83
83
  freshness = floor + (1 - floor) * 2 ** (-ageDays / halfLifeDays)
84
84
  ```
85
85
 
86
- | Kind | Half-life of the decaying portion | Floor | Hard recall expiry |
86
+ | Kind | Half-life of the decaying portion | Floor | Dormant after |
87
87
  |---|---:|---:|---|
88
- | project_state | 3 days | 0.50 | after 7 days |
89
- | fact | 90 days | 0.75 | none |
90
- | decision | 180 days | 0.85 | none |
91
- | preference | 365 days | 0.95 | none |
92
-
93
- Pinning sets freshness to 1 and exempts age expiry, but adds no credibility, cannot
94
- revive a conflict, and cannot bypass relevance. Facts/decisions/preferences never vanish
95
- just because they are old. The project-state cap remains unchanged so upgrading does not
96
- revive already expired project claims. This first step classifies by memory kind, not
97
- semantic subtypes such as completed/blocked tasks or volatile configuration facts.
88
+ | project_state | 3 days | 0.50 | 7 days |
89
+ | fact | 60 days | 0.50 | 180 days |
90
+ | decision | 120 days | 0.60 | 365 days |
91
+ | preference | 240 days | 0.70 | 730 days |
92
+
93
+ Age is not evidence of falsity, so time never deletes anything: only contradicting evidence
94
+ does, through `replaces`. **Dormant means no longer offered, not gone.** A dormant record is
95
+ still stored, still returned by an explicit recall that asks for it, and still shown to the
96
+ model as a replacement candidate which is the channel that revives it, with no human step.
97
+
98
+ Decay and dormancy run from the last time a record was **confirmed**, not the last time it was
99
+ edited. Two signals already present in a learning call supply that, so neither costs a request:
100
+ the model re-deriving content this origin already holds, and a record the host measured the
101
+ source to mention being left standing beside it. The second means *not disputed by evidence
102
+ that mentioned it* — never *verified* — so it feeds the clock and nothing else, and it is not
103
+ counted for `project_state`, whose states go stale silently, nor for `progress` sources, whose
104
+ candidates are host-nominated rather than measured. Confirmation never moves `updatedAt`: that
105
+ is the replacement authority gate, and moving it would make a refreshed record refuse a
106
+ legitimately older queued source. A known residual: a weak model that fails to supersede a
107
+ false record keeps its clock alive by leaving it standing. That is the existing supersession
108
+ failure, not a new one.
109
+
110
+ The floors used to be high because nothing could refresh a record, which left decay nearly
111
+ inert — a preference could lose at most 5 percentage points in its entire life. Reinforcement
112
+ is that missing mechanism, so the floors come down and freshness does real ranking work.
113
+
114
+ Pinning sets freshness to 1 and exempts a record from dormancy, but adds no credibility, cannot
115
+ revive a conflict, and cannot bypass relevance. The project-state horizon is unchanged, so
116
+ upgrading does not revive already dormant project claims. This first step classifies by memory
117
+ kind, not semantic subtypes such as completed/blocked tasks or volatile configuration facts.
98
118
 
99
119
  The existing lexical score and subject/literal/coverage/relative-cutoff gates run first.
100
120
  The 75% relative cutoff is based on **raw relevance**, not quality-adjusted scores.
package/docs/design.md CHANGED
@@ -112,11 +112,20 @@ are clipped with an ellipsis/hash suffix. Origins are provenance hints, not evid
112
112
  that another project's fact applies here. Identical content is deduplicated only within
113
113
  one origin: equal port/path text from different contexts can mean different facts.
114
114
 
115
- Forgotten/conflicted claims never recall. Unpinned project-state claims expire from recall
116
- after 7 days; facts/preferences/decisions have no automatic age deletion. All kinds have
117
- bounded gradual freshness decay, with separate half-lives/floors and pin exemption.
115
+ Forgotten/conflicted claims never recall. Every unpinned kind becomes **dormant** past its own
116
+ horizon 7 days for project state, 180 for facts, 365 for decisions, 730 for preferences which
117
+ stops it being offered for injection without deleting it: it stays stored, stays recallable on
118
+ request, and stays a replacement candidate, so later evidence can revive or retire it with no
119
+ human step. No kind has automatic age deletion. All kinds have bounded gradual freshness decay,
120
+ with separate half-lives/floors and pin exemption. Decay and dormancy run from the last
121
+ confirmation rather than the last edit; `updatedAt`, the replacement authority gate, never moves
122
+ for a confirmation.
118
123
  Pin/unpin, legacy annotation, explicit feedback and conflict resolution preserve the evidence
119
- date, and undo restores the prior date. Event history separately records when an operation occurred.
124
+ date, and undo restores the prior date. Undo also ignores the confirmation stamp when deciding
125
+ whether a record changed, and carries it forward rather than reverting it: confirmation writes no
126
+ event, so an event snapshot can never carry a later stamp, and comparing it would make every
127
+ confirmed record permanently un-undoable. Event history separately records when an operation
128
+ occurred.
120
129
 
121
130
  See [core-quality.md](core-quality.md) for the evidence contract, exact ranking policy,
122
131
  weaker-replacement guard, replay-safe feedback and read-only mid-task `memory_recall` tool.
package/docs/usage.md CHANGED
@@ -215,9 +215,16 @@ history, correction, pinning and undo rather than treating generated claims as v
215
215
  is withheld and its new variant quarantined, with history. Current user corrections
216
216
  and new tool-backed project progress can still update automatically; pins remain protected.
217
217
  - Freshness decreases smoothly by type: project state fastest, then facts, decisions,
218
- preferences. Stable kinds retain a nonzero floor and do not expire. Project states keep
219
- the seven-day safety cap; no upgrade revives old states. Read/search/injection, pinning,
220
- alias enrichment, feedback and conflict resolution do not reset the evidence clock.
218
+ preferences. Every kind retains a nonzero floor, and past its own horizon becomes
219
+ **dormant** no longer injected automatically, but still stored, still returned by the
220
+ `memory_recall` tool and by `/memory search`, and still offered to learning, so later evidence
221
+ revives or retires it without you doing anything. Dormancy governs what is pushed into a
222
+ session unprompted; it never makes a record unfindable for someone looking for it.
223
+ Project states keep the seven-day cap; no upgrade revives old states. Read/search/injection,
224
+ pinning, alias enrichment, feedback and conflict resolution never move the evidence date,
225
+ and nothing does except a replacement. The separate decay clock is moved only by a later
226
+ source that mentions a record and either leaves it standing or reaffirms it outright — never
227
+ for project states, whose seven-day cap nothing resets.
221
228
  - Ranking keeps **relevance, evidence, freshness and feedback separate**. Quality cannot
222
229
  rescue an unrelated/weak lexical match. `useful` is not `accurate`; neither is independent
223
230
  verification. Repeated retrieval or repeated positive feedback earns no cumulative boost.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-memory-evolution",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Memory that maintains itself. Pi learns what matters, injects what this session needs, and recalls the rest — nothing to configure, no commands to learn.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
package/src/index.ts CHANGED
@@ -195,7 +195,9 @@ export default async function memoryEvolution(pi: ExtensionAPI, dependencies: Me
195
195
  if (lifetime.signal.aborted || signal?.aborted) throw new Error("Memory recall cancelled");
196
196
  try {
197
197
  const query = resolveRecallQuery(redact(params.query));
198
- const result = retrieveMemories(query.query ? getStore().readMemories() : [], query);
198
+ // Asked for explicitly, so dormancy does not apply: it governs what is pushed into a session
199
+ // unprompted, not what can be found. Dormant records come back marked `aging`.
200
+ const result = retrieveMemories(query.query ? getStore().readMemories() : [], query, 3, Date.now(), { includeDormant: true });
199
201
  const text = buildRuntimeDigest(result.selected, query) ?? "No matching recallable memory. This is not proof the subject was never stored; try a specific subject or known alias, not arbitrary recent records.";
200
202
  return { content: [{ type: "text" as const, text }], details: { matches: text.split('\n').filter(line => line.startsWith('{')).length } };
201
203
  } catch { throw new Error("Memory recall failed; inspect /memory status. No memory update was performed."); }
@@ -249,7 +251,9 @@ export default async function memoryEvolution(pi: ExtensionAPI, dependencies: Me
249
251
  : operation === "list" && id === "here" ? scope : undefined);
250
252
  let pageInfo = "";
251
253
  if (operation === "show") memories = memories.filter((m) => m.id === id);
252
- else if (operation === "search") memories = selectRelevantMemories(memories, recallQuery([id, value].filter(Boolean).join(" ")), 10);
254
+ // An explicit search must find what is stored, including dormant records: dormancy stops a
255
+ // record being offered unprompted, and must never make it unfindable for someone looking.
256
+ else if (operation === "search") memories = selectRelevantMemories(memories, recallQuery([id, value].filter(Boolean).join(" ")), 10, Date.now(), { includeDormant: true });
253
257
  else {
254
258
  const filtered = id === "all" || id === "legacy" || id === "here";
255
259
  const pageText = (filtered ? value : id) || "1";
@@ -16,7 +16,9 @@ export function buildRuntimeDigest(memories: readonly DurableMemory[], prompt: R
16
16
  const text = excerpt(memory.content, prompt, 400);
17
17
  const line = JSON.stringify({ id: label(memory.id), kind: memory.kind, status: memory.status,
18
18
  origin: label(memory.scope), source: label(memory.evidence?.sourceId ?? memory.sourceEntryId), updated: memory.updatedAt.slice(0,10),
19
- evidence: `${quality.basis}/${quality.method}`, aging: quality.freshness < 0.85,
19
+ // The freshness anchor, shown only when later evidence actually moved it past the edit date.
20
+ ...(quality.confirmedAt.slice(0,10) > memory.updatedAt.slice(0,10) ? { confirmed: quality.confirmedAt.slice(0,10) } : {}),
21
+ evidence: `${quality.basis}/${quality.method}`, aging: quality.aging,
20
22
  ...(memory.feedback?.accuracy ? { assessment: memory.feedback.accuracy.verdict } : {}), text }) + "\n";
21
23
  if (Buffer.byteLength(digest + line) <= MAX_BYTES) digest += line;
22
24
  }
@@ -37,6 +37,11 @@ export interface DurableMemory {
37
37
  /** Exact superseded content, carried across explicit legacy adoption. */
38
38
  suppressedHashes?: string[];
39
39
  searchTerms?: string[];
40
+ /** When this record was last confirmed by later evidence, as distinct from last changed. Decay
41
+ * and dormancy run from it; `updatedAt` does not move, because it is the replacement authority
42
+ * gate and moving it would make a refreshed record refuse a legitimately older queued source.
43
+ * Missing on records nothing has reconfirmed, which simply means decay runs from `updatedAt`. */
44
+ reinforcedAt?: string;
40
45
  /** Host-assigned provenance; missing on old records means unknown, never verified. */
41
46
  evidence?: Evidence;
42
47
  feedback?: MemoryFeedback;
@@ -245,6 +250,25 @@ export class MemoryStore {
245
250
  private generation(scope: string): number {
246
251
  return Number(this.db.prepare("SELECT COALESCE(MAX(rowid),0) AS n FROM events WHERE scope=?").get(scope)!.n);
247
252
  }
253
+ /** Confirmation is not a change: no content, evidence, status or `updatedAt` moves, so it writes
254
+ * no event and creates no undo point - there is nothing to undo about having been mentioned. Only
255
+ * `reinforcedAt` moves, and only forward, so replay or an out-of-order source cannot roll it back.
256
+ * Pinned records are skipped because their freshness is already fixed at 1. */
257
+ private reinforce(ids: Iterable<string>, at: string): void {
258
+ const stamp = Date.parse(at);
259
+ if (!Number.isFinite(stamp)) return;
260
+ let touched = false;
261
+ for (const id of ids) {
262
+ const memory = this.get(id);
263
+ if (!memory || !active(memory) || memory.layer === "pinned") continue;
264
+ if (stamp <= Math.max(Date.parse(memory.updatedAt), Date.parse(memory.reinforcedAt ?? "") || 0)) continue;
265
+ const next = { ...memory, reinforcedAt: new Date(stamp).toISOString() };
266
+ if (!isMemory(next)) throw new Error("Invalid memory update");
267
+ this.db.prepare("UPDATE memories SET data=? WHERE id=?").run(JSON.stringify(next), id);
268
+ touched = true;
269
+ }
270
+ if (touched) this.cache.clear();
271
+ }
248
272
  private record(actor: string, reason: string, after: DurableMemory[], scope: string): string {
249
273
  const before = after.map((m) => this.get(m.id) ?? null);
250
274
  const at = new Date().toISOString();
@@ -393,6 +417,8 @@ export class MemoryStore {
393
417
  if (job?.state !== "running" || job.attempt !== run.attempt || this.generation(run.source.scope) !== run.generation) throw new EvolutionError("stale");
394
418
  const after = new Map<string, DurableMemory>();
395
419
  const targets = new Set<string>();
420
+ const confirmed = new Set<string>();
421
+ const reaffirmed = new Set<string>();
396
422
  let weakerConflicts = 0;
397
423
  const incoming = sourceEvidence(run.source, "model");
398
424
  // Two different things used to throw the same bare Error and land on write_rejected, which
@@ -438,6 +464,11 @@ export class MemoryStore {
438
464
  targets.add(old.id);
439
465
  if (claim.kind !== old.kind) broke('replaces_kind', index, 'kind');
440
466
  if (fingerprint(old.content) === fingerprint(claim.content)) {
467
+ // Naming a record and replacing it with itself is an explicit "this still holds", so it
468
+ // must count at least as much as leaving it standing silently. Without this, a summary
469
+ // source that reaffirms a record outright moves nothing while one that says nothing
470
+ // about it confirms it - the stronger signal worth less than the weaker one.
471
+ reaffirmed.add(old.id);
441
472
  if (run.source.kind !== "summary" && mayReplace(old, incoming)) after.set(old.id, { ...old, sourceEntryId: run.source.id,
442
473
  updatedAt: run.source.createdAt, evidence: incoming, status: "provisional", revision: old.revision + 1 });
443
474
  annotate(old, claim); continue;
@@ -473,7 +504,26 @@ export class MemoryStore {
473
504
  else annotate(run.memories.find((m) => m.kind === claim.kind && fingerprint(m.content) === fingerprint(claim.content)), claim);
474
505
  }
475
506
  }
507
+ // Shown, measured to be mentioned by this source, and either left standing or explicitly
508
+ // replaced by identical content: the model saw the record beside fresh evidence about the same
509
+ // terms and did not contradict it. That is "not disputed by evidence that mentioned it", not
510
+ // "verified" - enough to keep a record out of dormancy, never enough to raise its authority.
511
+ //
512
+ // The model producing content the store already holds is deliberately NOT a signal. The whole
513
+ // content of every candidate is in front of it and the prompt asks for aliases on unchanged
514
+ // records, so re-emitting one is the cheapest move available, not independent re-derivation.
515
+ // Progress sources are excluded because their candidates are host-nominated targets, not
516
+ // records measured to be mentioned. Today that guard is subsumed by the project_state rule
517
+ // below - `selectCandidates` only ever nominates project_state for a progress source - so it
518
+ // has no test of its own. It stays because the two rules answer different questions, and
519
+ // dropping it would make project_state's rule silently load-bearing for both.
520
+ // project_state is excluded for the same reason - states go stale silently, and silence is far
521
+ // too weak to keep resetting the one seven-day safety cap that actually does work.
522
+ if (run.source.kind !== "progress") for (const shown of run.candidates)
523
+ if (shown.kind !== "project_state" && (!targets.has(shown.id) || reaffirmed.has(shown.id))) confirmed.add(shown.id);
476
524
  const event = this.record("model", `${model}: ${run.source.id}${weakerConflicts ? `; weaker replacements withheld=${weakerConflicts}` : ""}`, [...after.values()], run.source.scope);
525
+ // After `record`, so a record this batch also changed is confirmed on top of that change.
526
+ this.reinforce(confirmed, run.source.createdAt);
477
527
  this.db.prepare("UPDATE sources SET state='done',lease=0,failures=0,output_failures=0,retry_at=0,failed_at=0,last_error='',diagnostic=? WHERE id=?")
478
528
  .run(JSON.stringify(diagnostic), run.source.id);
479
529
  finishCall(this.db, run.source.id, run.attempt, 'done', Date.now(), '', diagnostic);
@@ -656,10 +706,18 @@ export class MemoryStore {
656
706
  if (!row) throw new Error("Unknown event id");
657
707
  const event = parseEvent(row.data, id, row.scope);
658
708
  if (!event.after.length) throw new Error("Event has no memory changes");
709
+ // `reinforcedAt` is deliberately outside this comparison. Confirmation writes no event, so an
710
+ // event's snapshot can never carry a stamp written after it; comparing it would make every
711
+ // confirmed record permanently un-undoable. It is also not part of what an undo restores -
712
+ // there is nothing to undo about having been mentioned - so the current stamp is carried
713
+ // forward onto the restored record rather than reverted with it.
714
+ const settled = (memory: DurableMemory | undefined) => memory && JSON.stringify({ ...memory, reinforcedAt: undefined });
659
715
  const restored = event.after.map((after, i) => {
660
- if (JSON.stringify(this.get(after.id)) !== JSON.stringify(after)) throw new Error("Memory changed since this event; undo refused");
716
+ const current = this.get(after.id);
717
+ if (settled(current) !== settled(after)) throw new Error("Memory changed since this event; undo refused");
661
718
  this.block(after);
662
719
  return { ...(event.before[i] ?? { ...after, status: "forgotten" as const }), revision: after.revision + 1,
720
+ ...(current?.reinforcedAt ? { reinforcedAt: current.reinforcedAt } : {}),
663
721
  suppressedHashes: [...new Set([...(event.before[i]?.suppressedHashes ?? []), ...(after.suppressedHashes ?? []), fingerprint(after.content)])] };
664
722
  });
665
723
  return this.record("manual", `Undo ${id}`, restored, event.scope);
@@ -737,6 +795,7 @@ function isMemory(value: unknown): value is DurableMemory {
737
795
  && (m.suppressedHashes === undefined || (Array.isArray(m.suppressedHashes) && m.suppressedHashes.every((h) => typeof h === "string" && /^[a-f0-9]{24}$/u.test(h))))
738
796
  && typeof m.createdAt === "string" && typeof m.updatedAt === "string"
739
797
  && Number.isFinite(Date.parse(m.createdAt)) && Number.isFinite(Date.parse(m.updatedAt))
798
+ && (m.reinforcedAt === undefined || (typeof m.reinforcedAt === "string" && Number.isFinite(Date.parse(m.reinforcedAt))))
740
799
  && Number.isInteger(m.revision) && m.revision > 0 && ["durable", "pinned"].includes(m.layer)
741
800
  && ["provisional", "confirmed", "forgotten", "conflicted"].includes(m.status);
742
801
  }
@@ -56,26 +56,47 @@ export function mayReplace(old: DurableMemory, incoming: Evidence): boolean {
56
56
  }
57
57
 
58
58
  const DAY = 86400_000;
59
- /** No new expiry for stable facts/preferences/decisions and no revival of old states.
60
- * Floors preserve old useful knowledge; project states retain their seven-day safety cap. */
61
- export const AGING: Record<MemoryKind, { halfLifeDays: number; floor: number; expiresDays?: number }> = {
62
- project_state: { halfLifeDays: 3, floor: 0.5, expiresDays: 7 },
63
- fact: { halfLifeDays: 90, floor: 0.75 },
64
- decision: { halfLifeDays: 180, floor: 0.85 },
65
- preference: { halfLifeDays: 365, floor: 0.95 },
59
+ /** Age is not evidence of falsity. A preference nobody has restated in a year is almost certainly
60
+ * still true, and a fact nobody has restated in three months might be false — age alone cannot tell
61
+ * them apart, so time never deletes anything here. Only contradicting evidence does, through
62
+ * `replaces`. What time does is stop a record being *offered*: past `dormantDays` it is no longer
63
+ * injected, while staying stored, recallable on request and eligible as a replacement candidate, so
64
+ * a source that mentions it again revives it with no human step. See `reinforcedAt`.
65
+ *
66
+ * The horizons are reasoned, not fitted - the store is far too young to fit them to. A statement
67
+ * about in-flight work is worthless after a week. An environment fact nobody has confirmed in half
68
+ * a year is unreliable. A decision unreferenced for a year has usually been overtaken by unrecorded
69
+ * practice. A preference is the most persistent thing a user tells us, so it gets two years.
70
+ *
71
+ * The floors were high because nothing could refresh a record: they had to protect knowledge that
72
+ * had no other way to stay fresh, which left decay inert (a preference could lose at most 5pp in
73
+ * its whole life). Reinforcement is that other way, so the floors come down and decay does real
74
+ * ranking work again. A floor is what a never-reconfirmed record still counts for at its horizon. */
75
+ export const AGING: Record<MemoryKind, { halfLifeDays: number; floor: number; dormantDays: number }> = {
76
+ project_state: { halfLifeDays: 3, floor: 0.5, dormantDays: 7 },
77
+ fact: { halfLifeDays: 60, floor: 0.5, dormantDays: 180 },
78
+ decision: { halfLifeDays: 120, floor: 0.6, dormantDays: 365 },
79
+ preference: { halfLifeDays: 240, floor: 0.7, dormantDays: 730 },
66
80
  };
67
81
  export function memoryQuality(memory: DurableMemory, now = Date.now()) {
68
82
  const policy = AGING[memory.kind];
69
- const ageDays = Math.max(0, (now - Date.parse(memory.updatedAt)) / DAY);
83
+ // Decay runs from the last time the record was confirmed, not the last time it was edited.
84
+ // `updatedAt` remains the replacement authority gate and is never moved by confirmation.
85
+ const confirmed = Math.max(Date.parse(memory.updatedAt), Date.parse(memory.reinforcedAt ?? "") || 0);
86
+ const ageDays = Math.max(0, (now - confirmed) / DAY);
70
87
  const pinned = memory.layer === "pinned";
71
88
  const freshness = pinned ? 1 : policy.floor + (1 - policy.floor) * 2 ** (-ageDays / policy.halfLifeDays);
72
- const expired = !pinned && policy.expiresDays !== undefined && ageDays > policy.expiresDays;
89
+ const dormant = !pinned && ageDays > policy.dormantDays;
90
+ // Shown to the model as a caution once a record is halfway to dormancy, relative to its own
91
+ // horizon rather than a fixed freshness number, which the floors would make meaningless.
92
+ const aging = !pinned && ageDays > policy.dormantDays / 2;
73
93
  const basis = memory.evidence?.basis ?? "unknown";
74
94
  const priority = evidencePriority(memory.kind, memory.evidence?.basis);
75
95
  const evidenceWeight = 1 + priority * 0.04;
76
96
  // Last explicit verdict wins, not frequency. Usefulness never increases evidence priority.
77
97
  const utility = memory.feedback?.utility?.verdict === "useful" ? 1.05 : memory.feedback?.utility?.verdict === "unhelpful" ? 0.9 : 1;
78
98
  const accuracy = memory.feedback?.accuracy?.verdict === "accurate" ? 1.05 : memory.feedback?.accuracy?.verdict === "incorrect" ? 0.5 : 1;
79
- return { basis, method: memory.evidence?.method ?? "unknown", ageDays, freshness, expired,
99
+ return { basis, method: memory.evidence?.method ?? "unknown", ageDays, freshness, dormant, aging,
100
+ confirmedAt: new Date(confirmed).toISOString(),
80
101
  evidenceWeight, utility, accuracy, factor: freshness * evidenceWeight * utility * accuracy };
81
102
  }
@@ -12,7 +12,7 @@ function overlap(text: string, query: Set<string>): number {
12
12
 
13
13
  /** Relevance scores are NOT confidence/truth scores. No authority bonus for cwd,
14
14
  * legacy labels, source IDs or dates. Metadata can only help an explicit origin query. */
15
- type RecallOptions = { includeExpiredProjectState?: boolean };
15
+ type RecallOptions = { includeDormant?: boolean };
16
16
  type RankedMemory = { memory: DurableMemory; score: number; rankScore: number; quality: ReturnType<typeof memoryQuality>; coverage: number; matches: string[]; reason?: string };
17
17
  export interface RecallDiagnostics {
18
18
  mode: string;
@@ -34,7 +34,7 @@ function evaluate(memories: readonly DurableMemory[], prompt: RecallInput, now:
34
34
  const qualities = new Map(memories.map(m => [m.id, memoryQuality(m, now)]));
35
35
  const excludedReason = (m: DurableMemory) => ["forgotten", "conflicted"].includes(m.status) ? m.status
36
36
  : m.feedback?.accuracy?.verdict === "incorrect" ? "disputed"
37
- : !options.includeExpiredProjectState && qualities.get(m.id)!.expired ? "expired-project-state" : undefined;
37
+ : !options.includeDormant && qualities.get(m.id)!.dormant ? "dormant" : undefined;
38
38
  const active = memories.filter(m => !excludedReason(m));
39
39
  const diagnostics: RecallDiagnostics = { mode: plan.mode, query: [...query].slice(0, 32), context: [...context].slice(0, 32),
40
40
  eligible: active.length, excluded: memories.length - active.length, matched: 0, selected: [], candidates: [],