claude-mem-lite 3.63.0 → 3.65.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.
@@ -0,0 +1,87 @@
1
+ // lib/inject-search-core.mjs — the retrieval-side shared core (P2-11, audit
2
+ // 2026-08-14; second cut D#123, 2026-08-16). Shared home for the three SQL
3
+ // atoms that kept drifting across hand-copied twins — first on the five
4
+ // injection-side surfaces, then extended across the remaining read surfaces
5
+ // (hook-context / hook-handoff / hook-optimize / mem-cli / search-scoring /
6
+ // tfidf / deep-search / recall-core / recent-core / timeline-core / stats-core /
7
+ // search-core incl. its sessions+events decay arms / maintain-core; the ledger
8
+ // test enforces the full list). Deliberate compressed-only singles (maintain
9
+ // UPDATE guards, stats noise-gauge counts, export tombstone toggles, session-own
10
+ // history) stay inline — see the ledger test's non-member notes:
11
+ //
12
+ // * live-row filter — the compressed+superseded pair whose omission was
13
+ // the superseded-invariant's recurring reopening
14
+ // (7th occurrence fixed in v3.63 H-1, write-side)
15
+ // * clamped recency decay — the MAX(0,…) age clamp (audit M-1: a far-future
16
+ // created_at from restore/import-jsonl EXP-overflowed
17
+ // and pinned that row #1; the fix reached
18
+ // search-engine but not the UPS/error-recall twins)
19
+ // * injection relevance — the full multiplicative chain incl. cite/noise
20
+ // behavior factors (audit M-3: wired on every auto
21
+ // surface, missing from the explicit-surface score)
22
+ //
23
+ // Each surface keeps its own deliberate pipeline composition (BM25-sort + JS
24
+ // scoring vs SQL full chain vs file-keyed sort — see #8786: per-surface
25
+ // asymmetries stay explicit); only the ATOMS are shared.
26
+ // tests/inject-search-core.test.mjs holds the ledger: consumer files must
27
+ // compose these builders, never re-inline copies (and the decay shape may not
28
+ // be hand-rolled anywhere, benchmark included).
29
+ //
30
+ // Lives under lib/ (not scripts/) so hook.mjs can statically import it without
31
+ // colliding with the installExtractedRelease scripts-dir rename (same constraint
32
+ // as lib/mem-override.mjs). Dependency-light by design: pre-tool-recall's
33
+ // standalone fast-path (#8447) already imports scoring-sql.mjs.
34
+
35
+ import {
36
+ OBS_BM25, TYPE_DECAY_CASE, TYPE_QUALITY_CASE,
37
+ noisePenaltyClause, citeFactorClause,
38
+ } from '../scoring-sql.mjs';
39
+
40
+ /**
41
+ * Live-row filter: rows a model-facing retrieval surface may return. Excludes
42
+ * compression tombstones (positive keeper ids AND -2 pending-purge) and
43
+ * superseded rows (a retracted lesson must never outrank its correction).
44
+ * @param {string} [alias='o'] table alias; '' for unqualified single-table queries
45
+ * @returns {string} SQL boolean expression (no leading AND)
46
+ */
47
+ export function liveObsFilterSql(alias = 'o') {
48
+ const a = alias ? `${alias}.` : '';
49
+ return `COALESCE(${a}compressed_into, 0) = 0 AND ${a}superseded_at IS NULL`;
50
+ }
51
+
52
+ /**
53
+ * Clamped recency-decay factor: (1 + EXP(-ln2 · age / halfLife)), age >= 0.
54
+ * The MAX(0,…) clamp is the M-1 fix — restore/import-jsonl accept arbitrary
55
+ * epochs, and an unclamped future timestamp makes the exponent large-positive →
56
+ * EXP overflows to +Infinity → that row sorts #1 for every query (and its score
57
+ * serializes as null). A future row reads as age 0 = max finite recency instead.
58
+ * Binds one `?` placeholder: the caller passes `now` (epoch ms) at that position.
59
+ * @param {object} opts
60
+ * @param {string} opts.tsExpr - SQL expression for the row's reference timestamp
61
+ * (e.g. 'o.created_at_epoch', or the created/last-accessed MAX search-engine uses)
62
+ * @param {string} [opts.halfLifeSql=TYPE_DECAY_CASE] - SQL expression for the
63
+ * half-life in ms (constant or per-type CASE; TYPE_DECAY_CASE assumes alias 'o')
64
+ * @returns {string} SQL numeric expression (parenthesized)
65
+ */
66
+ export function recencyDecaySql({ tsExpr, halfLifeSql = TYPE_DECAY_CASE }) {
67
+ return `(1.0 + EXP(-0.693 * MAX(0, ? - ${tsExpr}) / ${halfLifeSql}))`;
68
+ }
69
+
70
+ /**
71
+ * Injection relevance: the full multiplicative chain the prompt-time injection
72
+ * surface (UPS searchByFts) ranks by — BM25 × clamped type-decay × type-quality
73
+ * × importance × noise penalty × cite factor. Alias is fixed to 'o' because
74
+ * TYPE_DECAY_CASE / TYPE_QUALITY_CASE bake that alias in.
75
+ * Binds one `?` placeholder (the decay `now`), first in parameter order.
76
+ * @param {string} [alias='o'] must be 'o'
77
+ * @returns {string} SQL numeric expression
78
+ */
79
+ export function injectionRelevanceSql(alias = 'o') {
80
+ if (alias !== 'o') throw new Error('injectionRelevanceSql: alias must be "o" (TYPE_*_CASE bake it in)');
81
+ return `${OBS_BM25}
82
+ * ${recencyDecaySql({ tsExpr: 'o.created_at_epoch' })}
83
+ * ${TYPE_QUALITY_CASE}
84
+ * (0.5 + 0.5 * COALESCE(o.importance, 1))
85
+ * ${noisePenaltyClause('o')}
86
+ * ${citeFactorClause('o')}`;
87
+ }
@@ -0,0 +1,49 @@
1
+ // lib/injected-ids.mjs — file-name derivation for the cross-hook injected-ids
2
+ // dedup marker. Single source of truth for user-prompt-search.js (writer),
3
+ // pre-tool-recall.js (read/merge), and hook.mjs (path-A reader): all three must
4
+ // derive the same name or cross-hook dedup silently goes blind.
5
+ //
6
+ // D#120: M-6 session-keyed the marker's PAYLOAD but kept ONE file per project,
7
+ // so two concurrent CC windows full-replaced each other's marker — no dedup
8
+ // between them and `count` reset on every alternation (MAX_SESSION_INJECTIONS
9
+ // unreachable). One file per SESSION instead, mirroring
10
+ // pre-recall-cooldown-<session>.json in the same runtime dir. GC: session-start
11
+ // sweep in hook.mjs (24h mtime, same policy as the cooldown files).
12
+ //
13
+ // Lives under lib/ (not scripts/) so hook.mjs can statically import it without
14
+ // colliding with the scripts/ directory rename in installExtractedRelease —
15
+ // same constraint as lib/mem-override.mjs.
16
+
17
+ /**
18
+ * Runtime-dir FILE NAME for the injected-ids marker (no directory component).
19
+ * No sessionId → legacy project-keyed name (env-less harnesses, old callers).
20
+ * @param {string} project - inferProject() value (already filename-safe)
21
+ * @param {string} [sessionId] - CC session id
22
+ * @returns {string}
23
+ */
24
+ export function injectedIdsFileName(project, sessionId) {
25
+ const base = `.claude-mem-injected-${project}`;
26
+ if (!sessionId) return base;
27
+ const safe = String(sessionId).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 64);
28
+ return `${base}-${safe}`;
29
+ }
30
+
31
+ /**
32
+ * Runtime-dir FILE NAME for the SessionStart Key Context marker: the obs ids
33
+ * ACTUALLY rendered into the <claude-mem-context> File Lessons / Key Context
34
+ * sections (empty under quiet/adopted). handleUserPrompt reads it as its
35
+ * exclude-set — D#123 review C-1: excluding the injector's QUERY result instead
36
+ * of what was really shown suppressed <memory-context> injection outright on
37
+ * quiet/adopted projects, where Key Context never renders at all.
38
+ * Session-lifetime validity (no time window): the SessionStart block stays in
39
+ * context for the whole session. Swept with the same 24h GC as the marker above.
40
+ * @param {string} project - inferProject() value (already filename-safe)
41
+ * @param {string} [sessionId] - CC session id
42
+ * @returns {string}
43
+ */
44
+ export function keyContextIdsFileName(project, sessionId) {
45
+ const base = `.claude-mem-keyctx-${project}`;
46
+ if (!sessionId) return base;
47
+ const safe = String(sessionId).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 64);
48
+ return `${base}-${safe}`;
49
+ }
@@ -15,6 +15,7 @@
15
15
  import { COMPRESSED_PENDING_PURGE, computeMinHash, estimateJaccardFromMinHash, jaccardSimilarity } from '../utils.mjs';
16
16
  import { rebuildVocabulary, computeVector, _resetVocabCache, vectorsEnabled, vecTextForRow } from '../tfidf.mjs';
17
17
  import { DEDUP_JACCARD_THRESHOLD, MINHASH_PRE_THRESHOLD as MINHASH_PRE_THRESHOLD_SRC, FUZZY_DEDUP_THRESHOLD, FUZZY_BODY_THRESHOLD, MINHASH_PREFILTER } from './dedup-constants.mjs';
18
+ import { liveObsFilterSql } from './inject-search-core.mjs';
18
19
 
19
20
  export const STALE_AGE_MS = 30 * 86400000;
20
21
  export const OP_CAP = 1000;
@@ -132,9 +133,8 @@ export function recoverOrphanedChildren(db, { projectFilter = '', baseParams = [
132
133
  export function recoverBuriedLessons(db, { projectFilter = '', baseParams = [] } = {}) {
133
134
  return db.prepare(`
134
135
  UPDATE observations SET importance = 1
135
- WHERE COALESCE(compressed_into, 0) = 0
136
+ WHERE ${liveObsFilterSql('')}
136
137
  AND COALESCE(importance, 1) = 0
137
- AND superseded_at IS NULL
138
138
  AND lesson_learned IS NOT NULL AND lesson_learned <> '' AND lower(lesson_learned) <> 'none'
139
139
  ${projectFilter}
140
140
  `).run(...baseParams).changes;
@@ -453,7 +453,7 @@ export function rebuildVectors(db) {
453
453
  if (!vocab) return { ok: false, reason: 'no observations to build vocabulary from' };
454
454
  const allObs = db.prepare(`
455
455
  SELECT id, title, narrative, concepts, lesson_learned, search_aliases FROM observations
456
- WHERE COALESCE(compressed_into, 0) = 0 AND superseded_at IS NULL
456
+ WHERE ${liveObsFilterSql('')}
457
457
  `).all();
458
458
  let updated = 0;
459
459
  const insertStmt = db.prepare('INSERT OR REPLACE INTO observation_vectors (observation_id, vector, vocab_version, created_at_epoch) VALUES (?, ?, ?, ?)');
@@ -9,7 +9,7 @@
9
9
  // + vector writes in one db.transaction so a failure can't leave a partial row).
10
10
 
11
11
  import { getVocabulary, computeVector, vectorsEnabled } from '../tfidf.mjs';
12
- import { debugCatch, cjkBigrams } from '../utils.mjs';
12
+ import { debugCatch, cjkBigrams, scrubSecrets } from '../utils.mjs';
13
13
 
14
14
  // Canonical column order — must mirror the observations schema (schema.mjs).
15
15
  const OBS_COLUMNS = [
@@ -84,7 +84,9 @@ export function insertObservationVector(db, obsId, vecText) {
84
84
  * block — the same drift class #8614/#8639 closed for compress/maintain. Caller
85
85
  * owns the transaction (vector write is internally non-critical).
86
86
  */
87
- export function rebuildObservationDerived(db, obsId) {
87
+ // P2-12: internal-only since applyObsUpdate became the single update choke point
88
+ // (both faces previously imported this directly; un-exported per knip discipline).
89
+ function rebuildObservationDerived(db, obsId) {
88
90
  const row = db.prepare('SELECT title, subtitle, narrative, concepts, facts, lesson_learned, search_aliases FROM observations WHERE id = ?').get(obsId);
89
91
  if (!row) return;
90
92
  const base = [row.title, row.subtitle, row.narrative, row.concepts, row.facts, row.lesson_learned, row.search_aliases].filter(Boolean).join(' ');
@@ -93,3 +95,37 @@ export function rebuildObservationDerived(db, obsId) {
93
95
  db.prepare('UPDATE observations SET text = ? WHERE id = ?').run(textField, obsId);
94
96
  insertObservationVector(db, obsId, textField);
95
97
  }
98
+
99
+ // P2-12 (audit 2026-08-14): shared update mutation for the CLI `update` / MCP
100
+ // `mem_update` twin. Both faces previously built the SET list + ran the
101
+ // transaction + rebuild inline (byte-equivalent copies); each keeps its own
102
+ // validation front-end (CLI flag guards / MCP zod) and passes only the fields
103
+ // it accepted. String values are secret-scrubbed here — the single choke point,
104
+ // so a new face can't forget it (concepts had already slipped through once).
105
+ const UPDATABLE_OBS_COLS = ['title', 'narrative', 'type', 'importance', 'lesson_learned', 'concepts'];
106
+
107
+ /**
108
+ * Apply a validated field patch to one observation: UPDATE + derived-column
109
+ * rebuild (FTS text + vector) in one transaction.
110
+ * @param {import('better-sqlite3').Database} db
111
+ * @param {number} id - observation id (caller has verified existence)
112
+ * @param {object} fields - subset of {title, narrative, type, importance, lesson_learned, concepts}
113
+ * @returns {string[]} column names actually updated ([] = nothing to do, no write)
114
+ */
115
+ export function applyObsUpdate(db, id, fields) {
116
+ const updates = [];
117
+ const params = [];
118
+ for (const col of UPDATABLE_OBS_COLS) {
119
+ if (fields[col] !== undefined) {
120
+ updates.push(`${col} = ?`);
121
+ params.push(typeof fields[col] === 'string' ? scrubSecrets(fields[col]) : fields[col]);
122
+ }
123
+ }
124
+ if (updates.length === 0) return [];
125
+ params.push(id);
126
+ db.transaction(() => {
127
+ db.prepare(`UPDATE observations SET ${updates.join(', ')} WHERE id = ?`).run(...params);
128
+ rebuildObservationDerived(db, id);
129
+ })();
130
+ return updates.map((u) => u.split(' =')[0]);
131
+ }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { basename } from 'path';
9
9
  import { notLowSignalTitleClause } from '../utils.mjs';
10
+ import { liveObsFilterSql } from './inject-search-core.mjs';
10
11
 
11
12
  /**
12
13
  * Recall observations linked to a file (basename or full path). Returns
@@ -31,8 +32,7 @@ export function recallByFile(db, file, { limit = 10, includeNoise = false } = {}
31
32
  o.created_at, o.created_at_epoch, o.project
32
33
  FROM observations o
33
34
  JOIN observation_files of2 ON of2.obs_id = o.id
34
- WHERE COALESCE(o.compressed_into, 0) = 0
35
- AND o.superseded_at IS NULL
35
+ WHERE ${liveObsFilterSql('o')}
36
36
  AND (of2.filename = ? OR of2.filename LIKE ? ESCAPE '\\')
37
37
  ${noiseClause}
38
38
  ORDER BY o.created_at_epoch DESC
@@ -14,6 +14,8 @@
14
14
  // MCP wants `project`) — same convention as recall-core, so neither surface needs
15
15
  // its own SELECT list.
16
16
 
17
+ import { liveObsFilterSql } from './inject-search-core.mjs';
18
+
17
19
  const RECENT_COLS = 'id, type, title, subtitle, importance, project, created_at, created_at_epoch';
18
20
 
19
21
  // Upper bound on rows a single `recent` call may pull, shared so the cap can't
@@ -38,7 +40,7 @@ export const RECENT_MAX = 1000;
38
40
  */
39
41
  export function fetchRecent(db, { project = null, type = null, since = null, limit = 10 } = {}) {
40
42
  const params = [];
41
- const wheres = ['COALESCE(compressed_into, 0) = 0', 'superseded_at IS NULL'];
43
+ const wheres = [liveObsFilterSql('')];
42
44
  if (project) { wheres.push('project = ?'); params.push(project); }
43
45
  if (type) { wheres.push('type = ?'); params.push(type); }
44
46
  if (Number.isFinite(since)) { wheres.push('created_at_epoch >= ?'); params.push(since); }
@@ -29,6 +29,7 @@ import { cjkPrecisionOk, extractCjkLikePatterns } from '../nlp.mjs';
29
29
  import { computeTier } from '../tier.mjs';
30
30
  import { countSearchTotal, attachBodyTokens } from '../search-engine.mjs';
31
31
  import { notLowSignalTitleClause } from '../scoring-sql.mjs';
32
+ import { liveObsFilterSql, recencyDecaySql } from './inject-search-core.mjs';
32
33
 
33
34
  /** Sanitize a user query to FTS5 syntax; optionally force OR semantics. */
34
35
  export function buildSearchFtsQuery(query, { or = false } = {}) {
@@ -146,7 +147,7 @@ export function searchSessionsFts(db, { ftsQuery, project = null, projectBoost =
146
147
  return db.prepare(`
147
148
  SELECT s.id, s.request, s.completed, s.project, s.created_at, s.created_at_epoch,
148
149
  ${SESS_BM25}
149
- * (1.0 + EXP(-0.693 * MAX(0, ? - s.created_at_epoch) / ${DEFAULT_DECAY_HALF_LIFE_MS}.0))
150
+ * ${recencyDecaySql({ tsExpr: 's.created_at_epoch', halfLifeSql: `${DEFAULT_DECAY_HALF_LIFE_MS}.0` })}
150
151
  * (CASE WHEN ? IS NOT NULL AND s.project = ? THEN 2.0 ELSE 1.0 END) as score
151
152
  FROM session_summaries_fts
152
153
  JOIN session_summaries s ON session_summaries_fts.rowid = s.id
@@ -235,7 +236,7 @@ export function searchEventsFts(db, { ftsQuery, project = null, projectBoost = n
235
236
  return db.prepare(`
236
237
  SELECT e.id, e.event_type, e.title, e.body, e.project, e.importance, e.file_paths, e.created_at_epoch,
237
238
  ${EVT_BM25}
238
- * (1.0 + EXP(-0.693 * MAX(0, ? - e.created_at_epoch) / ${DEFAULT_DECAY_HALF_LIFE_MS}.0))
239
+ * ${recencyDecaySql({ tsExpr: 'e.created_at_epoch', halfLifeSql: `${DEFAULT_DECAY_HALF_LIFE_MS}.0` })}
239
240
  * (CASE WHEN ? IS NOT NULL AND e.project = ? THEN 2.0 ELSE 1.0 END) as score
240
241
  FROM events_fts
241
242
  JOIN events e ON events_fts.rowid = e.id
@@ -258,7 +259,16 @@ export function searchEventsFts(db, { ftsQuery, project = null, projectBoost = n
258
259
  // obs/event/session BM25 live on comparable scales (weighted bm25 × decay ×
259
260
  // project-boost), so the ratio is meaningful there; small-scale sources (prompts,
260
261
  // bm25 ≈ -1) can only be penalized by this comparison, never inflated — the safe
261
- // direction. Bands (ratio = |lone| / globalMaxAbs, first match wins):
262
+ // direction.
263
+ // D#121 (2026-08-16): obs FULL_SCORE additionally carries citeFactor (0.4–3.0) ×
264
+ // noisePenalty (0.2–1.0) — behavior state events/sessions never have. A heavily-
265
+ // cited obs can therefore widen globalMaxAbs and demote a lone event's band: this
266
+ // is ACCEPTED direction (cite is genuine relevance evidence), and it is BOUNDED —
267
+ // citeFactor caps at 3.0, so a lone hit that was the raw max keeps ratio >= 1/3
268
+ // and never sinks below the -0.5 neutral mid; noise only shrinks obs (can only
269
+ // IMPROVE the lone hit's band). Real-SQL pins: benchmark/events-pipeline-probes.mjs
270
+ // (cite-widening-bounded-3x / decisive-lone-event-survives-max-cite / noise-shrink).
271
+ // Bands (ratio = |lone| / globalMaxAbs, first match wins):
262
272
  // ratio ≥ 1 → -1.05 the lone hit IS the strongest raw match anywhere: rank it
263
273
  // strictly ahead of every normalized -1 (ties in raw
264
274
  // strength resolve toward the lone hit — the [-1, 0] band
@@ -598,7 +608,7 @@ export async function coreRunSearchPipeline(ctx, opts) {
598
608
 
599
609
  // ── Type-list fallback (MCP): obs_type set + 0 matches → list recent of that type ──
600
610
  if (obsTypeFallback && results.length === 0 && obsType) {
601
- const typeWheres = ['COALESCE(compressed_into, 0) = 0', 'superseded_at IS NULL', 'type = ?'];
611
+ const typeWheres = [liveObsFilterSql(''), 'type = ?'];
602
612
  // Mirror the FTS path's low-signal filter (buildObsFtsQuery): this fallback is still a
603
613
  // SEARCH surface, so degraded titles ("Modified X", "Error: …") must not lead it —
604
614
  // acute for obs_type='change', the noise band. (The no-query recent-listing in
@@ -10,6 +10,7 @@ import { inferProject } from '../utils.mjs';
10
10
  import { buildNotLowSignalSql } from './low-signal-patterns.mjs';
11
11
  import { TIER_CASE_SQL, tierSqlParams } from '../tier.mjs';
12
12
  import { computeNoiseGauge } from './stats-quality.mjs';
13
+ import { liveObsFilterSql } from './inject-search-core.mjs';
13
14
 
14
15
  /**
15
16
  * Compute the primary stats feed. Row shapes are returned exactly as the twin
@@ -98,7 +99,7 @@ export function computeStatsFeed(db, { project = null, days = 30, now = Date.now
98
99
  const tierDist = db.prepare(`
99
100
  SELECT tier, COUNT(*) as c FROM (
100
101
  SELECT ${TIER_CASE_SQL} as tier FROM observations
101
- WHERE COALESCE(compressed_into, 0) = 0 AND superseded_at IS NULL ${projectFilter}
102
+ WHERE ${liveObsFilterSql('')} ${projectFilter}
102
103
  ) GROUP BY tier ORDER BY tier
103
104
  `).all(...tdParams, ...baseParams);
104
105
  const tierMap = Object.fromEntries(tierDist.map((r) => [r.tier, r.c]));
@@ -14,6 +14,7 @@
14
14
  import { parseIdToken } from './id-routing.mjs';
15
15
  import { findFtsAnchor } from '../search-engine.mjs';
16
16
  import { sanitizeFtsQuery } from '../utils.mjs';
17
+ import { liveObsFilterSql } from './inject-search-core.mjs';
17
18
 
18
19
  const TIMELINE_COLS = 'id, type, title, subtitle, project, created_at, created_at_epoch';
19
20
 
@@ -21,7 +22,7 @@ const TIMELINE_COLS = 'id, type, title, subtitle, project, created_at, created_a
21
22
  function nearestObservation(db, epoch, project) {
22
23
  return db.prepare(`
23
24
  SELECT id FROM observations
24
- WHERE COALESCE(compressed_into, 0) = 0 AND superseded_at IS NULL ${project ? 'AND project = ?' : ''}
25
+ WHERE ${liveObsFilterSql('')} ${project ? 'AND project = ?' : ''}
25
26
  ORDER BY ABS(created_at_epoch - ?) ASC LIMIT 1
26
27
  `).get(...(project ? [project, epoch] : [epoch]));
27
28
  }
@@ -162,9 +163,9 @@ export function resolveQueryAnchor(db, queryStr, { project = null } = {}) {
162
163
 
163
164
  /** No-anchor fallback: most recent live (non-compressed, non-superseded) obs, newest first. */
164
165
  export function fetchRecentTimeline(db, { project = null, limit }) {
165
- // superseded_at IS NULL mirrors the before/after window legs (fetchTimelineWindow) and
166
- // every other read path — a superseded row must not lead the "most recent" timeline.
167
- const liveFilter = 'COALESCE(compressed_into, 0) = 0 AND superseded_at IS NULL';
166
+ // A superseded row must not lead the "most recent" timeline — same live-row
167
+ // invariant as the before/after window legs (fetchTimelineWindow).
168
+ const liveFilter = liveObsFilterSql('');
168
169
  const where = project ? `WHERE ${liveFilter} AND project = ?` : `WHERE ${liveFilter}`;
169
170
  const params = project ? [project, limit] : [limit];
170
171
  return db.prepare(`
@@ -200,7 +201,7 @@ export function fetchTimelineWindow(db, anchorId, { before, after, project = nul
200
201
  const beforeRows = db.prepare(`
201
202
  SELECT ${TIMELINE_COLS}
202
203
  FROM observations
203
- WHERE created_at_epoch < ? AND COALESCE(compressed_into, 0) = 0 AND superseded_at IS NULL ${projectFilter}
204
+ WHERE created_at_epoch < ? AND ${liveObsFilterSql('')} ${projectFilter}
204
205
  ORDER BY created_at_epoch DESC
205
206
  LIMIT ?
206
207
  `).all(anchorRow.created_at_epoch, ...baseParams, before).reverse();
@@ -208,7 +209,7 @@ export function fetchTimelineWindow(db, anchorId, { before, after, project = nul
208
209
  const afterRows = db.prepare(`
209
210
  SELECT ${TIMELINE_COLS}
210
211
  FROM observations
211
- WHERE created_at_epoch > ? AND COALESCE(compressed_into, 0) = 0 AND superseded_at IS NULL ${projectFilter}
212
+ WHERE created_at_epoch > ? AND ${liveObsFilterSql('')} ${projectFilter}
212
213
  ORDER BY created_at_epoch ASC
213
214
  LIMIT ?
214
215
  `).all(anchorRow.created_at_epoch, ...baseParams, after);