@dsh-cc/memory 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.
Files changed (53) hide show
  1. package/LICENSE +201 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +158 -0
  4. package/README.zh.md +125 -0
  5. package/lib/index.d.ts +75 -0
  6. package/lib/index.d.ts.map +1 -0
  7. package/lib/index.js +91 -0
  8. package/lib/index.js.map +1 -0
  9. package/lib/invariant.d.ts +16 -0
  10. package/lib/invariant.d.ts.map +1 -0
  11. package/lib/invariant.js +22 -0
  12. package/lib/invariant.js.map +1 -0
  13. package/lib/parser.d.ts +30 -0
  14. package/lib/parser.d.ts.map +1 -0
  15. package/lib/parser.js +96 -0
  16. package/lib/parser.js.map +1 -0
  17. package/lib/paths.d.ts +98 -0
  18. package/lib/paths.d.ts.map +1 -0
  19. package/lib/paths.js +236 -0
  20. package/lib/paths.js.map +1 -0
  21. package/lib/recall.d.ts +91 -0
  22. package/lib/recall.d.ts.map +1 -0
  23. package/lib/recall.js +253 -0
  24. package/lib/recall.js.map +1 -0
  25. package/lib/save.d.ts +53 -0
  26. package/lib/save.d.ts.map +1 -0
  27. package/lib/save.js +180 -0
  28. package/lib/save.js.map +1 -0
  29. package/lib/scan.d.ts +29 -0
  30. package/lib/scan.d.ts.map +1 -0
  31. package/lib/scan.js +70 -0
  32. package/lib/scan.js.map +1 -0
  33. package/lib/section.d.ts +129 -0
  34. package/lib/section.d.ts.map +1 -0
  35. package/lib/section.js +353 -0
  36. package/lib/section.js.map +1 -0
  37. package/lib/team.d.ts +90 -0
  38. package/lib/team.d.ts.map +1 -0
  39. package/lib/team.js +167 -0
  40. package/lib/team.js.map +1 -0
  41. package/lib/truncate.d.ts +35 -0
  42. package/lib/truncate.d.ts.map +1 -0
  43. package/lib/truncate.js +52 -0
  44. package/lib/truncate.js.map +1 -0
  45. package/lib/types.d.ts +34 -0
  46. package/lib/types.d.ts.map +1 -0
  47. package/lib/types.js +18 -0
  48. package/lib/types.js.map +1 -0
  49. package/lib/writeback.d.ts +85 -0
  50. package/lib/writeback.d.ts.map +1 -0
  51. package/lib/writeback.js +121 -0
  52. package/lib/writeback.js.map +1 -0
  53. package/package.json +65 -0
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Dynamic recall: an `agent/pre-step` listener that asks a small-model side
3
+ * query (a forked subagent) which topic files are relevant to the turn, then
4
+ * injects their bodies through `agent.inject()`. Topic files already shown
5
+ * this session are never re-injected. Recall runs for top-level agents only —
6
+ * a forked child (including the selector itself) never recalls, so no chain of
7
+ * memory-recall subagents can form. Best-effort: absence of the subagent
8
+ * service or provider skips recall without error.
9
+ * @module @dsh-cc/memory/recall
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ import type { Agent } from '@deepseek-ai/dsh-agent';
13
+ declare module '@deepseek-ai/dsh-llm' {
14
+ interface MessageSourceMap {
15
+ memory: {
16
+ kind: 'memory';
17
+ };
18
+ }
19
+ }
20
+ /** How many topic files the selector may surface per query. */
21
+ export declare const MAX_RECALL_MEMORIES = 5;
22
+ /** One topic file offered to the selector. */
23
+ export interface RecallCandidate {
24
+ path: string;
25
+ filename: string;
26
+ description: string;
27
+ }
28
+ /** The async relevance selector contract, injectable for tests. */
29
+ export interface MemorySelector {
30
+ /**
31
+ * Pick at most {@link MAX_RECALL_MEMORIES} filenames relevant to a query.
32
+ * @param query - the user turn text.
33
+ * @param candidates - topic files (filename + description) not yet shown.
34
+ * @param signal - cancellation for the underlying model request.
35
+ * @param recentTools - tool names used earlier this session; the selector
36
+ * should not surface usage-reference/API-doc memories for these tools.
37
+ * @returns selected filenames.
38
+ */
39
+ select(query: string, candidates: readonly RecallCandidate[], signal: AbortSignal, recentTools: readonly string[]): Promise<string[]>;
40
+ }
41
+ /**
42
+ * Default selector backed by `ctx.subagents`: asks a forked small-model
43
+ * one-shot subagent to return the filenames most useful for the query. The
44
+ * parent agent is supplied per query so the child seeds from the right turn.
45
+ */
46
+ export declare class SubagentMemorySelector implements MemorySelector {
47
+ private readonly ctx;
48
+ private readonly parent;
49
+ private readonly providerName;
50
+ private readonly agentOptions?;
51
+ /**
52
+ * Create a subagent-backed selector for one parent agent.
53
+ * @param ctx - host context with the optional `subagents` service.
54
+ * @param parent - the agent whose turn triggers recall.
55
+ * @param providerName - the one-shot provider to fork (default `fork`).
56
+ * @param agentOptions - optional model selection passed to the child.
57
+ */
58
+ constructor(ctx: Context, parent: Agent, providerName?: string, agentOptions?: unknown | undefined);
59
+ select(query: string, candidates: readonly RecallCandidate[], signal: AbortSignal, recentTools: readonly string[]): Promise<string[]>;
60
+ }
61
+ /** Loose JSON extraction tolerant of code fences and prose around the array. */
62
+ export declare function extractSelectedNames(text: string): string[];
63
+ /** The per-agent recall coordinator. Holds the shown-path set per agent. */
64
+ export declare class MemoryRecall {
65
+ private readonly ctx;
66
+ private readonly home;
67
+ private readonly state;
68
+ private readonly providerName;
69
+ private readonly createSelector;
70
+ private readonly recentTools;
71
+ private readonly disposers;
72
+ /**
73
+ * Register the `agent/pre-step` and `tools/post-execute` listeners.
74
+ * @param ctx - host context with `fs`, `subagents`, and the agent channel.
75
+ * @param home - the memory home: the global directory and the root under
76
+ * which each agent's workspace directory (`projects/<slug>`) is resolved.
77
+ * @param options - provider name, whether recall is enabled, and an optional
78
+ * selector factory (defaults to {@link SubagentMemorySelector}; inject a
79
+ * fake for deterministic tests).
80
+ */
81
+ constructor(ctx: Context, home: string, options?: {
82
+ providerName?: string;
83
+ enabled?: boolean;
84
+ createSelector?: (ctx: Context, agent: Agent) => MemorySelector;
85
+ });
86
+ /** Remove both the pre-step and post-execute listeners. */
87
+ dispose(): void;
88
+ private onPreStep;
89
+ private maybeRecall;
90
+ }
91
+ //# sourceMappingURL=recall.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recall.d.ts","sourceRoot":"","sources":["../src/recall.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,KAAK,EAAmB,MAAM,wBAAwB,CAAA;AAMpE,OAAO,QAAQ,sBAAsB,CAAC;IACpC,UAAU,gBAAgB;QACxB,MAAM,EAAE;YAAE,IAAI,EAAE,QAAQ,CAAA;SAAE,CAAA;KAC3B;CACF;AAMD,+DAA+D;AAC/D,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC,8CAA8C;AAC9C,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,mEAAmE;AACnE,MAAM,WAAW,cAAc;IAC7B;;;;;;;;OAQG;IACH,MAAM,CACJ,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,SAAS,eAAe,EAAE,EACtC,MAAM,EAAE,WAAW,EACnB,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;CACrB;AAED;;;;GAIG;AACH,qBAAa,sBAAuB,YAAW,cAAc;IASzD,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;IAXhC;;;;;;OAMG;gBAEgB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,KAAK,EACb,YAAY,SAAS,EACrB,YAAY,CAAC,EAAE,OAAO,YAAA;IAGnC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,eAAe,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAmD5I;AAeD,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAY3D;AAED,4EAA4E;AAC5E,qBAAa,YAAY;IAiBrB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAjBvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkE;IACxF,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgD;IAC/E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoB;IAChD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAElD;;;;;;;;OAQG;gBAEgB,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,MAAM,EAC7B,OAAO,GAAE;QACP,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,KAAK,cAAc,CAAA;KAC3D;IAsBR,2DAA2D;IAC3D,OAAO,IAAI,IAAI;YAKD,SAAS;YA4BT,WAAW;CA+D1B"}
package/lib/recall.js ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Dynamic recall: an `agent/pre-step` listener that asks a small-model side
3
+ * query (a forked subagent) which topic files are relevant to the turn, then
4
+ * injects their bodies through `agent.inject()`. Topic files already shown
5
+ * this session are never re-injected. Recall runs for top-level agents only —
6
+ * a forked child (including the selector itself) never recalls, so no chain of
7
+ * memory-recall subagents can form. Best-effort: absence of the subagent
8
+ * service or provider skips recall without error.
9
+ * @module @dsh-cc/memory/recall
10
+ */
11
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
12
+ import { scanMemoryDirectory } from "./scan.js";
13
+ import { cwdOf, resolveWorkspaceMemoryDir } from "./paths.js";
14
+ // The canonical `tools/post-execute` waterfall signature comes from
15
+ // @dsh-cc/tools (a real dependency since save.ts); memory only observes
16
+ // tool usage for recall suppression and delegates to `next()` unchanged.
17
+ /** How many topic files the selector may surface per query. */
18
+ export const MAX_RECALL_MEMORIES = 5;
19
+ /**
20
+ * Default selector backed by `ctx.subagents`: asks a forked small-model
21
+ * one-shot subagent to return the filenames most useful for the query. The
22
+ * parent agent is supplied per query so the child seeds from the right turn.
23
+ */
24
+ export class SubagentMemorySelector {
25
+ ctx;
26
+ parent;
27
+ providerName;
28
+ agentOptions;
29
+ /**
30
+ * Create a subagent-backed selector for one parent agent.
31
+ * @param ctx - host context with the optional `subagents` service.
32
+ * @param parent - the agent whose turn triggers recall.
33
+ * @param providerName - the one-shot provider to fork (default `fork`).
34
+ * @param agentOptions - optional model selection passed to the child.
35
+ */
36
+ constructor(ctx, parent, providerName = 'fork', agentOptions) {
37
+ this.ctx = ctx;
38
+ this.parent = parent;
39
+ this.providerName = providerName;
40
+ this.agentOptions = agentOptions;
41
+ }
42
+ async select(query, candidates, signal, recentTools) {
43
+ const subagents = this.ctx.get('subagents');
44
+ if (subagents === undefined)
45
+ return [];
46
+ const manifest = candidates
47
+ .map(candidate => `- ${candidate.filename}: ${candidate.description}`)
48
+ .join('\n');
49
+ const system = [
50
+ 'You select memories useful for processing a user query.',
51
+ `Return a JSON object with a "selected_memories" array of filenames (at most ${MAX_RECALL_MEMORIES}).`,
52
+ 'Only include memories you are certain are helpful. If none are clearly useful, return an empty array.',
53
+ 'If a list of recently-used tools is provided, do not select memories that are usage reference or API documentation for those tools (the agent is already exercising them). DO still select memories containing warnings, gotchas, or known issues about those tools — active use is exactly when those matter.',
54
+ ].join('\n');
55
+ // When a tool is actively in use, its reference-doc memory is noise — the
56
+ // conversation already contains working usage and keyword overlap would
57
+ // otherwise false-positive the selector. Surface the list so it can suppress.
58
+ const toolsSection = recentTools.length > 0
59
+ ? `\n\nRecently used tools: ${recentTools.join(', ')}`
60
+ : '';
61
+ let run;
62
+ try {
63
+ run = await subagents.start(this.providerName, {
64
+ label: 'memory-recall',
65
+ signal,
66
+ prompt: [{ type: 'text', text: `${system}\n\nQuery: ${query}\n\nAvailable memories:\n${manifest}${toolsSection}` }],
67
+ parent: this.parent,
68
+ ...(this.agentOptions !== undefined ? { agentOptions: this.agentOptions } : {}),
69
+ });
70
+ }
71
+ catch {
72
+ // Provider absent or services unavailable: best-effort recall skips.
73
+ return [];
74
+ }
75
+ // run.result rejects only on infrastructure faults; child-level failures
76
+ // arrive RESOLVED with a non-completed stopReason. Both must skip recall
77
+ // quietly — this path is fire-and-forget, so a throw becomes an unhandled
78
+ // rejection in the host.
79
+ let result;
80
+ try {
81
+ result = await run.result;
82
+ }
83
+ catch {
84
+ return [];
85
+ }
86
+ if (result.stopReason === 'error')
87
+ return [];
88
+ // SubagentResult carries the transcript blocks as `output` (not `content`).
89
+ const text = (result.output ?? [])
90
+ .filter(block => block.type === 'text')
91
+ .map(block => block.text ?? '')
92
+ .join('');
93
+ const names = extractSelectedNames(text);
94
+ const valid = new Set(candidates.map(candidate => candidate.filename));
95
+ return names.filter(name => valid.has(name)).slice(0, MAX_RECALL_MEMORIES);
96
+ }
97
+ }
98
+ /** Loose JSON extraction tolerant of code fences and prose around the array. */
99
+ export function extractSelectedNames(text) {
100
+ const match = /"selected_memories"\s*:\s*(\[[^\]]*\])/.exec(text);
101
+ const encoded = match?.[1];
102
+ if (encoded === undefined)
103
+ return [];
104
+ try {
105
+ const parsed = JSON.parse(encoded);
106
+ return Array.isArray(parsed)
107
+ ? parsed.filter((value) => typeof value === 'string')
108
+ : [];
109
+ }
110
+ catch {
111
+ return [];
112
+ }
113
+ }
114
+ /** The per-agent recall coordinator. Holds the shown-path set per agent. */
115
+ export class MemoryRecall {
116
+ ctx;
117
+ home;
118
+ state = new WeakMap();
119
+ providerName;
120
+ createSelector;
121
+ recentTools = new Set();
122
+ disposers = [];
123
+ /**
124
+ * Register the `agent/pre-step` and `tools/post-execute` listeners.
125
+ * @param ctx - host context with `fs`, `subagents`, and the agent channel.
126
+ * @param home - the memory home: the global directory and the root under
127
+ * which each agent's workspace directory (`projects/<slug>`) is resolved.
128
+ * @param options - provider name, whether recall is enabled, and an optional
129
+ * selector factory (defaults to {@link SubagentMemorySelector}; inject a
130
+ * fake for deterministic tests).
131
+ */
132
+ constructor(ctx, home, options = {}) {
133
+ this.ctx = ctx;
134
+ this.home = home;
135
+ this.providerName = options.providerName ?? 'fork';
136
+ this.createSelector = options.createSelector
137
+ ?? ((ctx, agent) => new SubagentMemorySelector(ctx, agent, this.providerName));
138
+ if (options.enabled ?? true) {
139
+ this.disposers.push(this.ctx.on('agent/pre-step', (payload, next) => this.onPreStep(payload, next)));
140
+ }
141
+ // Track tools used this session so recall can suppress reference-doc
142
+ // memories for the tools the agent is already exercising. This is a
143
+ // waterfall observer: it must delegate to `next()` so the tools pipeline
144
+ // continues unchanged.
145
+ this.disposers.push(this.ctx.on('tools/post-execute', (exec, _result, next) => {
146
+ if (exec.name.length > 0)
147
+ this.recentTools.add(exec.name);
148
+ return next();
149
+ }));
150
+ }
151
+ /** Remove both the pre-step and post-execute listeners. */
152
+ dispose() {
153
+ for (const dispose of this.disposers.splice(0))
154
+ dispose();
155
+ this.recentTools.clear();
156
+ }
157
+ async onPreStep({ agent, messages, signal, }, next) {
158
+ const decision = await next();
159
+ if (decision.kind === 'reject')
160
+ return decision;
161
+ signal.throwIfAborted();
162
+ // Recall enriches top-level agents only. Every agent runs this same
163
+ // waterfall — including the forked memory-recall selector itself — so
164
+ // recalling inside a subagent would spawn another selector whose own
165
+ // pre-step recalls again: an unbounded chain of memory-recall subagents.
166
+ const header = agent.session.header;
167
+ if (header.origin === 'subagent' || (header.delegationDepth ?? 0) > 0)
168
+ return decision;
169
+ // Fire-and-forget: recall is model-visible enrichment, never worth an
170
+ // unhandled rejection in the host — any fault inside skips quietly.
171
+ void this.maybeRecall(agent, messages, signal, this.createSelector(this.ctx, agent))
172
+ .catch(() => { });
173
+ return decision;
174
+ }
175
+ async maybeRecall(agent, messages, signal, selector) {
176
+ const fileSystem = this.ctx.get('fs');
177
+ if (fileSystem === undefined)
178
+ return;
179
+ const query = messages
180
+ .map(message => message.content.filter(block => block.type === 'text').map(block => block.text ?? '').join(' '))
181
+ .join('\n')
182
+ .trim();
183
+ if (query.length === 0)
184
+ return;
185
+ // Recall spans both layers: the agent's workspace directory and the
186
+ // global directory. Shown-tracking keys on full paths, so identical
187
+ // filenames across layers never collide.
188
+ const workspaceDir = resolveWorkspaceMemoryDir(this.home, cwdOf(agent));
189
+ const [workspaceScan, globalScan] = await Promise.all([
190
+ scanMemoryDirectory(fileSystem, workspaceDir, signal),
191
+ scanMemoryDirectory(fileSystem, this.home, signal),
192
+ ]);
193
+ const topics = [...workspaceScan.topics, ...globalScan.topics];
194
+ if (topics.length === 0)
195
+ return;
196
+ let entry = this.state.get(agent);
197
+ if (entry === undefined) {
198
+ entry = { shown: new Set(), inFlight: false };
199
+ this.state.set(agent, entry);
200
+ }
201
+ const fresh = topics.filter(topic => !entry.shown.has(topic.path));
202
+ if (fresh.length === 0)
203
+ return;
204
+ // Pre-step fires once per step while a turn runs; a pending selection must
205
+ // not pile up overlapping selectors for the same agent.
206
+ if (entry.inFlight)
207
+ return;
208
+ entry.inFlight = true;
209
+ try {
210
+ const selected = await selector.select(query, fresh.map(topic => ({ path: topic.path, filename: topic.filename, description: topic.frontmatter.description })), signal, Array.from(this.recentTools));
211
+ if (signal.aborted || selected.length === 0)
212
+ return;
213
+ const byFilename = new Map(topics.map(topic => [topic.filename, topic]));
214
+ const bodies = [];
215
+ for (const filename of selected) {
216
+ const topic = byFilename.get(filename);
217
+ if (topic === undefined)
218
+ continue;
219
+ entry.shown.add(topic.path);
220
+ const raw = await readOptionalText(fileSystem, topic.path, signal);
221
+ if (raw !== undefined && raw.trim().length > 0) {
222
+ bodies.push(`## Memory: ${topic.frontmatter.name}\n\n${raw.trim()}`);
223
+ }
224
+ }
225
+ signal.throwIfAborted();
226
+ if (bodies.length === 0)
227
+ return;
228
+ agent.inject(createUserMessage({
229
+ content: [{ type: 'text', text: bodies.join('\n\n') }],
230
+ source: { kind: 'memory' },
231
+ }));
232
+ }
233
+ finally {
234
+ entry.inFlight = false;
235
+ }
236
+ }
237
+ }
238
+ async function readOptionalText(fs, path, signal) {
239
+ try {
240
+ const target = signal !== undefined
241
+ ? await fs.resolve(path, { signal })
242
+ : await fs.resolve(path);
243
+ return await fs.readText(target, signal);
244
+ }
245
+ catch (error) {
246
+ if (typeof error === 'object' && error !== null && 'code' in error
247
+ && (error.code === 'FS_NOT_FOUND'
248
+ || error.code === 'ENOENT'))
249
+ return undefined;
250
+ throw error;
251
+ }
252
+ }
253
+ //# sourceMappingURL=recall.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recall.js","sourceRoot":"","sources":["../src/recall.ts"],"names":[],"mappings":"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"}
package/lib/save.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The `memory_save` tool: the model-facing save channel for the memory
3
+ * directory.
4
+ *
5
+ * Direct `write`/`edit` calls aimed at the memory directory always fail — it
6
+ * lives in the harness home, outside every session workspace, so the fs
7
+ * sandbox fences them. This tool is the working alternative: the model passes
8
+ * structured fields, the plugin generates the frontmatter, maintains the
9
+ * MEMORY.md pointer, and writes host-side under a per-call policy confined to
10
+ * the memory directory (see `writeback.ts`).
11
+ * @module @dsh-cc/memory/save
12
+ */
13
+ import type { Context } from '@deepseek-ai/cordis';
14
+ import type { MemorySection } from './section.ts';
15
+ /** The registered tool name. */
16
+ export declare const MEMORY_SAVE_TOOL = "memory_save";
17
+ /** Where a saved memory lands. */
18
+ export declare const MEMORY_SAVE_SCOPES: readonly ["workspace", "global"];
19
+ /** A model-visible save failure (maps to an isError tool result). */
20
+ export declare class MemorySaveError extends Error {
21
+ }
22
+ export interface MemorySaveArgs {
23
+ /** Kebab-case topic slug; the topic file is `<name>.md`. */
24
+ name: string;
25
+ /** One of the four memory types. */
26
+ type: string;
27
+ /** One-line relevance description for the MEMORY.md pointer. */
28
+ description: string;
29
+ /** Markdown body (frontmatter is generated host-side). */
30
+ body: string;
31
+ /** `workspace` (default) saves to this workspace's directory; `global` saves to the cross-workspace directory. */
32
+ scope?: string;
33
+ }
34
+ /** Assemble the topic file body with rationalized frontmatter. */
35
+ export declare function renderTopicFile(args: MemorySaveArgs): string;
36
+ /** The MEMORY.md pointer line for one topic (mirrors the section's index format). */
37
+ export declare function pointerLine(args: MemorySaveArgs): string;
38
+ /**
39
+ * Upsert the topic's pointer in the MEMORY.md body: replace the line whose
40
+ * link target is `<name>.md`, append otherwise.
41
+ */
42
+ export declare function upsertPointer(entrypoint: string, args: MemorySaveArgs): string;
43
+ /**
44
+ * Register the `memory_save` tool. No-op when the host has no tools service
45
+ * or no fs seam (a providerless host keeps memory read-only).
46
+ * @param ctx - the host context.
47
+ * @param home - the memory home: the global directory and the root under
48
+ * which each workspace's private directory lives (`projects/<slug>`).
49
+ * @param section - the memory section to refresh after a successful save.
50
+ * @returns the registration disposer, or `undefined` when not registered.
51
+ */
52
+ export declare function registerMemorySaveTool(ctx: Context, home: string, section: MemorySection): (() => void) | undefined;
53
+ //# sourceMappingURL=save.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../src/save.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAQlD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAEjD,gCAAgC;AAChC,eAAO,MAAM,gBAAgB,gBAAgB,CAAA;AAO7C,kCAAkC;AAClC,eAAO,MAAM,kBAAkB,kCAAmC,CAAA;AAElE,qEAAqE;AACrE,qBAAa,eAAgB,SAAQ,KAAK;CAAG;AAE7C,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAA;IACZ,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAA;IACnB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAA;IACZ,kHAAkH;IAClH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,kEAAkE;AAClE,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAW5D;AAED,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAExD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,MAAM,CAW9E;AA4BD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,aAAa,GACrB,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAmF1B"}
package/lib/save.js ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * The `memory_save` tool: the model-facing save channel for the memory
3
+ * directory.
4
+ *
5
+ * Direct `write`/`edit` calls aimed at the memory directory always fail — it
6
+ * lives in the harness home, outside every session workspace, so the fs
7
+ * sandbox fences them. This tool is the working alternative: the model passes
8
+ * structured fields, the plugin generates the frontmatter, maintains the
9
+ * MEMORY.md pointer, and writes host-side under a per-call policy confined to
10
+ * the memory directory (see `writeback.ts`).
11
+ * @module @dsh-cc/memory/save
12
+ */
13
+ import { join } from 'node:path';
14
+ import { defineTool } from '@dsh-cc/tools';
15
+ import { MEMORY_TYPES } from "./types.js";
16
+ import { ENTRYPOINT_NAME } from "./truncate.js";
17
+ import { validateMemoryWrites, writeMemoryFiles } from "./writeback.js";
18
+ import { cwdOf, resolveWorkspaceMemoryDir } from "./paths.js";
19
+ /** The registered tool name. */
20
+ export const MEMORY_SAVE_TOOL = 'memory_save';
21
+ /** Topic slugs are kebab-case; the file is `<name>.md`. */
22
+ const NAME_RULE = /^[a-z0-9][a-z0-9-]*$/;
23
+ /** One-line relevance description cap. */
24
+ const MAX_DESCRIPTION_CHARS = 200;
25
+ /** Where a saved memory lands. */
26
+ export const MEMORY_SAVE_SCOPES = ['workspace', 'global'];
27
+ /** A model-visible save failure (maps to an isError tool result). */
28
+ export class MemorySaveError extends Error {
29
+ }
30
+ /** Assemble the topic file body with rationalized frontmatter. */
31
+ export function renderTopicFile(args) {
32
+ return [
33
+ '---',
34
+ `name: ${args.name}`,
35
+ `description: ${args.description}`,
36
+ `type: ${args.type}`,
37
+ '---',
38
+ '',
39
+ args.body.trimEnd(),
40
+ '',
41
+ ].join('\n');
42
+ }
43
+ /** The MEMORY.md pointer line for one topic (mirrors the section's index format). */
44
+ export function pointerLine(args) {
45
+ return `- [${args.name}](${args.name}.md) — ${args.description}`;
46
+ }
47
+ /**
48
+ * Upsert the topic's pointer in the MEMORY.md body: replace the line whose
49
+ * link target is `<name>.md`, append otherwise.
50
+ */
51
+ export function upsertPointer(entrypoint, args) {
52
+ const line = pointerLine(args);
53
+ const target = `](${args.name}.md)`;
54
+ const lines = entrypoint.split('\n');
55
+ const at = lines.findIndex(l => l.includes(target));
56
+ if (at >= 0) {
57
+ lines[at] = line;
58
+ return lines.join('\n');
59
+ }
60
+ const trimmed = entrypoint.trimEnd();
61
+ return trimmed.length > 0 ? `${trimmed}\n${line}\n` : `${line}\n`;
62
+ }
63
+ function validateArgs(args) {
64
+ if (!NAME_RULE.test(args.name)) {
65
+ throw new MemorySaveError(`invalid memory name "${args.name}": use a kebab-case slug (lowercase letters, digits, dashes)`);
66
+ }
67
+ if (args.name.toLowerCase() === 'memory') {
68
+ throw new MemorySaveError('the name "memory" is reserved for the MEMORY.md index');
69
+ }
70
+ if (!MEMORY_TYPES.includes(args.type)) {
71
+ throw new MemorySaveError(`invalid memory type "${args.type}": use one of ${MEMORY_TYPES.join(', ')}`);
72
+ }
73
+ if (args.description.trim().length === 0 || args.description.includes('\n')) {
74
+ throw new MemorySaveError('description must be a single non-empty line');
75
+ }
76
+ if (args.description.length > MAX_DESCRIPTION_CHARS) {
77
+ throw new MemorySaveError(`description is ${args.description.length} chars, over the ${MAX_DESCRIPTION_CHARS} cap`);
78
+ }
79
+ if (args.body.trim().length === 0) {
80
+ throw new MemorySaveError('body must not be empty');
81
+ }
82
+ if (args.scope !== undefined && !MEMORY_SAVE_SCOPES.includes(args.scope)) {
83
+ throw new MemorySaveError(`invalid memory scope "${args.scope}": use one of ${MEMORY_SAVE_SCOPES.join(', ')}`);
84
+ }
85
+ }
86
+ /**
87
+ * Register the `memory_save` tool. No-op when the host has no tools service
88
+ * or no fs seam (a providerless host keeps memory read-only).
89
+ * @param ctx - the host context.
90
+ * @param home - the memory home: the global directory and the root under
91
+ * which each workspace's private directory lives (`projects/<slug>`).
92
+ * @param section - the memory section to refresh after a successful save.
93
+ * @returns the registration disposer, or `undefined` when not registered.
94
+ */
95
+ export function registerMemorySaveTool(ctx, home, section) {
96
+ const tools = ctx.get('tools');
97
+ if (tools === undefined)
98
+ return undefined;
99
+ return tools.register(defineTool({
100
+ name: MEMORY_SAVE_TOOL,
101
+ description: 'Save a durable memory (a fact or preference useful in FUTURE conversations) to the persistent memory '
102
+ + 'system. By default the memory is private to the current workspace; pass `scope: "global"` for facts '
103
+ + 'useful across all workspaces. Writes the topic file and updates the MEMORY.md index for you. This is '
104
+ + 'the ONLY way to save memories: direct write/edit calls to a memory directory are fenced by the '
105
+ + 'sandbox and always fail. Do not use it for ephemeral task detail.',
106
+ parameters: {
107
+ name: {
108
+ type: 'string',
109
+ required: true,
110
+ description: 'Kebab-case topic slug, e.g. "user-profile". The topic file is `<name>.md`; saving an existing name overwrites it.',
111
+ },
112
+ type: {
113
+ type: 'string',
114
+ enum: MEMORY_TYPES,
115
+ required: true,
116
+ description: 'user: who the user is; feedback: how the user wants you to work; project: ongoing work state; reference: pointers to external resources.',
117
+ },
118
+ description: {
119
+ type: 'string',
120
+ required: true,
121
+ description: 'One-line relevance note shown in the MEMORY.md index (max 200 chars).',
122
+ },
123
+ body: {
124
+ type: 'string',
125
+ required: true,
126
+ description: 'Markdown body of the memory (without frontmatter — it is generated for you).',
127
+ },
128
+ scope: {
129
+ type: 'string',
130
+ enum: MEMORY_SAVE_SCOPES,
131
+ description: 'workspace (default): visible only to sessions in the current workspace. global: visible to every workspace.',
132
+ },
133
+ },
134
+ output: {
135
+ schema: {
136
+ type: 'object',
137
+ additionalProperties: false,
138
+ properties: {
139
+ path: { type: 'string', required: true },
140
+ message: { type: 'string', required: true },
141
+ },
142
+ },
143
+ render: (_args, value) => [{ type: 'text', text: value.message }],
144
+ },
145
+ isConcurrencySafe: () => false,
146
+ async execute(args, exec) {
147
+ const fs = ctx.get('fs');
148
+ if (fs === undefined)
149
+ throw new MemorySaveError('memory save unavailable: no fs seam');
150
+ validateArgs(args);
151
+ const dir = args.scope === 'global'
152
+ ? home
153
+ : resolveWorkspaceMemoryDir(home, exec.agent !== undefined ? cwdOf(exec.agent) : process.cwd());
154
+ const filename = `${args.name}.md`;
155
+ const writes = [
156
+ { path: filename, content: renderTopicFile(args) },
157
+ ];
158
+ // Reuse the write-back validator so tool saves and fork reports share
159
+ // one security boundary (filename rule + size caps).
160
+ const validated = validateMemoryWrites({ writes });
161
+ // Upsert the index pointer first from the CURRENT entrypoint body, then
162
+ // write both files under the policy confined to the memory directory.
163
+ let entrypoint = '';
164
+ try {
165
+ entrypoint = await fs.readText(await fs.resolve(join(dir, ENTRYPOINT_NAME)));
166
+ }
167
+ catch {
168
+ // No index yet — the upsert starts from an empty body.
169
+ }
170
+ validated.push({ path: ENTRYPOINT_NAME, content: upsertPointer(entrypoint, args) });
171
+ await writeMemoryFiles(fs, dir, validated);
172
+ await section.refresh(exec.agent);
173
+ return {
174
+ path: join(dir, filename),
175
+ message: `Saved memory "${args.name}" (${args.type}) to ${filename} and updated ${ENTRYPOINT_NAME}.`,
176
+ };
177
+ },
178
+ }));
179
+ }
180
+ //# sourceMappingURL=save.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save.js","sourceRoot":"","sources":["../src/save.ts"],"names":[],"mappings":"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"}
package/lib/scan.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Directory scan over a memdir through the optional `ctx.fs` seam: list topic
3
+ * files, parse their frontmatter, and read the always-loaded entrypoint. Used
4
+ * by the system-prompt section index and by recall.
5
+ * @module @dsh-cc/memory/scan
6
+ */
7
+ import type { FileSystem } from '@deepseek-ai/dsh-fs';
8
+ import type { MemoryIndexEntry } from './types.ts';
9
+ /** One observed memdir state. */
10
+ export interface MemoryDirectoryState {
11
+ /** The memory directory path. */
12
+ dir: string;
13
+ /** Raw (pre-truncation) entrypoint text, or `undefined` when absent. */
14
+ entrypoint: string | undefined;
15
+ /** Topic files with rationalized headers, excluding MEMORY.md itself. */
16
+ topics: MemoryIndexEntry[];
17
+ }
18
+ /**
19
+ * Scan a memory directory for its entrypoint and topic files.
20
+ * A missing directory or entrypoint is not an error — the caller renders an
21
+ * empty section. Files are read with the caller's cancellation signal; the
22
+ * result contains only topics whose frontmatter parsed successfully.
23
+ * @param fs - the contiguous filesystem seam.
24
+ * @param dir - the memory directory to scan.
25
+ * @param signal - optional cancellation for the underlying fs reads.
26
+ * @returns the observed directory state, never throwing for absent files.
27
+ */
28
+ export declare function scanMemoryDirectory(fs: FileSystem, dir: string, signal?: AbortSignal): Promise<MemoryDirectoryState>;
29
+ //# sourceMappingURL=scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAqB,MAAM,qBAAqB,CAAA;AAGxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAElD,iCAAiC;AACjC,MAAM,WAAW,oBAAoB;IACnC,iCAAiC;IACjC,GAAG,EAAE,MAAM,CAAA;IACX,wEAAwE;IACxE,UAAU,EAAE,MAAM,GAAG,SAAS,CAAA;IAC9B,yEAAyE;IACzE,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAC3B;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC,CA4B/B"}