dsh-memoir 0.4.3
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/LICENSE +201 -0
- package/README.md +242 -0
- package/cordis.patch.yml +21 -0
- package/lib/autodistill.d.ts +65 -0
- package/lib/autodistill.js +94 -0
- package/lib/client.js +973 -0
- package/lib/client.js.map +7 -0
- package/lib/index.d.ts +98 -0
- package/lib/index.js +204 -0
- package/lib/retrieval.d.ts +151 -0
- package/lib/retrieval.js +363 -0
- package/lib/routes.d.ts +77 -0
- package/lib/routes.js +253 -0
- package/lib/selector.d.ts +89 -0
- package/lib/selector.js +179 -0
- package/lib/snapshot.d.ts +88 -0
- package/lib/snapshot.js +111 -0
- package/lib/store.d.ts +248 -0
- package/lib/store.js +553 -0
- package/lib/tools.d.ts +35 -0
- package/lib/tools.js +292 -0
- package/package.json +78 -0
package/lib/snapshot.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session memory snapshot manager (roadmap §2.2) — freezes the project
|
|
3
|
+
* memory injected into a session's system prompt so that the prompt prefix
|
|
4
|
+
* stays stable for the whole session. The current session does NOT re-consume
|
|
5
|
+
* memory it just wrote: later assemblies reuse the first snapshot; a NEW
|
|
6
|
+
* session builds a fresh one and sees the new memory.
|
|
7
|
+
*
|
|
8
|
+
* This is what maximizes prompt-prefix cache hits — the goal is stable model
|
|
9
|
+
* input, not just fast reads (the store snapshot cache already covers those).
|
|
10
|
+
*
|
|
11
|
+
* Pure logic, unit-testable without any runtime dependency.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
/** Hash a text for prompt-stability comparison (truncated SHA-256). */
|
|
15
|
+
export function snapshotHash(text) {
|
|
16
|
+
return createHash('sha256').update(text).digest('hex').slice(0, 16);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Freezes one session's injected memory; bounded by a simple LRU (oldest
|
|
20
|
+
* snapshot evicted past the cap), so long-running processes never accumulate
|
|
21
|
+
* dead session entries.
|
|
22
|
+
*/
|
|
23
|
+
export class MemorySnapshotManager {
|
|
24
|
+
/** Live session snapshots in LRU order (most recent last). */
|
|
25
|
+
snapshots = new Map();
|
|
26
|
+
max;
|
|
27
|
+
/**
|
|
28
|
+
* @param options.max - LRU cap (default 128; config sessionSnapshotMax).
|
|
29
|
+
*/
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.max = options.max ?? 128;
|
|
32
|
+
}
|
|
33
|
+
/** Current snapshot count (diagnostics). */
|
|
34
|
+
get size() {
|
|
35
|
+
return this.snapshots.size;
|
|
36
|
+
}
|
|
37
|
+
/** The LRU cap this manager was created with. */
|
|
38
|
+
get cap() {
|
|
39
|
+
return this.max;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Return the session's frozen snapshot, or build one via builder.
|
|
43
|
+
* A later call for the same key ALWAYS returns the first snapshot — even
|
|
44
|
+
* if the store revision moved on (that is the point: stable prompt prefix).
|
|
45
|
+
*
|
|
46
|
+
* @param sessionKey - stable session identity (id + workspace).
|
|
47
|
+
* @param builder - builds { storeRevision, text } when no snapshot exists.
|
|
48
|
+
*/
|
|
49
|
+
getOrCreate(sessionKey, builder) {
|
|
50
|
+
const existing = this.snapshots.get(sessionKey);
|
|
51
|
+
if (existing !== undefined) {
|
|
52
|
+
// Refresh LRU recency.
|
|
53
|
+
this.snapshots.delete(sessionKey);
|
|
54
|
+
this.snapshots.set(sessionKey, existing);
|
|
55
|
+
return existing;
|
|
56
|
+
}
|
|
57
|
+
const built = builder();
|
|
58
|
+
const snapshot = {
|
|
59
|
+
sessionKey,
|
|
60
|
+
storeRevision: built.storeRevision,
|
|
61
|
+
text: built.text,
|
|
62
|
+
hash: snapshotHash(built.text),
|
|
63
|
+
createdAt: Date.now(),
|
|
64
|
+
};
|
|
65
|
+
this.snapshots.set(sessionKey, snapshot);
|
|
66
|
+
// Evict the oldest entry when past the cap.
|
|
67
|
+
while (this.snapshots.size > this.max) {
|
|
68
|
+
const oldest = this.snapshots.keys().next().value;
|
|
69
|
+
if (oldest === undefined)
|
|
70
|
+
break;
|
|
71
|
+
this.snapshots.delete(oldest);
|
|
72
|
+
}
|
|
73
|
+
return snapshot;
|
|
74
|
+
}
|
|
75
|
+
/** Peek at a session's snapshot (undefined when not frozen yet). */
|
|
76
|
+
peek(sessionKey) {
|
|
77
|
+
return this.snapshots.get(sessionKey);
|
|
78
|
+
}
|
|
79
|
+
/** The most recently created snapshot (diagnostics / inspector). */
|
|
80
|
+
latest() {
|
|
81
|
+
let latest;
|
|
82
|
+
for (const snapshot of this.snapshots.values())
|
|
83
|
+
latest = snapshot;
|
|
84
|
+
return latest;
|
|
85
|
+
}
|
|
86
|
+
/** Drop one session's snapshot (disposal hygiene). */
|
|
87
|
+
forget(sessionKey) {
|
|
88
|
+
this.snapshots.delete(sessionKey);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Derive a stable session key from the system-prompt assemble context:
|
|
93
|
+
* prefer the session id, then the agent id; always scoped by the workspace
|
|
94
|
+
* cwd. Returns undefined when no unique identity is known — no freezing then,
|
|
95
|
+
* every assembly builds fresh.
|
|
96
|
+
*
|
|
97
|
+
* v0.4.2: the cwd-only fallback was removed. A key of the form "cwd:<path>"
|
|
98
|
+
* is shared by every session of that workspace, so session A's frozen
|
|
99
|
+
* snapshot would be served to session B, hiding memory session A itself
|
|
100
|
+
* wrote. Without a unique session identity, cache miss beats cache
|
|
101
|
+
* corruption: freeze nothing, rebuild every assembly.
|
|
102
|
+
*/
|
|
103
|
+
export function sessionKeyOf(context) {
|
|
104
|
+
const agent = context.agent;
|
|
105
|
+
const cwd = agent?.session?.header?.cwd;
|
|
106
|
+
const cwdPart = typeof cwd === 'string' && cwd !== '' ? cwd : '';
|
|
107
|
+
const id = agent?.session?.id ?? agent?.id;
|
|
108
|
+
if (typeof id === 'string' && id !== '')
|
|
109
|
+
return id + '|' + cwdPart;
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
package/lib/store.d.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured memory store for dsh-memoir — the single source of truth is the
|
|
3
|
+
* global index JSON (~/.dsh/dsh-memoir.json); the per-project PROJECT_MEMORY.md
|
|
4
|
+
* is a regenerated human-readable rendering of the same entries (git-friendly,
|
|
5
|
+
* auto-injected into future sessions). Pure node:fs, no cordis dependency —
|
|
6
|
+
* unit-testable with an injected path.
|
|
7
|
+
*
|
|
8
|
+
* v0.3.1: revision-based in-memory snapshot cache — cold start reads the file
|
|
9
|
+
* once, warm reads return the snapshot without touching disk, writes bump the
|
|
10
|
+
* revision and refresh the snapshot; external file changes are picked up by a
|
|
11
|
+
* low-frequency mtime probe. Corrupt JSON is backed up (never silently
|
|
12
|
+
* overwritten), atomic writes use unique temp names, and project keys are
|
|
13
|
+
* normalized (drive-letter case + separators) so C:\A / c:\a\ / C:/A share
|
|
14
|
+
* one bucket.
|
|
15
|
+
*/
|
|
16
|
+
/** Global index format version. */
|
|
17
|
+
export declare const FORMAT_VERSION = 2;
|
|
18
|
+
/** Project memory file name (workspace root, git-committable). */
|
|
19
|
+
export declare const PROJECT_FILE = "PROJECT_MEMORY.md";
|
|
20
|
+
/** Section keys, human labels, and markdown headers (fixed order for rendering). */
|
|
21
|
+
export declare const SECTIONS: Record<SectionKey, {
|
|
22
|
+
label: string;
|
|
23
|
+
header: string;
|
|
24
|
+
}>;
|
|
25
|
+
/** Section keys in canonical render order. */
|
|
26
|
+
export declare const SECTION_KEYS: SectionKey[];
|
|
27
|
+
/**
|
|
28
|
+
* Legacy cap on how much project memory was auto-injected into the prompt,
|
|
29
|
+
* in JS string length (not bytes, not tokens). Kept for compatibility with
|
|
30
|
+
* existing imports; v0.4+ replaces this with the selector's token budget
|
|
31
|
+
* (targetTokens / hardMaxTokens).
|
|
32
|
+
*/
|
|
33
|
+
export declare const INJECT_LIMIT = 16000;
|
|
34
|
+
export type SectionKey = 'work' | 'lessons' | 'actions' | 'note';
|
|
35
|
+
/** One structured memory entry. */
|
|
36
|
+
export interface MemoirEntry {
|
|
37
|
+
id: string;
|
|
38
|
+
section: SectionKey;
|
|
39
|
+
title?: string;
|
|
40
|
+
content: string;
|
|
41
|
+
time: number;
|
|
42
|
+
sessionId?: string;
|
|
43
|
+
}
|
|
44
|
+
/** One project's bucket in the global index. */
|
|
45
|
+
export interface MemoirProject {
|
|
46
|
+
path: string;
|
|
47
|
+
title: string;
|
|
48
|
+
updatedAt: number;
|
|
49
|
+
entries: MemoirEntry[];
|
|
50
|
+
}
|
|
51
|
+
/** The persisted store file shape. */
|
|
52
|
+
export interface MemoirStoreFile {
|
|
53
|
+
version: number;
|
|
54
|
+
projects: Record<string, MemoirProject>;
|
|
55
|
+
}
|
|
56
|
+
/** A record payload accepted from tools and the panel API. */
|
|
57
|
+
export interface EntryPayload {
|
|
58
|
+
section: SectionKey;
|
|
59
|
+
title?: string;
|
|
60
|
+
content: string;
|
|
61
|
+
}
|
|
62
|
+
/** An in-memory snapshot of the store at one revision. */
|
|
63
|
+
export interface StoreSnapshot {
|
|
64
|
+
/** Write revision this snapshot reflects (bumped only by save()). */
|
|
65
|
+
revision: number;
|
|
66
|
+
/** Snapshot epoch: bumped on every snapshot (re)build — external changes
|
|
67
|
+
* and first loads included — so consumers can key caches on it. */
|
|
68
|
+
epoch: number;
|
|
69
|
+
/** The parsed (normalized) store file. */
|
|
70
|
+
file: MemoirStoreFile;
|
|
71
|
+
/** Disk signature backing the snapshot; null when the file was absent. */
|
|
72
|
+
stat: {
|
|
73
|
+
mtimeMs: number;
|
|
74
|
+
size: number;
|
|
75
|
+
} | null;
|
|
76
|
+
}
|
|
77
|
+
/** Cache/IO counters exposed for diagnostics and cache-hit-rate tests. */
|
|
78
|
+
export interface CacheStats {
|
|
79
|
+
/** Current write revision (bumped by record/remove). */
|
|
80
|
+
revision: number;
|
|
81
|
+
/** Current snapshot epoch (bumped on every snapshot (re)build). */
|
|
82
|
+
epoch: number;
|
|
83
|
+
/** Total load() calls. */
|
|
84
|
+
loads: number;
|
|
85
|
+
/** Warm reads served straight from the snapshot (no file read). */
|
|
86
|
+
hits: number;
|
|
87
|
+
/** Cold reads / rebuilds that consulted the file. */
|
|
88
|
+
misses: number;
|
|
89
|
+
/** hits / loads, in [0, 1]. */
|
|
90
|
+
hitRate: number;
|
|
91
|
+
/** Full file reads (parse) since construction. */
|
|
92
|
+
fileReads: number;
|
|
93
|
+
/** mtime stat probes issued against the store file. */
|
|
94
|
+
statProbes: number;
|
|
95
|
+
/** Corrupt store files that were backed up instead of overwritten. */
|
|
96
|
+
corruptBackups: number;
|
|
97
|
+
/** renderMarkdown() calls. */
|
|
98
|
+
renders: number;
|
|
99
|
+
/** Actual markdown recomputations. */
|
|
100
|
+
renderComputes: number;
|
|
101
|
+
/** (renders - renderComputes) / renders, in [0, 1]. */
|
|
102
|
+
renderHitRate: number;
|
|
103
|
+
/** Duration of the last cold load in milliseconds. */
|
|
104
|
+
lastLoadMs?: number;
|
|
105
|
+
}
|
|
106
|
+
/** How often (ms) warm load() calls re-probe the file mtime; 0 = every call. */
|
|
107
|
+
export declare const DEFAULT_MTIME_CHECK_MS = 2000;
|
|
108
|
+
/** Default store location: <home>/.dsh/dsh-memoir.json. */
|
|
109
|
+
export declare function defaultStorePath(): string;
|
|
110
|
+
/** Cross-process mutation lock defaults (roadmap §2.2). */
|
|
111
|
+
export declare const DEFAULT_LOCK_RETRY_MS = 25;
|
|
112
|
+
export declare const DEFAULT_LOCK_TIMEOUT_MS = 5000;
|
|
113
|
+
/**
|
|
114
|
+
* Run fn while holding an exclusive lock file created with openSync('wx')
|
|
115
|
+
* (atomic O_EXCL create, no race window). Retries every retryMs until
|
|
116
|
+
* timeoutMs, then throws. The lock is always released in finally — even
|
|
117
|
+
* when fn throws. Used to serialize read-modify-write store mutations
|
|
118
|
+
* across processes sharing one ~/.dsh/dsh-memoir.json.
|
|
119
|
+
*/
|
|
120
|
+
export declare function withFileLock<T>(lockPath: string, fn: () => T, options?: {
|
|
121
|
+
retryMs?: number;
|
|
122
|
+
timeoutMs?: number;
|
|
123
|
+
}): T;
|
|
124
|
+
/** `YYYY-MM-DD HH:mm` in local time. */
|
|
125
|
+
export declare function formatTime(ms: number): string;
|
|
126
|
+
/**
|
|
127
|
+
* Normalize one workspace path into a stable key:
|
|
128
|
+
* strip trailing separators, unify separators to '/'. Windows drive paths
|
|
129
|
+
* are FULLY lowercased (v0.4.2) — the canonical bucket key of C:\A /
|
|
130
|
+
* c:\a\ / C:/A is 'c:/a', so all case variants share one bucket. The
|
|
131
|
+
* display path stored on the project keeps its original case. POSIX paths
|
|
132
|
+
* are unchanged apart from trailing separators.
|
|
133
|
+
*/
|
|
134
|
+
export declare function projectKey(cwd: string): string;
|
|
135
|
+
/** Project display title: the last path segment. */
|
|
136
|
+
export declare function projectTitle(cwd: string): string;
|
|
137
|
+
/** Atomic write (unique tmp name + rename), creating the parent dir. */
|
|
138
|
+
export declare function writeFileAtomic(path: string, content: string, mode?: number): void;
|
|
139
|
+
/** Trim a long text to a bounded tail for prompt injection. */
|
|
140
|
+
export declare function bounded(value: string, limit: number): string;
|
|
141
|
+
/** Validate one record payload; returns an error message or undefined. */
|
|
142
|
+
export declare function validateEntryPayload(payload: unknown): string | undefined;
|
|
143
|
+
/**
|
|
144
|
+
* The structured memory store.
|
|
145
|
+
*/
|
|
146
|
+
export declare class MemoirStore {
|
|
147
|
+
/** The store file path. */
|
|
148
|
+
readonly path: string;
|
|
149
|
+
/** How often warm load() calls re-probe the file mtime (0 = every call). */
|
|
150
|
+
readonly mtimeCheckIntervalMs: number;
|
|
151
|
+
/** Cross-process mutation lock retry interval (withFileLock). */
|
|
152
|
+
readonly lockRetryMs: number;
|
|
153
|
+
/** Cross-process mutation lock acquisition timeout (withFileLock). */
|
|
154
|
+
readonly lockTimeoutMs: number;
|
|
155
|
+
/** The in-memory snapshot backing warm reads. */
|
|
156
|
+
private snapshot;
|
|
157
|
+
/** Write counter; bumped on every save() (record/remove). */
|
|
158
|
+
private revision;
|
|
159
|
+
/** Snapshot-rebuild counter; bumped on every snapshot (re)build. */
|
|
160
|
+
private epoch;
|
|
161
|
+
/** Timestamp of the last mtime probe (throttles external-change checks). */
|
|
162
|
+
private lastMtimeCheck;
|
|
163
|
+
private loadCount;
|
|
164
|
+
private hitCount;
|
|
165
|
+
private fileReadCount;
|
|
166
|
+
private statProbeCount;
|
|
167
|
+
private corruptBackupCount;
|
|
168
|
+
private lastLoadMs;
|
|
169
|
+
/** renderMarkdown cache: project key → { signature, markdown }. */
|
|
170
|
+
private renderCache;
|
|
171
|
+
private renderCount;
|
|
172
|
+
private renderComputeCount;
|
|
173
|
+
/**
|
|
174
|
+
* @param path - store file path (defaults to the standard location).
|
|
175
|
+
* @param options.mtimeCheckIntervalMs - mtime probe throttle; 0 probes on
|
|
176
|
+
* every load (tests), defaults to a low-frequency 2000ms.
|
|
177
|
+
* @param options.lockRetryMs / lockTimeoutMs - cross-process mutation lock
|
|
178
|
+
* tuning (tests shrink these; defaults 25ms / 5000ms).
|
|
179
|
+
*/
|
|
180
|
+
constructor(path?: string, options?: {
|
|
181
|
+
mtimeCheckIntervalMs?: number;
|
|
182
|
+
lockRetryMs?: number;
|
|
183
|
+
lockTimeoutMs?: number;
|
|
184
|
+
});
|
|
185
|
+
/** The cross-process lock file guarding mutations of this store. */
|
|
186
|
+
private lockFilePath;
|
|
187
|
+
/**
|
|
188
|
+
* Run one read-modify-write mutation inside the cross-process lock.
|
|
189
|
+
* Inside the critical section the in-memory snapshot is dropped and the
|
|
190
|
+
* store is re-read from disk, so a process whose snapshot went stale
|
|
191
|
+
* mutates the latest on-disk state (no lost update between processes).
|
|
192
|
+
*/
|
|
193
|
+
private mutateLocked;
|
|
194
|
+
/** Current store revision (0 before the first load/save). */
|
|
195
|
+
currentRevision(): number;
|
|
196
|
+
/** Stat the store file into the snapshot signature (null when absent). */
|
|
197
|
+
private statNow;
|
|
198
|
+
/**
|
|
199
|
+
* Load and normalize the store.
|
|
200
|
+
*
|
|
201
|
+
* Revision-based snapshot cache: the first call of a process reads and
|
|
202
|
+
* parses the file; every later call returns the in-memory snapshot without
|
|
203
|
+
* touching disk. External file changes (another dsh process) are picked up
|
|
204
|
+
* by a low-frequency mtime probe (mtimeCheckIntervalMs). Absence is
|
|
205
|
+
* negatively cached the same way. Corrupt JSON is renamed to a
|
|
206
|
+
* `.corrupt.<timestamp>` backup before the store starts fresh.
|
|
207
|
+
*/
|
|
208
|
+
load(): MemoirStoreFile;
|
|
209
|
+
/** Normalize a parsed store file: mint ids, coerce shapes, merge duplicate
|
|
210
|
+
* buckets that normalize to the same project key (legacy Windows variants). */
|
|
211
|
+
private normalize;
|
|
212
|
+
/** Persist the store atomically (0600 — may contain user's notes). */
|
|
213
|
+
save(file: MemoirStoreFile): void;
|
|
214
|
+
/** Drop the snapshot so the next load() re-reads and re-parses the file. */
|
|
215
|
+
invalidate(): void;
|
|
216
|
+
/** Cache/IO counters (diagnostics + tests). */
|
|
217
|
+
stats(): CacheStats;
|
|
218
|
+
/** One project record, or undefined. */
|
|
219
|
+
project(cwd: string): MemoirProject | undefined;
|
|
220
|
+
/** Entries of one project in insertion order. */
|
|
221
|
+
entries(cwd: string): MemoirEntry[];
|
|
222
|
+
/** Compact per-project summaries (path, title, entry count, updatedAt). */
|
|
223
|
+
listProjects(): Array<{
|
|
224
|
+
key: string;
|
|
225
|
+
path: string;
|
|
226
|
+
title: string;
|
|
227
|
+
count: number;
|
|
228
|
+
updatedAt: number;
|
|
229
|
+
}>;
|
|
230
|
+
/** Append one entry and regenerate the project markdown. Returns the entry. */
|
|
231
|
+
record(cwd: string, payload: EntryPayload, sessionId?: string): MemoirEntry;
|
|
232
|
+
/** Remove one entry by id; regenerates the project markdown. */
|
|
233
|
+
remove(cwd: string, id: string): boolean;
|
|
234
|
+
/** Render one entry as a markdown bullet line. */
|
|
235
|
+
renderEntryLine(entry: MemoirEntry): string;
|
|
236
|
+
/** Cheap O(1) signature of one project's entries (count + tail id/time). */
|
|
237
|
+
private renderSignature;
|
|
238
|
+
/** Regenerate the full PROJECT_MEMORY.md content for one project. */
|
|
239
|
+
renderMarkdown(cwd: string): string;
|
|
240
|
+
/** Pure markdown assembly for one project's entries (no cache access). */
|
|
241
|
+
private renderMarkdownNow;
|
|
242
|
+
/** Absolute path of one project's memory file (no write). */
|
|
243
|
+
projectFilePath(cwd: string): string;
|
|
244
|
+
/** Regenerate and write the project memory file; returns its path. */
|
|
245
|
+
writeProjectFile(cwd: string): string;
|
|
246
|
+
}
|
|
247
|
+
/** SHA-256 hex digest of a string, truncated for prompt-stability hashing. */
|
|
248
|
+
export declare function sha256(text: string, length?: number): string;
|