@dsh-cc/memory-consolidation 0.5.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/lib/index.js ADDED
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Background memory consolidation: turn-end extraction and the three-gate
3
+ * dream rewrite.
4
+ *
5
+ * `agent/turn-stopping` fires an extraction subagent (via `ctx.jobs` +
6
+ * `ctx.subagents`, tools restricted to read/search) that reports durable facts
7
+ * as structured output, and evaluates the dream gates (time, session count,
8
+ * lock) to schedule a read-only review whose structured output rewrites
9
+ * MEMORY.md and the topic files. The forks hold no write tools — the memory
10
+ * directory sits outside the session workspace, so the fs sandbox would fence
11
+ * every model-side write with no escalation path from a background job; the
12
+ * plugin validates each reported batch and writes it host-side under a
13
+ * per-call policy confined to the memory directory (see `writeback.ts`). A
14
+ * failed dream rolls back the lock so the time gate re-opens.
15
+ *
16
+ * @module @dsh-cc/memory-consolidation
17
+ */
18
+ import { join } from 'node:path';
19
+ import z from '@deepseek-ai/schemastery';
20
+ import { delegationDepthOf } from '@deepseek-ai/dsh-subagent';
21
+ import { defaultDshHome } from '@deepseek-ai/dsh-home-paths';
22
+ import { MEMORY_TOOL_FILTER } from "./tools.js";
23
+ import { buildConsolidationPrompt, buildExtractionPrompt } from "./prompts.js";
24
+ import { gatesPass } from "./gates.js";
25
+ import { readLastConsolidatedAt, rollbackLock, tryAcquireLock, LOCK_STALE_MS } from "./lock.js";
26
+ import { MEMORY_WRITES_SCHEMA, memoryWritePolicy, resolveWorkspaceMemoryDir, validateMemoryWrites, writeMemoryFiles, } from '@dsh-cc/memory';
27
+ export { LOCK_FILE, LOCK_STALE_MS, readLastConsolidatedAt, rollbackLock, tryAcquireLock } from "./lock.js";
28
+ export { gatesPass, timeGatePasses, sessionGatePasses } from "./gates.js";
29
+ export { MEMORY_AGENT_TOOLS, MEMORY_TOOL_FILTER } from "./tools.js";
30
+ export { buildConsolidationPrompt, buildExtractionPrompt } from "./prompts.js";
31
+ // The write-back lives in @dsh-cc/memory (the memory directory owner);
32
+ // re-exported here for consumers of the pre-move surface.
33
+ export { MEMORY_WRITES_SCHEMA, WRITEBACK_MAX_FILE_BYTES, WRITEBACK_MAX_FILES, WRITEBACK_MAX_TOTAL_BYTES, memoryWritePolicy, validateMemoryWrites, writeMemoryFiles, } from '@dsh-cc/memory';
34
+ export const name = 'memory-consolidation';
35
+ /** Services required for background jobs and the subagent provider. */
36
+ export const inject = ['jobs', 'subagents'];
37
+ export const Config = z.object({
38
+ memoryHome: z.string(),
39
+ extractEnabled: z.boolean().default(true),
40
+ dreamEnabled: z.boolean().default(true),
41
+ minHours: z.number().default(24),
42
+ minSessions: z.number().default(5),
43
+ lockStaleMs: z.number().default(LOCK_STALE_MS),
44
+ subagentProviderName: z.string().default('fork'),
45
+ });
46
+ /**
47
+ * Start a memory-scoped forked subagent as a background job. The fork reports
48
+ * its file set via `outputSchema`; on settlement the plugin validates the
49
+ * batch and writes it host-side under a policy confined to `dir`. Resolves to
50
+ * a control object with an abort hook and a settle promise (true only when
51
+ * the reported writes were validated and persisted).
52
+ */
53
+ async function startMemoryJob(ctx, agent, dir, provider, label, prompt) {
54
+ const jobs = ctx.get('jobs');
55
+ const subagents = ctx.get('subagents');
56
+ if (jobs === undefined || subagents === undefined) {
57
+ return { abort: () => { }, settled: Promise.resolve(false) };
58
+ }
59
+ const fs = ctx.get('fs');
60
+ const controller = new AbortController();
61
+ // `subagents.start` is async upstream — awaiting it is what exposes the run's
62
+ // `result` promise. Reading `run.result` on the un-awaited Promise throws
63
+ // "Cannot read properties of undefined (reading 'then')" and poisons the
64
+ // turn-stopping dispatch.
65
+ const run = await subagents.start(provider, {
66
+ label,
67
+ signal: controller.signal,
68
+ prompt: [{ type: 'text', text: prompt }],
69
+ parent: agent,
70
+ toolFilter: MEMORY_TOOL_FILTER,
71
+ // Defense-in-depth recursion cap: the top-level listener already gates on
72
+ // depth zero, so this fork's child never delegates. maxDepth is compared
73
+ // against the CHILD's resolved depth (parent + 1); a top-level parent's
74
+ // child resolves to 1 and passes, a grandchild to 2 is rejected.
75
+ maxDepth: 1,
76
+ outputSchema: MEMORY_WRITES_SCHEMA,
77
+ });
78
+ // Real job-done wiring: `done` maps the subagent outcome onto the JobHooks
79
+ // contract (must never reject). Aborted → killed (rolls back the dream
80
+ // lock); a non-completed stopReason, a missing/invalid payload, or a
81
+ // write-back failure → failed with detail; a validated, persisted batch →
82
+ // completed. All branches resolve.
83
+ const done = run.result.then(async (res) => {
84
+ if (controller.signal.aborted)
85
+ return { status: 'killed' };
86
+ if (res?.stopReason !== 'completed') {
87
+ return { status: 'failed', detail: `memory fork ended with stopReason ${String(res?.stopReason)}` };
88
+ }
89
+ if (fs === undefined) {
90
+ return { status: 'failed', detail: 'fs seam unavailable for memory write-back' };
91
+ }
92
+ try {
93
+ const writes = validateMemoryWrites(res.structured);
94
+ await writeMemoryFiles(fs, dir, writes);
95
+ return { status: 'completed' };
96
+ }
97
+ catch (err) {
98
+ return { status: 'failed', detail: String(err) };
99
+ }
100
+ }, (err) => controller.signal.aborted ? { status: 'killed' } : { status: 'failed', detail: String(err) });
101
+ const settled = done.then(o => o.status === 'completed');
102
+ jobs.start({
103
+ kind: 'subagent',
104
+ label,
105
+ owner: agent,
106
+ run: () => ({
107
+ cancel: (reason) => { controller.abort(reason); },
108
+ done,
109
+ }),
110
+ });
111
+ return { abort: (reason) => { controller.abort(reason); }, settled };
112
+ }
113
+ /**
114
+ * Register the consolidation plugin.
115
+ * @param ctx - the host context with jobs, subagents, fs, and sessions.
116
+ * @param config - consolidation behavior knobs.
117
+ */
118
+ export function apply(ctx, config = {}) {
119
+ // The memory home is the ROOT: extraction/dream write into the turning
120
+ // agent's repository directory (`<home>/projects/<slug>` of the canonical
121
+ // git root), never the shared root, so memories stay isolated per repo.
122
+ const home = config.memoryHome ?? join(defaultDshHome(), 'memory');
123
+ const provider = config.subagentProviderName ?? 'fork';
124
+ const minHours = config.minHours ?? 24;
125
+ const minSessions = config.minSessions ?? 5;
126
+ // Per-session extraction single-flight. Keyed by session id so each top-level
127
+ // agent's in-flight flag and last-spawned event count are isolated.
128
+ const flight = new Map();
129
+ // One dream in flight across the whole plugin instance (per memory dir).
130
+ let dreamInFlight = false;
131
+ // Reset the flight state when this plugin fiber is disposed (hygiene / test
132
+ // isolation). cordis `ctx.on` is typed strictly to `keyof Events`, so cleanup
133
+ // is registered as a fiber effect rather than a 'dispose' listener.
134
+ ctx.effect(() => () => {
135
+ flight.clear();
136
+ dreamInFlight = false;
137
+ }, 'memory-consolidation: reset flight state');
138
+ ctx.on('agent/turn-stopping', ({ agent, signal }) => {
139
+ // All predicates are synchronous and run before any await, so the flags
140
+ // below are set before a second, interleaved turn-stopping could observe
141
+ // them.
142
+ if (signal.aborted)
143
+ return;
144
+ if (!isTopLevel(agent))
145
+ return;
146
+ const sessionId = agent.session.header.id;
147
+ if (config.extractEnabled ?? true) {
148
+ const entry = flight.get(sessionId);
149
+ // Single-flight: skip while an extraction is in flight or when no new
150
+ // events have arrived since the last spawn.
151
+ if (!entry?.extracting && agent.session.events.length !== entry?.lastEvents) {
152
+ flight.set(sessionId, { extracting: true, lastEvents: agent.session.events.length });
153
+ void runExtraction(ctx, agent, home, provider).finally(() => {
154
+ const cur = flight.get(sessionId);
155
+ if (cur)
156
+ cur.extracting = false;
157
+ });
158
+ }
159
+ }
160
+ if (config.dreamEnabled ?? true) {
161
+ if (!dreamInFlight) {
162
+ dreamInFlight = true;
163
+ void runDream(ctx, agent, home, provider, minHours, minSessions)
164
+ .catch(() => { })
165
+ .finally(() => { dreamInFlight = false; });
166
+ }
167
+ }
168
+ });
169
+ }
170
+ /**
171
+ * Whether an agent is top-level (not a delegated subagent). This is the root
172
+ * fix for the extraction/dream recursion: a subagent's own turn-end must never
173
+ * spawn another memory fork. Fails CLOSED — any throw from reading the depth
174
+ * treats the agent as a child so nothing is spawned.
175
+ */
176
+ function isTopLevel(agent) {
177
+ try {
178
+ return delegationDepthOf(agent) === 0;
179
+ }
180
+ catch {
181
+ return false;
182
+ }
183
+ }
184
+ /** Surface event types whose count a single extraction batch reviews. */
185
+ const SURFACE_EVENT_TYPES = new Set(['user/message', 'assistant/message', 'tool/result']);
186
+ /** Upper bound on the injected index: first 200 lines or 8 KiB, whichever first. */
187
+ const INDEX_CAP_LINES = 200;
188
+ const INDEX_CAP_BYTES = 8 * 1024;
189
+ const INDEX_TRUNCATED_MARKER = '(index truncated; rely on MEMORY.md in-dir for the rest)';
190
+ async function runExtraction(ctx, agent, home, provider) {
191
+ // The extraction writes into the turning agent's repository directory —
192
+ // the shared home root holds only explicitly-global memories.
193
+ const dir = resolveWorkspaceMemoryDir(home, sessionTranscriptDir(agent));
194
+ // Only model-visible surface events count toward the batch size.
195
+ const surfaceCount = agent.session.events.filter((e) => SURFACE_EVENT_TYPES.has(e.type)).length;
196
+ // The index read happens AFTER the in-flight/content gates (runExtraction is
197
+ // only reached once a spawn is committed), so a skipped spawn never pays the
198
+ // fs cost. Any failure here degrades to an empty index and still spawns.
199
+ const existingIndex = await readExistingIndex(ctx, dir);
200
+ const prompt = buildExtractionPrompt(surfaceCount, dir, existingIndex);
201
+ // Fire-and-forget: extraction failure must never fail the turn itself. The
202
+ // job status still reflects the real outcome (the fork's structured report
203
+ // is validated and written host-side before `done` completes).
204
+ return startMemoryJob(ctx, agent, dir, provider, 'extract-memories', prompt)
205
+ .catch(() => { })
206
+ .then(() => { });
207
+ }
208
+ /**
209
+ * Read the existing topic index to inject into the extraction prompt: the
210
+ * MEMORY.md body when present, else the names of sibling topic `.md` files.
211
+ * Swallow-all: any error or an absent fs yields an empty index so a spawn is
212
+ * never blocked by the read. Content is capped at 200 lines / 8 KiB so a huge
213
+ * index cannot bloat the prompt.
214
+ */
215
+ async function readExistingIndex(ctx, dir) {
216
+ const fs = ctx.get('fs');
217
+ if (fs === undefined)
218
+ return '';
219
+ try {
220
+ const memoryTarget = await fs.resolve(join(dir, 'MEMORY.md'));
221
+ const info = await fs.stat(memoryTarget);
222
+ let raw;
223
+ if (info !== undefined) {
224
+ raw = await fs.readText(memoryTarget);
225
+ }
226
+ else {
227
+ // No index file: fall back to listing topic `.md` files in the directory.
228
+ const dirTarget = await fs.resolve(dir);
229
+ const entries = await fs.listDir(dirTarget);
230
+ raw = entries
231
+ .filter((e) => e.type === 'file' && e.name.endsWith('.md') && e.name !== 'MEMORY.md')
232
+ .map((e) => e.name)
233
+ .sort()
234
+ .join('\n');
235
+ }
236
+ if (raw === '')
237
+ return '';
238
+ // Cap at the first 200 lines OR 8 KiB, whichever comes first; append a
239
+ // marker when truncated so the model knows to rely on the in-dir file.
240
+ const lines = raw.split('\n');
241
+ const kept = [];
242
+ let bytes = 0;
243
+ let truncated = false;
244
+ for (const line of lines) {
245
+ if (kept.length >= INDEX_CAP_LINES) {
246
+ truncated = true;
247
+ break;
248
+ }
249
+ const add = line.length + (kept.length > 0 ? 1 : 0);
250
+ if (bytes + add > INDEX_CAP_BYTES) {
251
+ truncated = true;
252
+ break;
253
+ }
254
+ kept.push(line);
255
+ bytes += add;
256
+ }
257
+ const capped = kept.join('\n');
258
+ return truncated ? `${capped}\n${INDEX_TRUNCATED_MARKER}` : capped;
259
+ }
260
+ catch {
261
+ return '';
262
+ }
263
+ }
264
+ async function runDream(ctx, agent, home, provider, minHours, minSessions) {
265
+ const fs = ctx.get('fs');
266
+ if (fs === undefined)
267
+ return;
268
+ const dir = resolveWorkspaceMemoryDir(home, sessionTranscriptDir(agent));
269
+ const now = Date.now();
270
+ const lastAt = await readLastConsolidatedAt(fs, dir);
271
+ const sessionIds = listNewSessions(ctx, lastAt);
272
+ if (!gatesPass({
273
+ lastConsolidatedAt: lastAt,
274
+ now,
275
+ minHours,
276
+ sessionCount: sessionIds.length,
277
+ minSessions,
278
+ }))
279
+ return;
280
+ const priorAt = await tryAcquireLock(fs, dir, process.pid, now, memoryWritePolicy(dir));
281
+ if (priorAt === null)
282
+ return;
283
+ const prompt = buildConsolidationPrompt(dir, sessionTranscriptDir(agent), sessionIds);
284
+ const job = await startMemoryJob(ctx, agent, dir, provider, 'memory-consolidation', prompt);
285
+ void job.settled.then((ok) => {
286
+ if (!ok)
287
+ void rollbackLock(fs, dir, priorAt, memoryWritePolicy(dir));
288
+ });
289
+ }
290
+ /** Live sessions are the transcripts available to review; absent sessions skip. */
291
+ function listNewSessions(ctx, lastAt) {
292
+ const sessions = ctx.get('sessions');
293
+ if (sessions === undefined)
294
+ return [];
295
+ return sessions
296
+ .list()
297
+ // Subagent sessions (validated delegationDepth > 0) are excluded from both
298
+ // the min-sessions count and the dream input: their content is already
299
+ // covered by turn-end extraction. Absent/invalid depth is treated as 0.
300
+ .filter(session => {
301
+ const d = session.header?.delegationDepth;
302
+ return !(Number.isSafeInteger(d) && d > 0);
303
+ })
304
+ .map(session => ({ id: session.id, at: sessionStartOf(session) }))
305
+ .filter(session => session.at > lastAt)
306
+ .sort((a, b) => a.at - b.at)
307
+ .map(session => session.id);
308
+ }
309
+ /**
310
+ * Session start epoch, defensively read from the header. The dream gate is
311
+ * skip-oriented, so an unreadable or absent timestamp conservatively counts as
312
+ * NEW (over-inclusion only costs a re-read). Accepted limitation: a session
313
+ * created before `lastAt` but still active after it is excluded from the dream
314
+ * input — turn-end extraction covers recent content; dream is a coarse
315
+ * periodic pass.
316
+ */
317
+ function sessionStartOf(session) {
318
+ const at = session.header?.createdAt;
319
+ return Number.isSafeInteger(at) && at > 0 ? at : Number.MAX_SAFE_INTEGER;
320
+ }
321
+ /** The transcript directory is the agent's cwd. */
322
+ function sessionTranscriptDir(agent) {
323
+ return agent.session.header.cwd ?? process.cwd();
324
+ }
325
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@dsh-cc/memory-consolidation`.
3
+ * @module @dsh-cc/memory-consolidation/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "memory-consolidation-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invariant.d.ts","sourceRoot":"","sources":["../src/invariant.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAKlD,oCAAoC;AACpC,eAAO,MAAM,IAAI,mCAAmC,CAAA;AACpD,2EAA2E;AAC3E,eAAO,MAAM,MAAM,UAAiB,CAAA;AAQpC;;;;GAIG;AACH,eAAO,MAAM,KAAK,GAAI,KAAK,OAAO,KAAG,OAAO,CAAC,MAAM,IAAI,CACU,CAAA"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Package-owned invariant companion for `@dsh-cc/memory-consolidation`.
3
+ * @module @dsh-cc/memory-consolidation/invariant
4
+ */
5
+ const PACKAGE_NAME = '@dsh-cc/memory-consolidation';
6
+ /** Cordis companion plugin name. */
7
+ export const name = 'memory-consolidation-invariant';
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export const inject = ['invariants'];
10
+ /**
11
+ * No runtime invariant: the dream gates and lock acquisition are enforced at
12
+ * the owning plugin seams, with no independent event/data relation.
13
+ */
14
+ const install = () => { };
15
+ /**
16
+ * Register this package's invariant companion.
17
+ * @param ctx - Cordis context carrying the invariant service.
18
+ * @returns the installed registration's disposer after setup succeeds.
19
+ */
20
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
21
+ /* jscpd:ignore-end */
22
+ //# sourceMappingURL=invariant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invariant.js","sourceRoot":"","sources":["../src/invariant.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,YAAY,GAAG,8BAA8B,CAAA;AAEnD,oCAAoC;AACpC,MAAM,CAAC,MAAM,IAAI,GAAG,gCAAgC,CAAA;AACpD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,YAAY,CAAC,CAAA;AAEpC;;;GAGG;AACH,MAAM,OAAO,GAAuB,GAAG,EAAE,GAAE,CAAC,CAAA;AAE5C;;;;GAIG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,GAAY,EAAuB,EAAE,CACzD,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAA;AACjE,sBAAsB"}
package/lib/lock.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Memory consolidation lock and last-consolidated timestamp.
3
+ *
4
+ * The `ctx.fs` seam exposes no mtime, so the lock file's body carries the
5
+ * holder PID and the last-consolidated epoch rather than the file mtime (the
6
+ * reference uses mtime as the timestamp). A holder is stale and reclaimable
7
+ * once its stored timestamp passes {@link LOCK_STALE_MS}.
8
+ * @module @dsh-cc/memory-consolidation/lock
9
+ */
10
+ import type { FileSystem } from '@deepseek-ai/dsh-fs';
11
+ import type { MemoryWritePolicy } from '@dsh-cc/memory';
12
+ /** Lock filename inside the memory directory. */
13
+ export declare const LOCK_FILE = ".consolidation-lock";
14
+ /** A holder is stale past this even if it is still alive (also guards PID reuse). */
15
+ export declare const LOCK_STALE_MS: number;
16
+ /**
17
+ * Read the last consolidated epoch, 0 when no lock file exists.
18
+ * @param fs - the filesystem seam.
19
+ * @param dir - the memory directory holding the lock.
20
+ * @returns the epoch ms of the last consolidation, or 0.
21
+ */
22
+ export declare function readLastConsolidatedAt(fs: FileSystem, dir: string): Promise<number>;
23
+ /**
24
+ * Acquire the consolidation lock. Returns the PRIOR consolidated epoch (for
25
+ * rollback), or `null` when a non-stale holder blocks acquisition.
26
+ *
27
+ * On success the lock is written with this process's PID and `now` as the
28
+ * consolidated epoch. A stale holder (older than {@link LOCK_STALE_MS}) is
29
+ * reclaimed; a crash mid-consolidation leaves a stale file the next process
30
+ * reclaims. The prior epoch is captured before the write so a caller can roll
31
+ * back to the exact pre-acquire state.
32
+ * @param fs - the filesystem seam.
33
+ * @param dir - the memory directory holding the lock.
34
+ * @param pid - this process's id.
35
+ * @param now - the current epoch used as the new consolidated timestamp.
36
+ * @param policy - per-call sandbox policy for the lock write; required when the
37
+ * memory directory sits outside the caller's sandbox writable roots.
38
+ * @returns the pre-acquire epoch (0 when none) to roll back on failure, or `null` when blocked.
39
+ */
40
+ export declare function tryAcquireLock(fs: FileSystem, dir: string, pid: number, now: number, policy?: MemoryWritePolicy): Promise<number | null>;
41
+ /**
42
+ * Roll the lock back to `priorAt` after a failed or killed consolidation.
43
+ * A `priorAt` of 0 encodes the no-file state (the next read returns 0) with
44
+ * the holder PID cleared. Best-effort: failures are swallowed so the caller's
45
+ * teardown does not throw.
46
+ * @param fs - the filesystem seam.
47
+ * @param dir - the memory directory holding the lock.
48
+ * @param priorAt - the pre-acquire consolidated epoch to restore.
49
+ * @param policy - per-call sandbox policy for the lock write (see tryAcquireLock).
50
+ */
51
+ export declare function rollbackLock(fs: FileSystem, dir: string, priorAt: number, policy?: MemoryWritePolicy): Promise<void>;
52
+ //# sourceMappingURL=lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../src/lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAY,MAAM,qBAAqB,CAAA;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAEvD,iDAAiD;AACjD,eAAO,MAAM,SAAS,wBAAwB,CAAA;AAE9C,qFAAqF;AACrF,eAAO,MAAM,aAAa,QAAiB,CAAA;AAmB3C;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQzF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,iBAAiB,GACzB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA+BxB;AAED;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAAC,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAI1H"}
package/lib/lock.js ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Memory consolidation lock and last-consolidated timestamp.
3
+ *
4
+ * The `ctx.fs` seam exposes no mtime, so the lock file's body carries the
5
+ * holder PID and the last-consolidated epoch rather than the file mtime (the
6
+ * reference uses mtime as the timestamp). A holder is stale and reclaimable
7
+ * once its stored timestamp passes {@link LOCK_STALE_MS}.
8
+ * @module @dsh-cc/memory-consolidation/lock
9
+ */
10
+ import { join } from 'node:path';
11
+ /** Lock filename inside the memory directory. */
12
+ export const LOCK_FILE = '.consolidation-lock';
13
+ /** A holder is stale past this even if it is still alive (also guards PID reuse). */
14
+ export const LOCK_STALE_MS = 60 * 60 * 1000;
15
+ /** Content of the lock file: `<pid>\n<lastConsolidatedAtMs>\n`. */
16
+ const format = (pid, at) => `${pid}\n${at}\n`;
17
+ function parse(content) {
18
+ const [pidRaw, atRaw] = content.trim().split('\n');
19
+ const pid = pidRaw !== undefined && Number.isFinite(Number(pidRaw)) ? Number(pidRaw) : undefined;
20
+ const at = atRaw !== undefined && Number.isFinite(Number(atRaw)) ? Number(atRaw) : 0;
21
+ return { pid, at };
22
+ }
23
+ /** Resolve the lock target and whether it exists. */
24
+ async function lockTarget(fs, dir) {
25
+ const target = await fs.resolve(join(dir, LOCK_FILE));
26
+ const info = await fs.stat(target);
27
+ return { target, absent: info === undefined };
28
+ }
29
+ /**
30
+ * Read the last consolidated epoch, 0 when no lock file exists.
31
+ * @param fs - the filesystem seam.
32
+ * @param dir - the memory directory holding the lock.
33
+ * @returns the epoch ms of the last consolidation, or 0.
34
+ */
35
+ export async function readLastConsolidatedAt(fs, dir) {
36
+ const { target, absent } = await lockTarget(fs, dir);
37
+ if (absent)
38
+ return 0;
39
+ try {
40
+ return parse(await fs.readText(target)).at;
41
+ }
42
+ catch {
43
+ return 0;
44
+ }
45
+ }
46
+ /**
47
+ * Acquire the consolidation lock. Returns the PRIOR consolidated epoch (for
48
+ * rollback), or `null` when a non-stale holder blocks acquisition.
49
+ *
50
+ * On success the lock is written with this process's PID and `now` as the
51
+ * consolidated epoch. A stale holder (older than {@link LOCK_STALE_MS}) is
52
+ * reclaimed; a crash mid-consolidation leaves a stale file the next process
53
+ * reclaims. The prior epoch is captured before the write so a caller can roll
54
+ * back to the exact pre-acquire state.
55
+ * @param fs - the filesystem seam.
56
+ * @param dir - the memory directory holding the lock.
57
+ * @param pid - this process's id.
58
+ * @param now - the current epoch used as the new consolidated timestamp.
59
+ * @param policy - per-call sandbox policy for the lock write; required when the
60
+ * memory directory sits outside the caller's sandbox writable roots.
61
+ * @returns the pre-acquire epoch (0 when none) to roll back on failure, or `null` when blocked.
62
+ */
63
+ export async function tryAcquireLock(fs, dir, pid, now, policy) {
64
+ const { target, absent } = await lockTarget(fs, dir);
65
+ let priorAt = 0;
66
+ if (!absent) {
67
+ try {
68
+ const { at } = parse(await fs.readText(target));
69
+ priorAt = at;
70
+ if (at > 0 && now - at < LOCK_STALE_MS) {
71
+ // A recently stamped lock is held; the stale window reclaims crashed holders.
72
+ return null;
73
+ }
74
+ }
75
+ catch {
76
+ // Unreadable or malformed lock: reclaim below, treating it as empty.
77
+ }
78
+ }
79
+ await fs.writeText(target, format(pid, now), undefined, undefined, policy);
80
+ // Read the lock back once to verify we own it. If the content is not EXACTLY
81
+ // ours, a cross-process writer interleaved between our read and write and we
82
+ // lost the race. This narrows the TOCTOU window to a sub-millisecond
83
+ // symmetric-collision; the residual race is accepted (the stale-window
84
+ // reclaim handles crashes, and the worst case is a redundant MEMORY.md
85
+ // rewrite bounded by minHours). A read-back THROW is treated as a
86
+ // verify-failure too (conservative).
87
+ let owned = false;
88
+ try {
89
+ owned = (await fs.readText(target)) === format(pid, now);
90
+ }
91
+ catch {
92
+ owned = false;
93
+ }
94
+ if (!owned)
95
+ return null;
96
+ return priorAt;
97
+ }
98
+ /**
99
+ * Roll the lock back to `priorAt` after a failed or killed consolidation.
100
+ * A `priorAt` of 0 encodes the no-file state (the next read returns 0) with
101
+ * the holder PID cleared. Best-effort: failures are swallowed so the caller's
102
+ * teardown does not throw.
103
+ * @param fs - the filesystem seam.
104
+ * @param dir - the memory directory holding the lock.
105
+ * @param priorAt - the pre-acquire consolidated epoch to restore.
106
+ * @param policy - per-call sandbox policy for the lock write (see tryAcquireLock).
107
+ */
108
+ export async function rollbackLock(fs, dir, priorAt, policy) {
109
+ const target = await fs.resolve(join(dir, LOCK_FILE)).catch(() => undefined);
110
+ if (target === undefined)
111
+ return;
112
+ await fs.writeText(target, format(0, priorAt), undefined, undefined, policy).catch(() => { });
113
+ }
114
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../src/lock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Prompt builders for the extraction and dream forks. Both are pure text
3
+ * assembly so they are unit-testable and stable model-visible contracts.
4
+ *
5
+ * The forks hold no write tools: they report their file set through the
6
+ * driver-injected `structured_output` tool, and the plugin writes the files
7
+ * host-side (see `writeback.ts`). The prompts below are the model-facing half
8
+ * of that contract.
9
+ * @module @dsh-cc/memory-consolidation/prompts
10
+ */
11
+ /**
12
+ * Build the <system-reminder> user prompt that extracts durable facts from a
13
+ * batch of recent model-visible messages into structured memory writes.
14
+ * @param surfaceMessageCount - how many model-visible surface events (user/message
15
+ * + assistant/message + tool/result) this run reviews.
16
+ * @param memoryDir - the memory directory the reported files belong to.
17
+ * @param existingIndex - a prior topic manifest, if any.
18
+ * @returns the prompt text.
19
+ */
20
+ export declare function buildExtractionPrompt(surfaceMessageCount: number, memoryDir: string, existingIndex: string): string;
21
+ /**
22
+ * Build the <system-reminder> user prompt that reviews past sessions and
23
+ * consolidates them into the memory directory, returned as structured writes.
24
+ * @param memoryDir - the memory directory being rewritten.
25
+ * @param transcriptDir - the directory holding past session transcripts.
26
+ * @param sessionHints - a list of session ids to review.
27
+ * @returns the prompt text.
28
+ */
29
+ export declare function buildConsolidationPrompt(memoryDir: string, transcriptDir: string, sessionHints: readonly string[]): string;
30
+ //# sourceMappingURL=prompts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,mBAAmB,EAAE,MAAM,EAC3B,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GACpB,MAAM,CAeR;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,SAAS,MAAM,EAAE,GAC9B,MAAM,CAYR"}