@qearlyao/familiar 0.1.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 (72) hide show
  1. package/.env.example +31 -0
  2. package/HEARTBEAT.md +23 -0
  3. package/LICENSE +21 -0
  4. package/MEMORY.md +1 -0
  5. package/README.md +245 -0
  6. package/SOUL.md +13 -0
  7. package/USER.md +13 -0
  8. package/config.example.toml +221 -0
  9. package/dist/agent-events.js +167 -0
  10. package/dist/agent.js +590 -0
  11. package/dist/browser-tools.js +638 -0
  12. package/dist/chat-log.js +130 -0
  13. package/dist/cli.js +168 -0
  14. package/dist/config.js +804 -0
  15. package/dist/data-retention.js +54 -0
  16. package/dist/discord.js +1203 -0
  17. package/dist/generated-media.js +86 -0
  18. package/dist/image-derivatives.js +102 -0
  19. package/dist/image-gen.js +440 -0
  20. package/dist/inbound-attachments.js +266 -0
  21. package/dist/index.js +10 -0
  22. package/dist/media-understanding.js +120 -0
  23. package/dist/memory/diary/ambient-injector.js +180 -0
  24. package/dist/memory/diary/ambient.js +124 -0
  25. package/dist/memory/diary/chunks.js +231 -0
  26. package/dist/memory/diary/index.js +3 -0
  27. package/dist/memory/diary/indexer.js +93 -0
  28. package/dist/memory/doctor.js +250 -0
  29. package/dist/memory/index/chunk-indexer.js +151 -0
  30. package/dist/memory/index/embedding-provider.js +119 -0
  31. package/dist/memory/index/fts-query.js +18 -0
  32. package/dist/memory/index/retrieval.js +246 -0
  33. package/dist/memory/index/schema.js +157 -0
  34. package/dist/memory/index/store.js +513 -0
  35. package/dist/memory/index/vec.js +72 -0
  36. package/dist/memory/index/vector-codec.js +27 -0
  37. package/dist/memory/lcm/backfill.js +247 -0
  38. package/dist/memory/lcm/condense.js +146 -0
  39. package/dist/memory/lcm/context-transformer.js +662 -0
  40. package/dist/memory/lcm/context.js +421 -0
  41. package/dist/memory/lcm/eviction-score.js +38 -0
  42. package/dist/memory/lcm/index.js +6 -0
  43. package/dist/memory/lcm/indexer.js +200 -0
  44. package/dist/memory/lcm/normalize.js +235 -0
  45. package/dist/memory/lcm/schema.js +188 -0
  46. package/dist/memory/lcm/segment-manager.js +136 -0
  47. package/dist/memory/lcm/store.js +722 -0
  48. package/dist/memory/lcm/summarizer.js +258 -0
  49. package/dist/memory/lcm/types.js +1 -0
  50. package/dist/memory/operator.js +477 -0
  51. package/dist/memory/service.js +202 -0
  52. package/dist/memory/tools.js +205 -0
  53. package/dist/models.js +165 -0
  54. package/dist/persona.js +54 -0
  55. package/dist/runtime.js +493 -0
  56. package/dist/scheduler.js +200 -0
  57. package/dist/settings.js +116 -0
  58. package/dist/skills.js +38 -0
  59. package/dist/tts.js +143 -0
  60. package/dist/web-auth.js +105 -0
  61. package/dist/web-events.js +114 -0
  62. package/dist/web-http.js +29 -0
  63. package/dist/web-static.js +106 -0
  64. package/dist/web-tools.js +940 -0
  65. package/dist/web-types.js +2 -0
  66. package/dist/web.js +844 -0
  67. package/package.json +60 -0
  68. package/web/dist/assets/index-ClgkMgaq.css +2 -0
  69. package/web/dist/assets/index-Cu2QquuR.js +59 -0
  70. package/web/dist/favicon.svg +1 -0
  71. package/web/dist/icons.svg +24 -0
  72. package/web/dist/index.html +20 -0
@@ -0,0 +1,124 @@
1
+ import { retrieveMemory, } from "../index/retrieval.js";
2
+ import { DIARY_CHUNK_CORPUS } from "./chunks.js";
3
+ const DEFAULT_LIMIT = 4;
4
+ const DEFAULT_CANDIDATE_MULTIPLIER = 5;
5
+ const DEFAULT_HALF_LIFE_DAYS = 45;
6
+ const MAX_AMBIENT_SEMANTIC_DISTANCE = 0.38;
7
+ const DEFAULT_WEIGHTS = {
8
+ similarity: 1.0,
9
+ valence: 0.08,
10
+ intensity: 0.1,
11
+ recency: 0.08,
12
+ };
13
+ export async function retrieveAmbientDiary(options) {
14
+ const limit = positiveIntegerOrDefault(options.limit, DEFAULT_LIMIT);
15
+ const candidateLimit = positiveIntegerOrDefault(options.candidateLimit, Math.max(limit * DEFAULT_CANDIDATE_MULTIPLIER, limit));
16
+ const hits = await retrieveMemory({
17
+ query: options.query,
18
+ store: options.store,
19
+ embeddingProvider: options.embeddingProvider,
20
+ scope: { corpora: [DIARY_CHUNK_CORPUS] },
21
+ limit: candidateLimit,
22
+ candidateLimit,
23
+ useLexical: options.useLexical,
24
+ useSemantic: options.useSemantic,
25
+ signal: options.signal,
26
+ });
27
+ const weights = { ...DEFAULT_WEIGHTS, ...options.metadataBoosts, ...options.weights };
28
+ const now = options.now ?? new Date();
29
+ const halfLifeDays = positiveIntegerOrDefault(options.recencyHalfLifeDays, DEFAULT_HALF_LIFE_DAYS);
30
+ const scored = hits.map((hit) => scoreAmbientHit(hit, { now, halfLifeDays, weights }));
31
+ const relevant = scored.filter(hasAmbientRelevance);
32
+ debugAmbientHits(options.query, scored, relevant);
33
+ return relevant.sort(compareAmbientHits).slice(0, limit);
34
+ }
35
+ function hasAmbientRelevance(hit) {
36
+ if (hit.semanticScore === null)
37
+ return hit.lexicalRank !== null;
38
+ return hit.semanticScore <= MAX_AMBIENT_SEMANTIC_DISTANCE;
39
+ }
40
+ function debugAmbientHits(query, scored, relevant) {
41
+ if (!process.env.DEBUG?.split(",")
42
+ .map((part) => part.trim())
43
+ .includes("memory-ambient"))
44
+ return;
45
+ const relevantIds = new Set(relevant.map((hit) => hit.id));
46
+ console.error(JSON.stringify({
47
+ event: "ambient_diary_hits",
48
+ query,
49
+ maxSemanticDistance: MAX_AMBIENT_SEMANTIC_DISTANCE,
50
+ hits: scored.map((hit) => ({
51
+ id: hit.id,
52
+ passed: relevantIds.has(hit.id),
53
+ ambientScore: hit.ambientScore,
54
+ lexicalRank: hit.lexicalRank,
55
+ semanticRank: hit.semanticRank,
56
+ lexicalScore: hit.lexicalScore,
57
+ semanticScore: hit.semanticScore,
58
+ snippet: (hit.chunk.snippet || hit.chunk.text).slice(0, 120),
59
+ })),
60
+ }));
61
+ }
62
+ function scoreAmbientHit(hit, options) {
63
+ const similarity = hit.score * options.weights.similarity;
64
+ const valence = normalizeValence(metadataNumber(hit.chunk, "valence")) * options.weights.valence;
65
+ const intensity = normalizeUnit(metadataNumber(hit.chunk, "intensity")) * options.weights.intensity;
66
+ const recency = recencyScore(hit.chunk, options.now, options.halfLifeDays) * options.weights.recency;
67
+ const ambientScore = similarity + valence + intensity + recency;
68
+ return {
69
+ ...hit,
70
+ ambientScore,
71
+ boosts: { similarity, valence, intensity, recency },
72
+ };
73
+ }
74
+ function compareAmbientHits(a, b) {
75
+ return (b.ambientScore - a.ambientScore ||
76
+ b.score - a.score ||
77
+ (a.semanticRank ?? Number.POSITIVE_INFINITY) - (b.semanticRank ?? Number.POSITIVE_INFINITY) ||
78
+ (a.lexicalRank ?? Number.POSITIVE_INFINITY) - (b.lexicalRank ?? Number.POSITIVE_INFINITY) ||
79
+ a.id - b.id);
80
+ }
81
+ function recencyScore(chunk, now, halfLifeDays) {
82
+ const date = metadataDate(chunk) ?? timestampDate(chunk.createdAt);
83
+ if (!date)
84
+ return 0;
85
+ const ageMs = Math.max(0, now.getTime() - date.getTime());
86
+ const ageDays = ageMs / 86_400_000;
87
+ return 0.5 ** (ageDays / halfLifeDays);
88
+ }
89
+ function metadataDate(chunk) {
90
+ const value = chunk.metadata?.date;
91
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value))
92
+ return null;
93
+ const date = new Date(`${value}T00:00:00.000Z`);
94
+ return Number.isNaN(date.getTime()) ? null : date;
95
+ }
96
+ function timestampDate(value) {
97
+ if (!Number.isFinite(value) || value <= 0)
98
+ return null;
99
+ const milliseconds = value < 10_000_000_000 ? value * 1000 : value;
100
+ const date = new Date(milliseconds);
101
+ return Number.isNaN(date.getTime()) ? null : date;
102
+ }
103
+ function metadataNumber(chunk, key) {
104
+ const value = chunk.metadata?.[key];
105
+ if (typeof value === "number" && Number.isFinite(value))
106
+ return value;
107
+ if (typeof value === "string" && value.trim() && Number.isFinite(Number(value)))
108
+ return Number(value);
109
+ return null;
110
+ }
111
+ function normalizeValence(value) {
112
+ if (value === null)
113
+ return 0;
114
+ return Math.max(0, Math.min(1, value));
115
+ }
116
+ function normalizeUnit(value) {
117
+ if (value === null)
118
+ return 0;
119
+ const absolute = Math.abs(value);
120
+ return Math.max(0, Math.min(1, absolute > 1 ? absolute / 10 : absolute));
121
+ }
122
+ function positiveIntegerOrDefault(value, fallback) {
123
+ return value !== undefined && Number.isInteger(value) && value > 0 ? value : fallback;
124
+ }
@@ -0,0 +1,231 @@
1
+ import { basename } from "node:path";
2
+ export const DIARY_CHUNK_CORPUS = "diary_chunk";
3
+ const DEFAULT_MAX_CHARS = 2400;
4
+ const DIARY_DATE_RE = /^(\d{4}-\d{2}-\d{2})\.md$/;
5
+ const FRONTMATTER_RE = /^\s*---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/;
6
+ const METADATA_LINE_RE = /^\s*(?:<!--\s*)?@?([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*?)\s*(?:-->)?\s*$/;
7
+ const SUPPORTED_METADATA_KEYS = new Set(["date", "heading", "valence", "intensity"]);
8
+ export function chunkDiaryMarkdown(markdown, options = {}) {
9
+ const sourceId = options.sourceId ?? "diary.md";
10
+ const sourceRef = options.sourceRef ?? sourceId;
11
+ const { body, metadata: documentMetadata } = stripFrontmatter(markdown);
12
+ const baseMetadata = normalizeMetadata({
13
+ date: options.date ?? dateFromSourceId(sourceId),
14
+ ...documentMetadata,
15
+ ...options.metadata,
16
+ });
17
+ const sections = splitMarkdownSections(body);
18
+ const chunks = [];
19
+ const maxChars = positiveIntegerOrDefault(options.maxChars, DEFAULT_MAX_CHARS);
20
+ for (const section of sections) {
21
+ const { metadata: sectionMetadata, lines } = peelMetadataLines(section.lines);
22
+ const text = lines.join("\n").trim();
23
+ if (!text)
24
+ continue;
25
+ const metadata = normalizeMetadata({
26
+ ...baseMetadata,
27
+ ...(section.heading ? { heading: section.heading } : {}),
28
+ ...sectionMetadata,
29
+ });
30
+ for (const part of splitDiaryTextBlocks(text, maxChars)) {
31
+ chunks.push({
32
+ text: part,
33
+ chunkIndex: chunks.length,
34
+ sourceId,
35
+ sourceRef,
36
+ metadata,
37
+ snippet: buildSnippet(part, metadata),
38
+ });
39
+ }
40
+ }
41
+ return chunks;
42
+ }
43
+ export function diaryChunksToIndexInputs(chunks) {
44
+ return chunks.map((chunk) => ({
45
+ corpus: DIARY_CHUNK_CORPUS,
46
+ sourceId: chunk.sourceId,
47
+ sourceRef: chunk.sourceRef,
48
+ chunkIndex: chunk.chunkIndex,
49
+ text: chunk.text,
50
+ snippet: chunk.snippet,
51
+ metadata: chunk.metadata,
52
+ }));
53
+ }
54
+ export async function indexDiaryMarkdown(options) {
55
+ const sourceId = options.sourceId ?? basename(options.path);
56
+ const sourceRef = options.sourceRef ?? options.path;
57
+ const chunks = chunkDiaryMarkdown(options.markdown, { ...options, sourceId, sourceRef });
58
+ return options.indexer.replaceSource(DIARY_CHUNK_CORPUS, sourceId, diaryChunksToIndexInputs(chunks), options.signal);
59
+ }
60
+ function stripFrontmatter(markdown) {
61
+ const match = FRONTMATTER_RE.exec(markdown);
62
+ if (!match)
63
+ return { body: markdown, metadata: {} };
64
+ return {
65
+ body: markdown.slice(match[0].length),
66
+ metadata: parseMetadataBlock(match[1] ?? ""),
67
+ };
68
+ }
69
+ function splitMarkdownSections(markdown) {
70
+ const lines = markdown.replace(/\r\n/g, "\n").split("\n");
71
+ const sections = [{ lines: [], startLine: 1 }];
72
+ for (const [lineIndex, line] of lines.entries()) {
73
+ const heading = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
74
+ if (heading) {
75
+ const current = sections[sections.length - 1];
76
+ if (current?.lines.every((value) => !value.trim()))
77
+ current.lines = [];
78
+ sections.push({
79
+ heading: stripInlineMarkdown(heading[2] ?? ""),
80
+ level: (heading[1] ?? "").length,
81
+ lines: [line],
82
+ startLine: lineIndex + 1,
83
+ });
84
+ continue;
85
+ }
86
+ sections[sections.length - 1].lines.push(line);
87
+ }
88
+ return sections;
89
+ }
90
+ function peelMetadataLines(lines) {
91
+ const metadata = {};
92
+ const body = [];
93
+ let inMetadataLead = true;
94
+ for (const line of lines) {
95
+ if (isMarkdownHeading(line)) {
96
+ continue;
97
+ }
98
+ const parsed = parseMetadataLine(line);
99
+ if (inMetadataLead && parsed) {
100
+ metadata[parsed.key] = parsed.value;
101
+ continue;
102
+ }
103
+ if (line.trim())
104
+ inMetadataLead = false;
105
+ body.push(line);
106
+ }
107
+ return { metadata, lines: body };
108
+ }
109
+ function parseMetadataBlock(block) {
110
+ const metadata = {};
111
+ for (const line of block.split(/\r?\n/)) {
112
+ const parsed = parseMetadataLine(line);
113
+ if (parsed)
114
+ metadata[parsed.key] = parsed.value;
115
+ }
116
+ return metadata;
117
+ }
118
+ function parseMetadataLine(line) {
119
+ const match = METADATA_LINE_RE.exec(line);
120
+ if (!match)
121
+ return null;
122
+ const rawKey = (match[1] ?? "").trim();
123
+ const key = camelMetadataKey(rawKey);
124
+ if (!SUPPORTED_METADATA_KEYS.has(key))
125
+ return null;
126
+ const rawValue = (match[2] ?? "").trim();
127
+ return { key, value: parseMetadataValue(rawValue) };
128
+ }
129
+ function parseMetadataValue(value) {
130
+ const unquoted = value.replace(/^["']|["']$/g, "");
131
+ if (/^-?\d+(?:\.\d+)?$/.test(unquoted))
132
+ return Number(unquoted);
133
+ return unquoted;
134
+ }
135
+ function normalizeMetadata(metadata) {
136
+ const out = {};
137
+ for (const [key, value] of Object.entries(metadata)) {
138
+ if (value === undefined || value === null || value === "")
139
+ continue;
140
+ out[camelMetadataKey(key)] = value;
141
+ }
142
+ return out;
143
+ }
144
+ function camelMetadataKey(key) {
145
+ const lower = key.toLowerCase();
146
+ return lower.replace(/[-_]([a-z])/g, (_match, letter) => letter.toUpperCase());
147
+ }
148
+ function splitDiaryTextBlocks(text, maxChars) {
149
+ const chunks = [];
150
+ for (const block of collectDiaryBlocks(text)) {
151
+ chunks.push(...splitOversizedParagraph(block, maxChars));
152
+ }
153
+ return chunks;
154
+ }
155
+ function collectDiaryBlocks(text) {
156
+ const blocks = [];
157
+ let current = [];
158
+ let currentListIndent = null;
159
+ const flush = () => {
160
+ const block = current.join(" ").replace(/\s+/g, " ").trim();
161
+ if (block)
162
+ blocks.push(block);
163
+ current = [];
164
+ currentListIndent = null;
165
+ };
166
+ for (const rawLine of text.replace(/\r\n/g, "\n").split("\n")) {
167
+ const line = rawLine.replace(/\s+$/g, "");
168
+ if (!line.trim()) {
169
+ flush();
170
+ continue;
171
+ }
172
+ const listItem = /^(\s*)(?:[-*+]|\d+[.)])\s+(.+)$/.exec(line);
173
+ if (listItem) {
174
+ flush();
175
+ current = [(listItem[2] ?? "").trim()];
176
+ currentListIndent = (listItem[1] ?? "").length;
177
+ continue;
178
+ }
179
+ if (currentListIndent !== null && /^\s+/.test(line)) {
180
+ current.push(line.trim());
181
+ continue;
182
+ }
183
+ flush();
184
+ current = [line.trim()];
185
+ }
186
+ flush();
187
+ return blocks;
188
+ }
189
+ function splitOversizedParagraph(text, maxChars) {
190
+ if (text.length <= maxChars)
191
+ return [text];
192
+ const chunks = [];
193
+ for (let index = 0; index < text.length;) {
194
+ let end = Math.min(index + maxChars, text.length);
195
+ if (end < text.length && isHighSurrogate(text.charCodeAt(end - 1)))
196
+ end -= 1;
197
+ if (end <= index)
198
+ end = Math.min(index + maxChars, text.length);
199
+ const chunk = text.slice(index, end).trim();
200
+ if (chunk)
201
+ chunks.push(chunk);
202
+ index = end;
203
+ }
204
+ return chunks;
205
+ }
206
+ function isHighSurrogate(value) {
207
+ return value >= 0xd800 && value <= 0xdbff;
208
+ }
209
+ function buildSnippet(text, metadata) {
210
+ const prefix = [metadata.date, metadata.heading]
211
+ .filter((value) => typeof value === "string" && value.trim())
212
+ .join(" ");
213
+ const body = text.replace(/\s+/g, " ").trim().slice(0, 220);
214
+ return prefix ? `${prefix}: ${body}` : body;
215
+ }
216
+ function dateFromSourceId(sourceId) {
217
+ const match = DIARY_DATE_RE.exec(basename(sourceId));
218
+ return match?.[1];
219
+ }
220
+ function stripInlineMarkdown(value) {
221
+ return value
222
+ .replace(/^\s*#+\s*/, "")
223
+ .replace(/[*_`~[\]()]/g, "")
224
+ .trim();
225
+ }
226
+ function isMarkdownHeading(line) {
227
+ return /^#{1,6}\s+/.test(line);
228
+ }
229
+ function positiveIntegerOrDefault(value, fallback) {
230
+ return value !== undefined && Number.isInteger(value) && value > 0 ? value : fallback;
231
+ }
@@ -0,0 +1,3 @@
1
+ export { retrieveAmbientDiary, } from "./ambient.js";
2
+ export { chunkDiaryMarkdown, DIARY_CHUNK_CORPUS, diaryChunksToIndexInputs, indexDiaryMarkdown, } from "./chunks.js";
3
+ export { DIARY_INDEX_FILE_RE, indexAllDiaryFiles, indexDiaryFile, isDatedDiaryMarkdownFile, listDiaryMarkdownFiles, removeDiaryFileIndex, } from "./indexer.js";
@@ -0,0 +1,93 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { basename, isAbsolute, join, resolve } from "node:path";
3
+ import { DIARY_CHUNK_CORPUS, indexDiaryMarkdown } from "./chunks.js";
4
+ export const DIARY_INDEX_FILE_RE = /^\d{4}-\d{2}-\d{2}\.md$/;
5
+ export function isDatedDiaryMarkdownFile(path) {
6
+ return DIARY_INDEX_FILE_RE.test(basename(path));
7
+ }
8
+ export async function listDiaryMarkdownFiles(config) {
9
+ let entries;
10
+ try {
11
+ entries = await readdir(config.memory.diariesDir, { withFileTypes: true });
12
+ }
13
+ catch (error) {
14
+ if (isEnoent(error)) {
15
+ console.info(`diary indexer found no diary directory at ${config.memory.diariesDir}; indexed 0 files`);
16
+ return [];
17
+ }
18
+ throw error;
19
+ }
20
+ return entries
21
+ .filter((entry) => entry.isFile() && DIARY_INDEX_FILE_RE.test(entry.name))
22
+ .map((entry) => join(config.memory.diariesDir, entry.name))
23
+ .sort();
24
+ }
25
+ function isEnoent(error) {
26
+ return !!error && typeof error === "object" && "code" in error && error.code === "ENOENT";
27
+ }
28
+ export async function indexDiaryFile(options) {
29
+ const path = resolveDiaryPath(options.config, options.path);
30
+ const sourceId = basename(path);
31
+ const skipInvalid = options.skipInvalid ?? true;
32
+ if (!isDatedDiaryMarkdownFile(path)) {
33
+ if (skipInvalid)
34
+ return { path, sourceId, skipped: true, reason: "not-dated-markdown" };
35
+ throw new Error(`Diary file must be named YYYY-MM-DD.md: ${path}`);
36
+ }
37
+ const fileStat = await stat(path);
38
+ if (!fileStat.isFile()) {
39
+ if (skipInvalid)
40
+ return { path, sourceId, skipped: true, reason: "not-file" };
41
+ throw new Error(`Diary path is not a file: ${path}`);
42
+ }
43
+ if (options.store && isDiarySourceUnchanged(options.store, sourceId, path, fileStat)) {
44
+ return {
45
+ path,
46
+ sourceId,
47
+ result: { ids: [], embedded: 0, reused: 0, skipped: 0 },
48
+ skippedUnchanged: true,
49
+ };
50
+ }
51
+ const markdown = await readFile(path, "utf8");
52
+ const result = await indexDiaryMarkdown({
53
+ indexer: options.indexer,
54
+ path,
55
+ markdown,
56
+ signal: options.signal,
57
+ });
58
+ options.store?.upsertSourceState({
59
+ corpus: DIARY_CHUNK_CORPUS,
60
+ sourceId,
61
+ sourceRef: path,
62
+ mtimeMs: fileStat.mtimeMs,
63
+ sizeBytes: fileStat.size,
64
+ });
65
+ return { path, sourceId, result };
66
+ }
67
+ export async function removeDiaryFileIndex(options) {
68
+ const path = resolveDiaryPath(options.config, options.path);
69
+ const sourceId = basename(path);
70
+ const result = await options.indexer.replaceSource(DIARY_CHUNK_CORPUS, sourceId, [], options.signal);
71
+ return { path, sourceId, result };
72
+ }
73
+ export async function indexAllDiaryFiles(options) {
74
+ const paths = await listDiaryMarkdownFiles(options.config);
75
+ const files = [];
76
+ for (const path of paths) {
77
+ const result = await indexDiaryFile({ ...options, path });
78
+ if (!("skipped" in result))
79
+ files.push(result);
80
+ }
81
+ return { files };
82
+ }
83
+ function resolveDiaryPath(config, path) {
84
+ return isAbsolute(path) ? resolve(path) : resolve(config.memory.diariesDir, path);
85
+ }
86
+ function isDiarySourceUnchanged(store, sourceId, sourceRef, fileStat) {
87
+ const state = store.getSourceState(DIARY_CHUNK_CORPUS, sourceId);
88
+ if (!state?.hasMappings)
89
+ return false;
90
+ if (state.sourceRef !== sourceRef)
91
+ return false;
92
+ return state.mtimeMs === Math.floor(fileStat.mtimeMs) && state.sizeBytes === fileStat.size;
93
+ }