opencode-harnmem 0.0.0-stage → 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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -2
  3. package/index.js +512 -0
  4. package/lib/engine.mjs +72 -0
  5. package/lib/format.mjs +119 -0
  6. package/lib/settings.mjs +131 -0
  7. package/package.json +23 -3
  8. package/vendor/core/assets.d.ts +52 -0
  9. package/vendor/core/assets.d.ts.map +1 -0
  10. package/vendor/core/assets.js +227 -0
  11. package/vendor/core/assets.js.map +1 -0
  12. package/vendor/core/config.d.ts +36 -0
  13. package/vendor/core/config.d.ts.map +1 -0
  14. package/vendor/core/config.js +134 -0
  15. package/vendor/core/config.js.map +1 -0
  16. package/vendor/core/consolidate.d.ts +14 -0
  17. package/vendor/core/consolidate.d.ts.map +1 -0
  18. package/vendor/core/consolidate.js +237 -0
  19. package/vendor/core/consolidate.js.map +1 -0
  20. package/vendor/core/data/synonyms.en.txt +61 -0
  21. package/vendor/core/data/synonyms.zh.txt +157 -0
  22. package/vendor/core/extract.d.ts +49 -0
  23. package/vendor/core/extract.d.ts.map +1 -0
  24. package/vendor/core/extract.js +179 -0
  25. package/vendor/core/extract.js.map +1 -0
  26. package/vendor/core/frontmatter.d.ts +31 -0
  27. package/vendor/core/frontmatter.d.ts.map +1 -0
  28. package/vendor/core/frontmatter.js +372 -0
  29. package/vendor/core/frontmatter.js.map +1 -0
  30. package/vendor/core/harnmem.d.ts +87 -0
  31. package/vendor/core/harnmem.d.ts.map +1 -0
  32. package/vendor/core/harnmem.js +918 -0
  33. package/vendor/core/harnmem.js.map +1 -0
  34. package/vendor/core/hot-context.d.ts +28 -0
  35. package/vendor/core/hot-context.d.ts.map +1 -0
  36. package/vendor/core/hot-context.js +179 -0
  37. package/vendor/core/hot-context.js.map +1 -0
  38. package/vendor/core/index.d.ts +16 -0
  39. package/vendor/core/index.d.ts.map +1 -0
  40. package/vendor/core/index.js +10 -0
  41. package/vendor/core/index.js.map +1 -0
  42. package/vendor/core/indexer.d.ts +107 -0
  43. package/vendor/core/indexer.d.ts.map +1 -0
  44. package/vendor/core/indexer.js +282 -0
  45. package/vendor/core/indexer.js.map +1 -0
  46. package/vendor/core/inspect.d.ts +278 -0
  47. package/vendor/core/inspect.d.ts.map +1 -0
  48. package/vendor/core/inspect.js +643 -0
  49. package/vendor/core/inspect.js.map +1 -0
  50. package/vendor/core/lifecycle.d.ts +50 -0
  51. package/vendor/core/lifecycle.d.ts.map +1 -0
  52. package/vendor/core/lifecycle.js +249 -0
  53. package/vendor/core/lifecycle.js.map +1 -0
  54. package/vendor/core/project.d.ts +41 -0
  55. package/vendor/core/project.d.ts.map +1 -0
  56. package/vendor/core/project.js +197 -0
  57. package/vendor/core/project.js.map +1 -0
  58. package/vendor/core/prompts/README.md +96 -0
  59. package/vendor/core/prompts/consolidate-digest.md +31 -0
  60. package/vendor/core/prompts/extract.md +80 -0
  61. package/vendor/core/prompts/migrate.md +96 -0
  62. package/vendor/core/prompts/synonyms.md +43 -0
  63. package/vendor/core/prompts/write-policy.md +12 -0
  64. package/vendor/core/retrieval.d.ts +95 -0
  65. package/vendor/core/retrieval.d.ts.map +1 -0
  66. package/vendor/core/retrieval.js +569 -0
  67. package/vendor/core/retrieval.js.map +1 -0
  68. package/vendor/core/store.d.ts +145 -0
  69. package/vendor/core/store.d.ts.map +1 -0
  70. package/vendor/core/store.js +511 -0
  71. package/vendor/core/store.js.map +1 -0
  72. package/vendor/core/suggest.d.ts +128 -0
  73. package/vendor/core/suggest.d.ts.map +1 -0
  74. package/vendor/core/suggest.js +384 -0
  75. package/vendor/core/suggest.js.map +1 -0
  76. package/vendor/core/text.d.ts +80 -0
  77. package/vendor/core/text.d.ts.map +1 -0
  78. package/vendor/core/text.js +253 -0
  79. package/vendor/core/text.js.map +1 -0
  80. package/vendor/core/types.d.ts +453 -0
  81. package/vendor/core/types.d.ts.map +1 -0
  82. package/vendor/core/types.js +14 -0
  83. package/vendor/core/types.js.map +1 -0
  84. package/vendor/core/util.d.ts +38 -0
  85. package/vendor/core/util.d.ts.map +1 -0
  86. package/vendor/core/util.js +248 -0
  87. package/vendor/core/util.js.map +1 -0
@@ -0,0 +1,145 @@
1
+ import { dirname } from "node:path";
2
+ import type { MemoryRecord, MemoryScope, TranscriptMessage } from "./types.js";
3
+ export interface ShardRef {
4
+ scope: MemoryScope;
5
+ /** projectId for project scope; undefined for user scope. */
6
+ project?: string;
7
+ /** Directory name used in paths, e.g. "user" or the projectId. */
8
+ name: string;
9
+ }
10
+ export interface ShardRecords {
11
+ shard: ShardRef;
12
+ records: MemoryRecord[];
13
+ errors: {
14
+ file: string;
15
+ message: string;
16
+ }[];
17
+ }
18
+ export interface AuditEntry {
19
+ seq?: number;
20
+ ts: number;
21
+ kind: string;
22
+ harness?: string;
23
+ session?: string;
24
+ [key: string]: unknown;
25
+ }
26
+ export interface DigestEntry {
27
+ name: string;
28
+ title: string;
29
+ body: string;
30
+ updated?: number;
31
+ }
32
+ export interface UsageEntry {
33
+ lastUsed: number;
34
+ usedCount: number;
35
+ }
36
+ export declare class Store {
37
+ readonly home: string;
38
+ private seqCache;
39
+ private shardSeqCache;
40
+ constructor(home: string);
41
+ path(...parts: string[]): string;
42
+ get memoriesDir(): string;
43
+ get stateDir(): string;
44
+ get indexPath(): string;
45
+ get trashDir(): string;
46
+ init(): Promise<void>;
47
+ shardRef(scope: MemoryScope, projectId?: string): ShardRef;
48
+ shardDir(shard: ShardRef): string;
49
+ indexFile(shard: ShardRef): string;
50
+ digestDir(shard: ShardRef): string;
51
+ recordPath(shard: ShardRef, id: string): string;
52
+ listShards(): Promise<ShardRef[]>;
53
+ /** Read every `.md` file in a shard (excluding MEMORY.md). */
54
+ readShard(shard: ShardRef): Promise<ShardRecords>;
55
+ readRecord(shard: ShardRef, id: string): Promise<MemoryRecord | null>;
56
+ /** Find a record by id across all shards (cheap scan; ids are unique). */
57
+ findRecord(id: string): Promise<{
58
+ record: MemoryRecord;
59
+ shard: ShardRef;
60
+ } | null>;
61
+ writeRecord(shard: ShardRef, record: MemoryRecord): Promise<void>;
62
+ /** Bulk write (import/migration): files first, index once, seq once. */
63
+ writeRecordsBulk(shard: ShardRef, records: MemoryRecord[]): Promise<void>;
64
+ /** Update MEMORY.md one line at a time (index is derived, safe to rebuild). */
65
+ private upsertIndexLine;
66
+ /** Move a record file to trash (soft delete for `forget`). */
67
+ moveToTrash(shard: ShardRef, record: MemoryRecord, now: number): Promise<string>;
68
+ getSeq(): Promise<number>;
69
+ /**
70
+ * Per-shard sequence: index validity is checked against it, so a write to one
71
+ * project never invalidates unrelated shard indexes (global seq invalidates all).
72
+ */
73
+ getShardSeq(name: string): Promise<number>;
74
+ /** Read the seq from disk, bypassing the in-process cache (cross-process freshness). */
75
+ peekSeq(): Promise<number>;
76
+ bumpSeq(shardName?: string): Promise<number>;
77
+ audit(entry: Omit<AuditEntry, "seq"> & {
78
+ seq?: number;
79
+ }): Promise<void>;
80
+ getCursor(key: string): Promise<string | null>;
81
+ setCursor(key: string, value: string): Promise<void>;
82
+ private usageCache;
83
+ /**
84
+ * Usage counters live in `state/usage.json`, not in the memory files: a search
85
+ * touches up to `budget` records, and rewriting N markdown files per query
86
+ * (plus invalidating every shard index) does not scale. Memory files stay the
87
+ * SSOT for content; `doctor --merge-usage` folds these counters back into the
88
+ * frontmatter when it matters.
89
+ */
90
+ loadUsage(): Promise<Map<string, UsageEntry>>;
91
+ /** Persist usage counters (lastUsed/usedCount) touched by searches. */
92
+ saveUsage(updates: Map<string, UsageEntry>): Promise<void>;
93
+ /**
94
+ * Session journal entry (only what extraction needs).
95
+ *
96
+ * Idempotent per message: re-persisting a snapshot that already covers
97
+ * messages from an earlier process (the adapter rehydrates its digest from
98
+ * this journal) appends only the lines that are not already present.
99
+ */
100
+ appendSession(input: {
101
+ harness: string;
102
+ sessionId: string;
103
+ cwd: string;
104
+ date?: string;
105
+ messages: {
106
+ role: string;
107
+ text: string;
108
+ ts?: number;
109
+ }[];
110
+ }): Promise<void>;
111
+ /**
112
+ * Read one session's persisted journal across day directories, oldest first,
113
+ * deduplicated and bounded to the last 200 messages.
114
+ *
115
+ * DSH resumes a session in a new process without replaying earlier events, so
116
+ * an adapter that only kept a process-local digest would always hit the
117
+ * `too_few_messages` gate at the next quit. Adapters use this to rehydrate.
118
+ */
119
+ readSessionTranscript(harness: string, sessionId: string): Promise<TranscriptMessage[]>;
120
+ listDigests(shard: ShardRef): Promise<DigestEntry[]>;
121
+ /** Load/save per-shard search index JSON. */
122
+ loadIndex(name: string): Promise<unknown | null>;
123
+ saveIndex(name: string, value: unknown): Promise<void>;
124
+ readManifest(): Promise<{
125
+ version: number;
126
+ seq: number;
127
+ shards: unknown[];
128
+ } | null>;
129
+ writeManifest(seq: number, shards: unknown[]): Promise<void>;
130
+ /** Rebuild human-readable MEMORY.md for a shard from its files. */
131
+ rebuildIndexFile(shard: ShardRef): Promise<number>;
132
+ stats(): Promise<{
133
+ memories: number;
134
+ user: number;
135
+ projects: Record<string, number>;
136
+ archived: number;
137
+ superseded: number;
138
+ trash: number;
139
+ digests: number;
140
+ }>;
141
+ }
142
+ export declare function shardKey(shard: ShardRef): string;
143
+ export declare function shardFromName(name: string): ShardRef;
144
+ export { dirname };
145
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,511 @@
1
+ /**
2
+ * Filesystem store: the single source of truth. One directory per scope shard,
3
+ * one markdown file per memory, JSON for state/index. No database.
4
+ *
5
+ * Layout (docs/design/02-data-model.md):
6
+ * memories/user/<id>.md
7
+ * memories/projects/<projectId>/<id>.md
8
+ * state/seq, state/audit.jsonl, state/cursors.json
9
+ * index/manifest.json, index/<shard>.json
10
+ * digests/<shard>/*.md
11
+ * sessions/<date>/<harness>_<id>.jsonl
12
+ * trash/<ts>-<id>.md
13
+ */
14
+ import { promises as fs } from "node:fs";
15
+ import { basename, dirname, join } from "node:path";
16
+ import { parseFrontmatter, recordFromFrontmatter, recordFileName, serializeRecord } from "./frontmatter.js";
17
+ import { atomicWriteFile, ensureDir, normalizeNewlines, pathExists, readJson, writeJson } from "./util.js";
18
+ export class Store {
19
+ home;
20
+ seqCache = null;
21
+ shardSeqCache = null;
22
+ constructor(home) {
23
+ this.home = home;
24
+ }
25
+ path(...parts) {
26
+ return join(this.home, ...parts);
27
+ }
28
+ get memoriesDir() {
29
+ return this.path("memories");
30
+ }
31
+ get stateDir() {
32
+ return this.path("state");
33
+ }
34
+ get indexPath() {
35
+ return this.path("index");
36
+ }
37
+ get trashDir() {
38
+ return this.path("trash");
39
+ }
40
+ async init() {
41
+ await Promise.all([
42
+ ensureDir(this.path("memories", "user")),
43
+ ensureDir(this.path("memories", "projects")),
44
+ ensureDir(this.stateDir),
45
+ ensureDir(this.indexPath),
46
+ ensureDir(this.trashDir),
47
+ ensureDir(this.path("digests")),
48
+ ensureDir(this.path("sessions")),
49
+ ensureDir(this.path("prompts")),
50
+ ensureDir(this.path("data")),
51
+ ]);
52
+ if (!(await pathExists(this.path("VERSION")))) {
53
+ await atomicWriteFile(this.path("VERSION"), "1\n");
54
+ }
55
+ }
56
+ shardRef(scope, projectId) {
57
+ if (scope === "user")
58
+ return { scope, name: "user" };
59
+ if (!projectId)
60
+ throw new Error("project scope requires a projectId");
61
+ return { scope, project: projectId, name: projectId };
62
+ }
63
+ shardDir(shard) {
64
+ return shard.scope === "user"
65
+ ? this.path("memories", "user")
66
+ : this.path("memories", "projects", shard.project);
67
+ }
68
+ indexFile(shard) {
69
+ return join(this.shardDir(shard), "MEMORY.md");
70
+ }
71
+ digestDir(shard) {
72
+ return this.path("digests", shard.name);
73
+ }
74
+ recordPath(shard, id) {
75
+ return join(this.shardDir(shard), recordFileName(id));
76
+ }
77
+ async listShards() {
78
+ const out = [];
79
+ if (await pathExists(this.path("memories", "user")))
80
+ out.push({ scope: "user", name: "user" });
81
+ const projectsDir = this.path("memories", "projects");
82
+ if (await pathExists(projectsDir)) {
83
+ for (const entry of await fs.readdir(projectsDir, { withFileTypes: true })) {
84
+ if (!entry.isDirectory())
85
+ continue;
86
+ out.push({ scope: "project", project: entry.name, name: entry.name });
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+ /** Read every `.md` file in a shard (excluding MEMORY.md). */
92
+ async readShard(shard) {
93
+ const dir = this.shardDir(shard);
94
+ const records = [];
95
+ const errors = [];
96
+ let files = [];
97
+ try {
98
+ files = await fs.readdir(dir);
99
+ }
100
+ catch {
101
+ return { shard, records, errors };
102
+ }
103
+ for (const file of files) {
104
+ if (!file.endsWith(".md") || file === "MEMORY.md")
105
+ continue;
106
+ const full = join(dir, file);
107
+ try {
108
+ const raw = await fs.readFile(full, "utf8");
109
+ const parsed = parseFrontmatter(raw);
110
+ const fallbackId = basename(file, ".md");
111
+ const record = recordFromFrontmatter(parsed.data, parsed.body, fallbackId, 0);
112
+ record.id = record.id || fallbackId;
113
+ if (parsed.errors.length > 0)
114
+ errors.push({ file: full, message: parsed.errors.join("; ") });
115
+ records.push(record);
116
+ }
117
+ catch (error) {
118
+ errors.push({ file: full, message: error.message });
119
+ }
120
+ }
121
+ records.sort((a, b) => a.created - b.created || a.id.localeCompare(b.id));
122
+ return { shard, records, errors };
123
+ }
124
+ async readRecord(shard, id) {
125
+ const file = this.recordPath(shard, id);
126
+ try {
127
+ const raw = await fs.readFile(file, "utf8");
128
+ const parsed = parseFrontmatter(raw);
129
+ const record = recordFromFrontmatter(parsed.data, parsed.body, id, 0);
130
+ record.id = record.id || id;
131
+ return record;
132
+ }
133
+ catch {
134
+ return null;
135
+ }
136
+ }
137
+ /** Find a record by id across all shards (cheap scan; ids are unique). */
138
+ async findRecord(id) {
139
+ for (const shard of await this.listShards()) {
140
+ const record = await this.readRecord(shard, id);
141
+ if (record)
142
+ return { record, shard };
143
+ }
144
+ return null;
145
+ }
146
+ async writeRecord(shard, record) {
147
+ await ensureDir(this.shardDir(shard));
148
+ await atomicWriteFile(this.recordPath(shard, record.id), serializeRecord(record));
149
+ await this.upsertIndexLine(shard, record);
150
+ await this.bumpSeq(shard.name);
151
+ }
152
+ /** Bulk write (import/migration): files first, index once, seq once. */
153
+ async writeRecordsBulk(shard, records) {
154
+ if (records.length === 0)
155
+ return;
156
+ await ensureDir(this.shardDir(shard));
157
+ const CONCURRENCY = 32;
158
+ for (let i = 0; i < records.length; i += CONCURRENCY) {
159
+ const batch = records.slice(i, i + CONCURRENCY);
160
+ await Promise.all(batch.map((record) => atomicWriteFile(this.recordPath(shard, record.id), serializeRecord(record))));
161
+ }
162
+ await this.rebuildIndexFile(shard);
163
+ await this.bumpSeq(shard.name);
164
+ }
165
+ /** Update MEMORY.md one line at a time (index is derived, safe to rebuild). */
166
+ async upsertIndexLine(shard, record) {
167
+ const file = this.indexFile(shard);
168
+ let lines = [];
169
+ try {
170
+ const raw = await fs.readFile(file, "utf8");
171
+ lines = normalizeNewlines(raw).split("\n");
172
+ }
173
+ catch {
174
+ lines = ["# Memory Index", "", "<!-- derived file, regenerated by `harnmem doctor --rebuild-index`; do not edit by hand -->", ""];
175
+ }
176
+ const { indexLine } = await import("./frontmatter.js");
177
+ const line = indexLine(record);
178
+ const needle = `(${recordFileName(record.id)})`;
179
+ const idx = lines.findIndex((l) => l.includes(needle));
180
+ if (idx >= 0) {
181
+ if (record.status === "dropped" || record.status === "archived")
182
+ lines.splice(idx, 1);
183
+ else
184
+ lines[idx] = line;
185
+ }
186
+ else if (record.status === "active") {
187
+ const headerEnd = Math.max(0, lines.findIndex((l) => l.startsWith("<!--")) + 1);
188
+ lines.splice(headerEnd, 0, line);
189
+ }
190
+ else {
191
+ return;
192
+ }
193
+ await atomicWriteFile(file, `${lines.join("\n").replace(/\n+$/, "")}\n`);
194
+ }
195
+ /** Move a record file to trash (soft delete for `forget`). */
196
+ async moveToTrash(shard, record, now) {
197
+ const src = this.recordPath(shard, record.id);
198
+ const dest = join(this.trashDir, `${dayStamp(now)}-${record.id}.md`);
199
+ await ensureDir(this.trashDir);
200
+ try {
201
+ await fs.rename(src, dest);
202
+ }
203
+ catch {
204
+ await fs.copyFile(src, dest);
205
+ await fs.unlink(src);
206
+ }
207
+ await this.upsertIndexLine(shard, { ...record, status: "dropped" });
208
+ await this.bumpSeq(shard.name);
209
+ return dest;
210
+ }
211
+ async getSeq() {
212
+ if (this.seqCache !== null)
213
+ return this.seqCache;
214
+ return this.peekSeq();
215
+ }
216
+ /**
217
+ * Per-shard sequence: index validity is checked against it, so a write to one
218
+ * project never invalidates unrelated shard indexes (global seq invalidates all).
219
+ */
220
+ async getShardSeq(name) {
221
+ if (this.seqCache === null || this.shardSeqCache === null)
222
+ await this.peekSeq();
223
+ return this.shardSeqCache?.get(name) ?? 0;
224
+ }
225
+ /** Read the seq from disk, bypassing the in-process cache (cross-process freshness). */
226
+ async peekSeq() {
227
+ const parsed = await readJson(this.path("state", "seq.json"), {
228
+ seq: 0,
229
+ });
230
+ const seq = typeof parsed.seq === "number" ? parsed.seq : 0;
231
+ this.seqCache = seq;
232
+ this.shardSeqCache = new Map(Object.entries(parsed.shards ?? {}));
233
+ return seq;
234
+ }
235
+ async bumpSeq(shardName) {
236
+ if (this.seqCache === null)
237
+ await this.peekSeq();
238
+ const next = (this.seqCache ?? 0) + 1;
239
+ this.seqCache = next;
240
+ if (shardName) {
241
+ const current = this.shardSeqCache?.get(shardName) ?? 0;
242
+ this.shardSeqCache?.set(shardName, current + 1);
243
+ }
244
+ const shards = this.shardSeqCache ? Object.fromEntries(this.shardSeqCache) : {};
245
+ await writeJson(this.path("state", "seq.json"), { seq: next, shards });
246
+ return next;
247
+ }
248
+ async audit(entry) {
249
+ const seq = entry.seq ?? (await this.getSeq());
250
+ const line = JSON.stringify({ seq, ...entry });
251
+ await ensureDir(this.stateDir);
252
+ const file = this.path("state", "audit.jsonl");
253
+ if (!(await pathExists(file))) {
254
+ await atomicWriteFile(file, `${line}\n`);
255
+ return;
256
+ }
257
+ const current = await fs.readFile(file, "utf8");
258
+ await atomicWriteFile(file, `${current.replace(/\n+$/, "")}\n${line}\n`);
259
+ }
260
+ async getCursor(key) {
261
+ const cursors = await readJson(this.path("state", "cursors.json"), {});
262
+ return cursors[key] ?? null;
263
+ }
264
+ async setCursor(key, value) {
265
+ const cursors = await readJson(this.path("state", "cursors.json"), {});
266
+ cursors[key] = value;
267
+ await writeJson(this.path("state", "cursors.json"), cursors);
268
+ }
269
+ usageCache = null;
270
+ /**
271
+ * Usage counters live in `state/usage.json`, not in the memory files: a search
272
+ * touches up to `budget` records, and rewriting N markdown files per query
273
+ * (plus invalidating every shard index) does not scale. Memory files stay the
274
+ * SSOT for content; `doctor --merge-usage` folds these counters back into the
275
+ * frontmatter when it matters.
276
+ */
277
+ async loadUsage() {
278
+ if (this.usageCache)
279
+ return this.usageCache;
280
+ const raw = await readJson(this.path("state", "usage.json"), {});
281
+ this.usageCache = new Map(Object.entries(raw));
282
+ return this.usageCache;
283
+ }
284
+ /** Persist usage counters (lastUsed/usedCount) touched by searches. */
285
+ async saveUsage(updates) {
286
+ if (updates.size === 0)
287
+ return;
288
+ const usage = await this.loadUsage();
289
+ for (const [id, update] of updates)
290
+ usage.set(id, update);
291
+ await writeJson(this.path("state", "usage.json"), Object.fromEntries(usage));
292
+ }
293
+ /**
294
+ * Session journal entry (only what extraction needs).
295
+ *
296
+ * Idempotent per message: re-persisting a snapshot that already covers
297
+ * messages from an earlier process (the adapter rehydrates its digest from
298
+ * this journal) appends only the lines that are not already present.
299
+ */
300
+ async appendSession(input) {
301
+ const date = input.date ?? dayStamp(Date.now());
302
+ const dir = this.path("sessions", date);
303
+ await ensureDir(dir);
304
+ const file = join(dir, `${input.harness}_${sanitizeFilePart(input.sessionId)}.jsonl`);
305
+ const existing = (await pathExists(file)) ? await fs.readFile(file, "utf8") : "";
306
+ const seen = new Set();
307
+ for (const rawLine of existing.split(/\r?\n/)) {
308
+ // Tolerate a UTF-8 BOM from hand-edited / PowerShell-written journals.
309
+ const line = rawLine.replace(/^\uFEFF/, "");
310
+ if (!line.trim())
311
+ continue;
312
+ try {
313
+ const parsed = JSON.parse(line);
314
+ if (parsed.role && typeof parsed.text === "string") {
315
+ seen.add(journalKey(parsed.ts, parsed.role, parsed.text));
316
+ }
317
+ }
318
+ catch {
319
+ // Ignore malformed journal lines; new messages are still appended.
320
+ }
321
+ }
322
+ const fresh = [];
323
+ for (const m of input.messages) {
324
+ const ts = m.ts ?? Date.now();
325
+ const key = journalKey(ts, m.role, m.text);
326
+ if (seen.has(key))
327
+ continue;
328
+ seen.add(key);
329
+ fresh.push(JSON.stringify({ ts, role: m.role, text: m.text.slice(0, 4096), cwd: input.cwd }));
330
+ }
331
+ if (fresh.length === 0)
332
+ return;
333
+ await atomicWriteFile(file, `${existing}${fresh.join("\n")}\n`);
334
+ }
335
+ /**
336
+ * Read one session's persisted journal across day directories, oldest first,
337
+ * deduplicated and bounded to the last 200 messages.
338
+ *
339
+ * DSH resumes a session in a new process without replaying earlier events, so
340
+ * an adapter that only kept a process-local digest would always hit the
341
+ * `too_few_messages` gate at the next quit. Adapters use this to rehydrate.
342
+ */
343
+ async readSessionTranscript(harness, sessionId) {
344
+ const root = this.path("sessions");
345
+ let dates = [];
346
+ try {
347
+ dates = (await fs.readdir(root)).filter((name) => /^\d{8}$/.test(name)).sort();
348
+ }
349
+ catch {
350
+ return [];
351
+ }
352
+ const file = `${harness}_${sanitizeFilePart(sessionId)}.jsonl`;
353
+ const seen = new Set();
354
+ const out = [];
355
+ for (const date of dates) {
356
+ const path = join(root, date, file);
357
+ if (!(await pathExists(path)))
358
+ continue;
359
+ let raw = "";
360
+ try {
361
+ raw = await fs.readFile(path, "utf8");
362
+ }
363
+ catch {
364
+ continue;
365
+ }
366
+ for (const rawLine of raw.split(/\r?\n/)) {
367
+ // Tolerate a UTF-8 BOM from hand-edited / PowerShell-written journals.
368
+ const line = rawLine.replace(/^\uFEFF/, "");
369
+ if (!line.trim())
370
+ continue;
371
+ try {
372
+ const parsed = JSON.parse(line);
373
+ if (typeof parsed.text !== "string" || parsed.text.trim() === "")
374
+ continue;
375
+ const role = parsed.role;
376
+ if (role !== "user" && role !== "assistant" && role !== "tool" && role !== "system")
377
+ continue;
378
+ const key = journalKey(parsed.ts, role, parsed.text);
379
+ if (seen.has(key))
380
+ continue;
381
+ seen.add(key);
382
+ const message = { role, text: parsed.text };
383
+ if (typeof parsed.ts === "number")
384
+ message.ts = parsed.ts;
385
+ out.push(message);
386
+ }
387
+ catch {
388
+ // Ignore malformed journal lines.
389
+ }
390
+ }
391
+ }
392
+ out.sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
393
+ return out.slice(-200);
394
+ }
395
+ async listDigests(shard) {
396
+ const dir = this.digestDir(shard);
397
+ let files = [];
398
+ try {
399
+ files = (await fs.readdir(dir)).filter((f) => f.endsWith(".md"));
400
+ }
401
+ catch {
402
+ return [];
403
+ }
404
+ const out = [];
405
+ for (const file of files) {
406
+ try {
407
+ const raw = await fs.readFile(join(dir, file), "utf8");
408
+ const parsed = parseFrontmatter(raw);
409
+ const firstHeading = parsed.body.split("\n").find((l) => l.trim().startsWith("#"));
410
+ out.push({
411
+ name: file.replace(/\.md$/, ""),
412
+ title: firstHeading ? firstHeading.replace(/^#+\s*/, "").trim() : file.replace(/\.md$/, ""),
413
+ body: parsed.body.trim(),
414
+ updated: typeof parsed.data["updated"] === "number" ? parsed.data["updated"] : undefined,
415
+ });
416
+ }
417
+ catch {
418
+ // skip unreadable digest
419
+ }
420
+ }
421
+ out.sort((a, b) => a.name.localeCompare(b.name));
422
+ return out;
423
+ }
424
+ /** Load/save per-shard search index JSON. */
425
+ async loadIndex(name) {
426
+ const file = this.path("index", `${sanitizeFilePart(name)}.json`);
427
+ if (!(await pathExists(file)))
428
+ return null;
429
+ return readJson(file, null);
430
+ }
431
+ async saveIndex(name, value) {
432
+ await writeJson(this.path("index", `${sanitizeFilePart(name)}.json`), value);
433
+ }
434
+ async readManifest() {
435
+ return readJson(this.path("index", "manifest.json"), null);
436
+ }
437
+ async writeManifest(seq, shards) {
438
+ await writeJson(this.path("index", "manifest.json"), { version: 1, seq, builtAt: Date.now(), shards });
439
+ }
440
+ /** Rebuild human-readable MEMORY.md for a shard from its files. */
441
+ async rebuildIndexFile(shard) {
442
+ const { records } = await this.readShard(shard);
443
+ const { indexLine } = await import("./frontmatter.js");
444
+ const active = records
445
+ .filter((r) => r.status === "active")
446
+ .sort((a, b) => (Number(b.pinned) - Number(a.pinned)) || b.updated - a.updated || a.id.localeCompare(b.id));
447
+ const lines = [
448
+ "# Memory Index",
449
+ "",
450
+ "<!-- derived file, regenerated by `harnmem doctor --rebuild-index`; do not edit by hand -->",
451
+ "",
452
+ ...active.map((r) => indexLine(r)),
453
+ ];
454
+ await atomicWriteFile(this.indexFile(shard), `${lines.join("\n")}\n`);
455
+ return active.length;
456
+ }
457
+ async stats() {
458
+ let user = 0;
459
+ let archived = 0;
460
+ let superseded = 0;
461
+ const projects = {};
462
+ for (const shard of await this.listShards()) {
463
+ const { records } = await this.readShard(shard);
464
+ const active = records.filter((r) => r.status === "active").length;
465
+ if (shard.scope === "user")
466
+ user = active;
467
+ else
468
+ projects[shard.project] = active;
469
+ archived += records.filter((r) => r.status === "archived").length;
470
+ superseded += records.filter((r) => r.status === "superseded").length;
471
+ }
472
+ let trash = 0;
473
+ try {
474
+ trash = (await fs.readdir(this.trashDir)).filter((f) => f.endsWith(".md")).length;
475
+ }
476
+ catch {
477
+ trash = 0;
478
+ }
479
+ let digests = 0;
480
+ for (const shard of await this.listShards())
481
+ digests += (await this.listDigests(shard)).length;
482
+ return {
483
+ memories: user + Object.values(projects).reduce((a, b) => a + b, 0),
484
+ user,
485
+ projects,
486
+ archived,
487
+ superseded,
488
+ trash,
489
+ digests,
490
+ };
491
+ }
492
+ }
493
+ function sanitizeFilePart(input) {
494
+ return input.replace(/[^A-Za-z0-9_.-]+/g, "_").slice(0, 80) || "x";
495
+ }
496
+ /** Identity of a journal message: same ts+role+text = same append. */
497
+ function journalKey(ts, role, text) {
498
+ return `${ts ?? 0}\u0000${role}\u0000${text.slice(0, 200)}`;
499
+ }
500
+ function dayStamp(ts) {
501
+ const d = new Date(ts);
502
+ return `${d.getFullYear()}${`${d.getMonth() + 1}`.padStart(2, "0")}${`${d.getDate()}`.padStart(2, "0")}`;
503
+ }
504
+ export function shardKey(shard) {
505
+ return shard.name;
506
+ }
507
+ export function shardFromName(name) {
508
+ return name === "user" ? { scope: "user", name: "user" } : { scope: "project", project: name, name };
509
+ }
510
+ export { dirname };
511
+ //# sourceMappingURL=store.js.map