@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.
- package/LICENSE +201 -0
- package/README.i18n.yaml +6 -0
- package/README.md +158 -0
- package/README.zh.md +125 -0
- package/lib/index.d.ts +75 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +91 -0
- package/lib/index.js.map +1 -0
- package/lib/invariant.d.ts +16 -0
- package/lib/invariant.d.ts.map +1 -0
- package/lib/invariant.js +22 -0
- package/lib/invariant.js.map +1 -0
- package/lib/parser.d.ts +30 -0
- package/lib/parser.d.ts.map +1 -0
- package/lib/parser.js +96 -0
- package/lib/parser.js.map +1 -0
- package/lib/paths.d.ts +98 -0
- package/lib/paths.d.ts.map +1 -0
- package/lib/paths.js +236 -0
- package/lib/paths.js.map +1 -0
- package/lib/recall.d.ts +91 -0
- package/lib/recall.d.ts.map +1 -0
- package/lib/recall.js +253 -0
- package/lib/recall.js.map +1 -0
- package/lib/save.d.ts +53 -0
- package/lib/save.d.ts.map +1 -0
- package/lib/save.js +180 -0
- package/lib/save.js.map +1 -0
- package/lib/scan.d.ts +29 -0
- package/lib/scan.d.ts.map +1 -0
- package/lib/scan.js +70 -0
- package/lib/scan.js.map +1 -0
- package/lib/section.d.ts +129 -0
- package/lib/section.d.ts.map +1 -0
- package/lib/section.js +353 -0
- package/lib/section.js.map +1 -0
- package/lib/team.d.ts +90 -0
- package/lib/team.d.ts.map +1 -0
- package/lib/team.js +167 -0
- package/lib/team.js.map +1 -0
- package/lib/truncate.d.ts +35 -0
- package/lib/truncate.d.ts.map +1 -0
- package/lib/truncate.js +52 -0
- package/lib/truncate.js.map +1 -0
- package/lib/types.d.ts +34 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +18 -0
- package/lib/types.js.map +1 -0
- package/lib/writeback.d.ts +85 -0
- package/lib/writeback.d.ts.map +1 -0
- package/lib/writeback.js +121 -0
- package/lib/writeback.js.map +1 -0
- package/package.json +65 -0
package/lib/team.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team memory: an opt-in shared per-project memory directory layered on the
|
|
3
|
+
* private memdir. Provides the team path resolution, the dual-directory
|
|
4
|
+
* (private + team) merged prompt, and a seam-native read/write path validation
|
|
5
|
+
* chain so a relative key cannot escape the team directory.
|
|
6
|
+
*
|
|
7
|
+
* Security model (three hard prerequisites, all enforced before any read or
|
|
8
|
+
* write):
|
|
9
|
+
* 1. **Key sanitization** — a pure string pass (`sanitizePathKey`) run before
|
|
10
|
+
* any filesystem operation rejects null bytes, URL-encoded traversal, NFKC
|
|
11
|
+
* normalization differences, backslashes, and absolute keys.
|
|
12
|
+
* 2. **Seam-native validation chain** — for the sanitized key: `fs.lstat`
|
|
13
|
+
* rejects a final-segment symlink, then `fs.resolve` + `fs.contains`
|
|
14
|
+
* enforce prefix containment against the resolved team root.
|
|
15
|
+
* 3. **Containment on every access** — reads and writes go through the chain,
|
|
16
|
+
* so a symlinked intermediate component that points outside is caught by
|
|
17
|
+
* the resolve + contains step.
|
|
18
|
+
*
|
|
19
|
+
* Residual gap (documented): this closes per-access traversal, but a
|
|
20
|
+
* mutate-between-check TOCTOU window on *intermediate* components is not fully
|
|
21
|
+
* closed — only the final segment is lstat-checked, and the resolve/contains
|
|
22
|
+
* check and the read are not atomic. Do not enable `teamEnabled` in multi-tenant
|
|
23
|
+
* or untrusted-writer deployments.
|
|
24
|
+
* @module @dsh-cc/memory/team
|
|
25
|
+
*/
|
|
26
|
+
import type { FileSystem, FsTarget } from '@deepseek-ai/dsh-fs';
|
|
27
|
+
/** Error thrown when a team-memory key/path fails security validation. */
|
|
28
|
+
export declare class TeamMemoryError extends Error {
|
|
29
|
+
constructor(message: string);
|
|
30
|
+
}
|
|
31
|
+
/** Default subdirectory of the memory home holding team memory. */
|
|
32
|
+
export declare const TEAM_MEMORY_DIR = "team";
|
|
33
|
+
/**
|
|
34
|
+
* The team memory entrypoint filename, matching the private entrypoint.
|
|
35
|
+
* Each directory (private and team) keeps its own MEMORY.md index.
|
|
36
|
+
*/
|
|
37
|
+
export declare const TEAM_ENTRYPOINT_NAME = "MEMORY.md";
|
|
38
|
+
/**
|
|
39
|
+
* Sanitize a relative path key by rejecting dangerous patterns, mirroring the
|
|
40
|
+
* Claude Code `sanitizePathKey` semantics. Pure string — run this before any
|
|
41
|
+
* filesystem operation. Returns the sanitized key unchanged or throws
|
|
42
|
+
* {@link TeamMemoryError}. Rejects:
|
|
43
|
+
* - null bytes (can truncate paths in C-based syscalls)
|
|
44
|
+
* - URL-encoded traversal (`%2e%2e%2f` → `../`)
|
|
45
|
+
* - NFKC normalization that collapses to `..` / separators (fullwidth `../`)
|
|
46
|
+
* - backslashes (Windows separator used as a traversal vector)
|
|
47
|
+
* - absolute paths
|
|
48
|
+
* @param key - the relative path key under the team directory.
|
|
49
|
+
* @returns the sanitized key (unchanged).
|
|
50
|
+
*/
|
|
51
|
+
export declare function sanitizePathKey(key: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the team memory directory for one workspace. Team memory is shared
|
|
54
|
+
* by all users of THE PROJECT, so it lives inside the workspace's private
|
|
55
|
+
* memory directory: `<workspaceDir>/team`. (Before per-workspace isolation it
|
|
56
|
+
* sat at the global `<memoryHome>/team`; that directory is now inert.)
|
|
57
|
+
* @param workspaceDir - the workspace's private memory directory.
|
|
58
|
+
* @returns the team directory path (`<workspaceDir>/team`).
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveTeamMemoryRoot(workspaceDir: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* The seam-native team path validation chain. For a relative key under a team
|
|
63
|
+
* directory, enforce, in order:
|
|
64
|
+
* 1. `sanitizePathKey` — pure-string rejection (no fs access).
|
|
65
|
+
* 2. `fs.lstat` — reject a final-segment symlink before following it.
|
|
66
|
+
* 3. `fs.resolve` of both the team root and the candidate, then `fs.contains`
|
|
67
|
+
* — reject when the resolved candidate escapes the resolved team root.
|
|
68
|
+
*
|
|
69
|
+
* Throws {@link TeamMemoryError} when any step fails. A missing file is not an
|
|
70
|
+
* error here: the resolve+contains step still spans the nearest existing
|
|
71
|
+
* ancestor (realpath), so a missing child under an escaping symlinked ancestor
|
|
72
|
+
* is still caught.
|
|
73
|
+
* @param fs - the contiguous filesystem seam.
|
|
74
|
+
* @param teamDir - the resolved team directory path.
|
|
75
|
+
* @param relativeKey - the sanitizable relative key to validate.
|
|
76
|
+
* @returns the resolved, contained {@link FsTarget} for the candidate.
|
|
77
|
+
*/
|
|
78
|
+
export declare function validateTeamMemKey(fs: FileSystem, teamDir: string, relativeKey: string): Promise<FsTarget>;
|
|
79
|
+
/**
|
|
80
|
+
* Validate and read a team memory file body, or `undefined` when the file is
|
|
81
|
+
* absent. The read only proceeds when {@link validateTeamMemKey} passes, so a
|
|
82
|
+
* traversal or symlink-escape key never reaches the read.
|
|
83
|
+
* @param fs - the contiguous filesystem seam.
|
|
84
|
+
* @param teamDir - the resolved team directory path.
|
|
85
|
+
* @param relativeKey - the sanitizable relative key to read.
|
|
86
|
+
* @param signal - optional cancellation.
|
|
87
|
+
* @returns the file body, or `undefined` if absent.
|
|
88
|
+
*/
|
|
89
|
+
export declare function readTeamMemFile(fs: FileSystem, teamDir: string, relativeKey: string, signal?: AbortSignal): Promise<string | undefined>;
|
|
90
|
+
//# sourceMappingURL=team.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"team.d.ts","sourceRoot":"","sources":["../src/team.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAE/D,0EAA0E;AAC1E,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED,mEAAmE;AACnE,eAAO,MAAM,eAAe,SAAS,CAAA;AAErC;;;GAGG;AACH,eAAO,MAAM,oBAAoB,cAAc,CAAA;AAE/C;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA0CnD;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,QAAQ,CAAC,CAiBnB;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B"}
|
package/lib/team.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team memory: an opt-in shared per-project memory directory layered on the
|
|
3
|
+
* private memdir. Provides the team path resolution, the dual-directory
|
|
4
|
+
* (private + team) merged prompt, and a seam-native read/write path validation
|
|
5
|
+
* chain so a relative key cannot escape the team directory.
|
|
6
|
+
*
|
|
7
|
+
* Security model (three hard prerequisites, all enforced before any read or
|
|
8
|
+
* write):
|
|
9
|
+
* 1. **Key sanitization** — a pure string pass (`sanitizePathKey`) run before
|
|
10
|
+
* any filesystem operation rejects null bytes, URL-encoded traversal, NFKC
|
|
11
|
+
* normalization differences, backslashes, and absolute keys.
|
|
12
|
+
* 2. **Seam-native validation chain** — for the sanitized key: `fs.lstat`
|
|
13
|
+
* rejects a final-segment symlink, then `fs.resolve` + `fs.contains`
|
|
14
|
+
* enforce prefix containment against the resolved team root.
|
|
15
|
+
* 3. **Containment on every access** — reads and writes go through the chain,
|
|
16
|
+
* so a symlinked intermediate component that points outside is caught by
|
|
17
|
+
* the resolve + contains step.
|
|
18
|
+
*
|
|
19
|
+
* Residual gap (documented): this closes per-access traversal, but a
|
|
20
|
+
* mutate-between-check TOCTOU window on *intermediate* components is not fully
|
|
21
|
+
* closed — only the final segment is lstat-checked, and the resolve/contains
|
|
22
|
+
* check and the read are not atomic. Do not enable `teamEnabled` in multi-tenant
|
|
23
|
+
* or untrusted-writer deployments.
|
|
24
|
+
* @module @dsh-cc/memory/team
|
|
25
|
+
*/
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
/** Error thrown when a team-memory key/path fails security validation. */
|
|
28
|
+
export class TeamMemoryError extends Error {
|
|
29
|
+
constructor(message) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'TeamMemoryError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Default subdirectory of the memory home holding team memory. */
|
|
35
|
+
export const TEAM_MEMORY_DIR = 'team';
|
|
36
|
+
/**
|
|
37
|
+
* The team memory entrypoint filename, matching the private entrypoint.
|
|
38
|
+
* Each directory (private and team) keeps its own MEMORY.md index.
|
|
39
|
+
*/
|
|
40
|
+
export const TEAM_ENTRYPOINT_NAME = 'MEMORY.md';
|
|
41
|
+
/**
|
|
42
|
+
* Sanitize a relative path key by rejecting dangerous patterns, mirroring the
|
|
43
|
+
* Claude Code `sanitizePathKey` semantics. Pure string — run this before any
|
|
44
|
+
* filesystem operation. Returns the sanitized key unchanged or throws
|
|
45
|
+
* {@link TeamMemoryError}. Rejects:
|
|
46
|
+
* - null bytes (can truncate paths in C-based syscalls)
|
|
47
|
+
* - URL-encoded traversal (`%2e%2e%2f` → `../`)
|
|
48
|
+
* - NFKC normalization that collapses to `..` / separators (fullwidth `../`)
|
|
49
|
+
* - backslashes (Windows separator used as a traversal vector)
|
|
50
|
+
* - absolute paths
|
|
51
|
+
* @param key - the relative path key under the team directory.
|
|
52
|
+
* @returns the sanitized key (unchanged).
|
|
53
|
+
*/
|
|
54
|
+
export function sanitizePathKey(key) {
|
|
55
|
+
if (key.length === 0) {
|
|
56
|
+
throw new TeamMemoryError('Empty path key');
|
|
57
|
+
}
|
|
58
|
+
// Null bytes can truncate paths in C-based syscalls.
|
|
59
|
+
if (key.includes('\0')) {
|
|
60
|
+
throw new TeamMemoryError(`Null byte in path key: "${key}"`);
|
|
61
|
+
}
|
|
62
|
+
// URL-encoded traversals (e.g. %2e%2e%2f = ../).
|
|
63
|
+
let decoded;
|
|
64
|
+
try {
|
|
65
|
+
decoded = decodeURIComponent(key);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Malformed percent-encoding (e.g. %ZZ, lone %) — not valid URL-encoding,
|
|
69
|
+
// so no URL-encoded traversal is possible.
|
|
70
|
+
decoded = key;
|
|
71
|
+
}
|
|
72
|
+
if (decoded !== key && (decoded.includes('..') || decoded.includes('/') || decoded.includes('\\'))) {
|
|
73
|
+
throw new TeamMemoryError(`URL-encoded traversal in path key: "${key}"`);
|
|
74
|
+
}
|
|
75
|
+
// Unicode normalization attacks: fullwidth ../ normalize to ASCII ../ under
|
|
76
|
+
// NFKC. Reject for defense-in-depth even though a literal-fs backend treats
|
|
77
|
+
// these as bytes, not separators.
|
|
78
|
+
const normalized = key.normalize('NFKC');
|
|
79
|
+
if (normalized !== key &&
|
|
80
|
+
(normalized.includes('..') ||
|
|
81
|
+
normalized.includes('/') ||
|
|
82
|
+
normalized.includes('\\') ||
|
|
83
|
+
normalized.includes('\0'))) {
|
|
84
|
+
throw new TeamMemoryError(`Unicode-normalized traversal in path key: "${key}"`);
|
|
85
|
+
}
|
|
86
|
+
// Reject backslashes (Windows path separator used as a traversal vector).
|
|
87
|
+
if (key.includes('\\')) {
|
|
88
|
+
throw new TeamMemoryError(`Backslash in path key: "${key}"`);
|
|
89
|
+
}
|
|
90
|
+
// Reject absolute paths.
|
|
91
|
+
if (key.startsWith('/')) {
|
|
92
|
+
throw new TeamMemoryError(`Absolute path key: "${key}"`);
|
|
93
|
+
}
|
|
94
|
+
return key;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the team memory directory for one workspace. Team memory is shared
|
|
98
|
+
* by all users of THE PROJECT, so it lives inside the workspace's private
|
|
99
|
+
* memory directory: `<workspaceDir>/team`. (Before per-workspace isolation it
|
|
100
|
+
* sat at the global `<memoryHome>/team`; that directory is now inert.)
|
|
101
|
+
* @param workspaceDir - the workspace's private memory directory.
|
|
102
|
+
* @returns the team directory path (`<workspaceDir>/team`).
|
|
103
|
+
*/
|
|
104
|
+
export function resolveTeamMemoryRoot(workspaceDir) {
|
|
105
|
+
return join(workspaceDir, TEAM_MEMORY_DIR);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The seam-native team path validation chain. For a relative key under a team
|
|
109
|
+
* directory, enforce, in order:
|
|
110
|
+
* 1. `sanitizePathKey` — pure-string rejection (no fs access).
|
|
111
|
+
* 2. `fs.lstat` — reject a final-segment symlink before following it.
|
|
112
|
+
* 3. `fs.resolve` of both the team root and the candidate, then `fs.contains`
|
|
113
|
+
* — reject when the resolved candidate escapes the resolved team root.
|
|
114
|
+
*
|
|
115
|
+
* Throws {@link TeamMemoryError} when any step fails. A missing file is not an
|
|
116
|
+
* error here: the resolve+contains step still spans the nearest existing
|
|
117
|
+
* ancestor (realpath), so a missing child under an escaping symlinked ancestor
|
|
118
|
+
* is still caught.
|
|
119
|
+
* @param fs - the contiguous filesystem seam.
|
|
120
|
+
* @param teamDir - the resolved team directory path.
|
|
121
|
+
* @param relativeKey - the sanitizable relative key to validate.
|
|
122
|
+
* @returns the resolved, contained {@link FsTarget} for the candidate.
|
|
123
|
+
*/
|
|
124
|
+
export async function validateTeamMemKey(fs, teamDir, relativeKey) {
|
|
125
|
+
sanitizePathKey(relativeKey);
|
|
126
|
+
const fullPath = join(teamDir, relativeKey);
|
|
127
|
+
// Step 2: reject a final-segment symlink.
|
|
128
|
+
const pathInfo = await fs.lstat(fullPath);
|
|
129
|
+
if (pathInfo !== undefined && pathInfo.type === 'symlink') {
|
|
130
|
+
throw new TeamMemoryError(`Symlink at final path segment: "${relativeKey}"`);
|
|
131
|
+
}
|
|
132
|
+
// Step 3: resolve + prefix containment.
|
|
133
|
+
const candidate = await fs.resolve(fullPath);
|
|
134
|
+
const teamRoot = await fs.resolve(teamDir);
|
|
135
|
+
if (!fs.contains(teamRoot, candidate)) {
|
|
136
|
+
throw new TeamMemoryError(`Path escapes team memory directory: "${relativeKey}"`);
|
|
137
|
+
}
|
|
138
|
+
return candidate;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Validate and read a team memory file body, or `undefined` when the file is
|
|
142
|
+
* absent. The read only proceeds when {@link validateTeamMemKey} passes, so a
|
|
143
|
+
* traversal or symlink-escape key never reaches the read.
|
|
144
|
+
* @param fs - the contiguous filesystem seam.
|
|
145
|
+
* @param teamDir - the resolved team directory path.
|
|
146
|
+
* @param relativeKey - the sanitizable relative key to read.
|
|
147
|
+
* @param signal - optional cancellation.
|
|
148
|
+
* @returns the file body, or `undefined` if absent.
|
|
149
|
+
*/
|
|
150
|
+
export async function readTeamMemFile(fs, teamDir, relativeKey, signal) {
|
|
151
|
+
const target = await validateTeamMemKey(fs, teamDir, relativeKey);
|
|
152
|
+
try {
|
|
153
|
+
return await fs.readText(target, signal);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (isNotFound(error))
|
|
157
|
+
return undefined;
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function isNotFound(error) {
|
|
162
|
+
return typeof error === 'object' && error !== null
|
|
163
|
+
&& 'code' in error
|
|
164
|
+
&& (error.code === 'FS_NOT_FOUND'
|
|
165
|
+
|| error.code === 'ENOENT');
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=team.js.map
|
package/lib/team.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"team.js","sourceRoot":"","sources":["../src/team.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MEMORY.md entrypoint bounds and truncation.
|
|
3
|
+
* The entrypoint is the always-loaded index; its content is capped so a
|
|
4
|
+
* runaway index cannot flood the system prompt.
|
|
5
|
+
* @module @dsh-cc/memory/truncate
|
|
6
|
+
*/
|
|
7
|
+
/** The always-loaded index filename inside a memory directory. */
|
|
8
|
+
export declare const ENTRYPOINT_NAME = "MEMORY.md";
|
|
9
|
+
/** Line cap for the entrypoint index. */
|
|
10
|
+
export declare const MAX_ENTRYPOINT_LINES = 200;
|
|
11
|
+
/** Byte cap for the entrypoint index (long lines are the failure mode). */
|
|
12
|
+
export declare const MAX_ENTRYPOINT_BYTES = 25000;
|
|
13
|
+
/** Result of applying the entrypoint caps. */
|
|
14
|
+
export interface EntrypointTruncation {
|
|
15
|
+
/** Truncated content, plus a trailing warning line when a cap fired. */
|
|
16
|
+
content: string;
|
|
17
|
+
/** Original trimmed line count (before any truncation). */
|
|
18
|
+
lineCount: number;
|
|
19
|
+
/** Original trimmed byte count (before any truncation). */
|
|
20
|
+
byteCount: number;
|
|
21
|
+
/** Whether the line cap fired. */
|
|
22
|
+
wasLineTruncated: boolean;
|
|
23
|
+
/** Whether the byte cap fired (against the original, pre-line-cap size). */
|
|
24
|
+
wasByteTruncated: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Truncate MEMORY.md content to the line AND byte caps. Line-truncates first
|
|
28
|
+
* (a natural boundary), then byte-truncates at the last newline before the cap
|
|
29
|
+
* so a line is never cut mid-way, then appends a warning that names which cap
|
|
30
|
+
* fired. Unmodified content is returned unchanged.
|
|
31
|
+
* @param raw - the raw entrypoint index text.
|
|
32
|
+
* @returns the capped content and the cap flags.
|
|
33
|
+
*/
|
|
34
|
+
export declare function truncateEntrypointContent(raw: string): EntrypointTruncation;
|
|
35
|
+
//# sourceMappingURL=truncate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../src/truncate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,kEAAkE;AAClE,eAAO,MAAM,eAAe,cAAc,CAAA;AAE1C,yCAAyC;AACzC,eAAO,MAAM,oBAAoB,MAAM,CAAA;AAEvC,2EAA2E;AAC3E,eAAO,MAAM,oBAAoB,QAAS,CAAA;AAE1C,8CAA8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAA;IACf,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAA;IACjB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAA;IACjB,kCAAkC;IAClC,gBAAgB,EAAE,OAAO,CAAA;IACzB,4EAA4E;IAC5E,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,oBAAoB,CAoC3E"}
|
package/lib/truncate.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MEMORY.md entrypoint bounds and truncation.
|
|
3
|
+
* The entrypoint is the always-loaded index; its content is capped so a
|
|
4
|
+
* runaway index cannot flood the system prompt.
|
|
5
|
+
* @module @dsh-cc/memory/truncate
|
|
6
|
+
*/
|
|
7
|
+
/** The always-loaded index filename inside a memory directory. */
|
|
8
|
+
export const ENTRYPOINT_NAME = 'MEMORY.md';
|
|
9
|
+
/** Line cap for the entrypoint index. */
|
|
10
|
+
export const MAX_ENTRYPOINT_LINES = 200;
|
|
11
|
+
/** Byte cap for the entrypoint index (long lines are the failure mode). */
|
|
12
|
+
export const MAX_ENTRYPOINT_BYTES = 25_000;
|
|
13
|
+
/**
|
|
14
|
+
* Truncate MEMORY.md content to the line AND byte caps. Line-truncates first
|
|
15
|
+
* (a natural boundary), then byte-truncates at the last newline before the cap
|
|
16
|
+
* so a line is never cut mid-way, then appends a warning that names which cap
|
|
17
|
+
* fired. Unmodified content is returned unchanged.
|
|
18
|
+
* @param raw - the raw entrypoint index text.
|
|
19
|
+
* @returns the capped content and the cap flags.
|
|
20
|
+
*/
|
|
21
|
+
export function truncateEntrypointContent(raw) {
|
|
22
|
+
const trimmed = raw.trim();
|
|
23
|
+
const contentLines = trimmed.split('\n');
|
|
24
|
+
const lineCount = contentLines.length;
|
|
25
|
+
const byteCount = trimmed.length;
|
|
26
|
+
const wasLineTruncated = lineCount > MAX_ENTRYPOINT_LINES;
|
|
27
|
+
const wasByteTruncated = byteCount > MAX_ENTRYPOINT_BYTES;
|
|
28
|
+
if (!wasLineTruncated && !wasByteTruncated) {
|
|
29
|
+
return { content: trimmed, lineCount, byteCount, wasLineTruncated, wasByteTruncated };
|
|
30
|
+
}
|
|
31
|
+
let truncated = wasLineTruncated
|
|
32
|
+
? contentLines.slice(0, MAX_ENTRYPOINT_LINES).join('\n')
|
|
33
|
+
: trimmed;
|
|
34
|
+
if (truncated.length > MAX_ENTRYPOINT_BYTES) {
|
|
35
|
+
const cutAt = truncated.lastIndexOf('\n', MAX_ENTRYPOINT_BYTES);
|
|
36
|
+
truncated = truncated.slice(0, cutAt > 0 ? cutAt : MAX_ENTRYPOINT_BYTES);
|
|
37
|
+
}
|
|
38
|
+
const size = byteCount >= 1024 ? `${(byteCount / 1024).toFixed(1)}KB` : `${byteCount}B`;
|
|
39
|
+
const reason = wasByteTruncated && !wasLineTruncated
|
|
40
|
+
? `${size} (limit: 25KB) — index entries are too long`
|
|
41
|
+
: wasLineTruncated && !wasByteTruncated
|
|
42
|
+
? `${lineCount} lines (limit: ${MAX_ENTRYPOINT_LINES})`
|
|
43
|
+
: `${lineCount} lines and ${size}`;
|
|
44
|
+
return {
|
|
45
|
+
content: `${truncated}\n\n> WARNING: ${ENTRYPOINT_NAME} is ${reason}. Only part of it was loaded. Keep index entries to one line under ~200 chars; move detail into topic files.`,
|
|
46
|
+
lineCount,
|
|
47
|
+
byteCount,
|
|
48
|
+
wasLineTruncated,
|
|
49
|
+
wasByteTruncated,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=truncate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"truncate.js","sourceRoot":"","sources":["../src/truncate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,kEAAkE;AAClE,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAA;AAE1C,yCAAyC;AACzC,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAA;AAEvC,2EAA2E;AAC3E,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAA;AAgB1C;;;;;;;GAOG;AACH,MAAM,UAAU,yBAAyB,CAAC,GAAW;IACnD,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;IAC1B,MAAM,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACxC,MAAM,SAAS,GAAG,YAAY,CAAC,MAAM,CAAA;IACrC,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAA;IAEhC,MAAM,gBAAgB,GAAG,SAAS,GAAG,oBAAoB,CAAA;IACzD,MAAM,gBAAgB,GAAG,SAAS,GAAG,oBAAoB,CAAA;IAEzD,IAAI,CAAC,gBAAgB,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC3C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAA;IACvF,CAAC;IAED,IAAI,SAAS,GAAG,gBAAgB;QAC9B,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,oBAAoB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QACxD,CAAC,CAAC,OAAO,CAAA;IAEX,IAAI,SAAS,CAAC,MAAM,GAAG,oBAAoB,EAAE,CAAC;QAC5C,MAAM,KAAK,GAAG,SAAS,CAAC,WAAW,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAA;QAC/D,SAAS,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAA;IAC1E,CAAC;IAED,MAAM,IAAI,GAAG,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,SAAS,GAAG,CAAA;IACvF,MAAM,MAAM,GAAG,gBAAgB,IAAI,CAAC,gBAAgB;QAClD,CAAC,CAAC,GAAG,IAAI,6CAA6C;QACtD,CAAC,CAAC,gBAAgB,IAAI,CAAC,gBAAgB;YACrC,CAAC,CAAC,GAAG,SAAS,kBAAkB,oBAAoB,GAAG;YACvD,CAAC,CAAC,GAAG,SAAS,cAAc,IAAI,EAAE,CAAA;IAEtC,OAAO;QACL,OAAO,EAAE,GAAG,SAAS,kBAAkB,eAAe,OAAO,MAAM,8GAA8G;QACjL,SAAS;QACT,SAAS;QACT,gBAAgB;QAChB,gBAAgB;KACjB,CAAA;AACH,CAAC"}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closed memory-type taxonomy and rationalized frontmatter for `dsh-memory`
|
|
3
|
+
* topic files.
|
|
4
|
+
* @module @dsh-cc/memory/types
|
|
5
|
+
*/
|
|
6
|
+
/** The four memory types a topic file may declare. */
|
|
7
|
+
export declare const MEMORY_TYPES: readonly ["user", "feedback", "project", "reference"];
|
|
8
|
+
/** One valid memory type; unknown or absent values degrade gracefully. */
|
|
9
|
+
export type MemoryType = (typeof MEMORY_TYPES)[number];
|
|
10
|
+
/**
|
|
11
|
+
* Parse a raw frontmatter `type` value into a {@link MemoryType}.
|
|
12
|
+
* @param raw - the value read from a topic file's frontmatter.
|
|
13
|
+
* @returns the matching type, or `undefined` for missing or unknown values.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseMemoryType(raw: unknown): MemoryType | undefined;
|
|
16
|
+
/** One topic file's rationalized frontmatter header. */
|
|
17
|
+
export interface MemoryFrontmatter {
|
|
18
|
+
/** Topic name used as the MEMORY.md index title. */
|
|
19
|
+
name: string;
|
|
20
|
+
/** One-line relevance description presented to the recall selector. */
|
|
21
|
+
description: string;
|
|
22
|
+
/** Topic type, or `undefined` for legacy files without a `type` field. */
|
|
23
|
+
type?: MemoryType;
|
|
24
|
+
}
|
|
25
|
+
/** A topic file discovered in a memory directory, with its rationalized header. */
|
|
26
|
+
export interface MemoryIndexEntry {
|
|
27
|
+
/** Absolute path of the topic file. */
|
|
28
|
+
path: string;
|
|
29
|
+
/** Basename used to join a MEMORY.md pointer and to dedupe recall. */
|
|
30
|
+
filename: string;
|
|
31
|
+
/** Rationalized frontmatter (name/description/type). */
|
|
32
|
+
frontmatter: MemoryFrontmatter;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,sDAAsD;AACtD,eAAO,MAAM,YAAY,uDAAwD,CAAA;AAEjF,0EAA0E;AAC1E,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAA;AAEtD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAIpE;AAED,wDAAwD;AACxD,MAAM,WAAW,iBAAiB;IAChC,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAA;IACnB,0EAA0E;IAC1E,IAAI,CAAC,EAAE,UAAU,CAAA;CAClB;AAED,mFAAmF;AACnF,MAAM,WAAW,gBAAgB;IAC/B,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAA;IAChB,wDAAwD;IACxD,WAAW,EAAE,iBAAiB,CAAA;CAC/B"}
|
package/lib/types.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closed memory-type taxonomy and rationalized frontmatter for `dsh-memory`
|
|
3
|
+
* topic files.
|
|
4
|
+
* @module @dsh-cc/memory/types
|
|
5
|
+
*/
|
|
6
|
+
/** The four memory types a topic file may declare. */
|
|
7
|
+
export const MEMORY_TYPES = ['user', 'feedback', 'project', 'reference'];
|
|
8
|
+
/**
|
|
9
|
+
* Parse a raw frontmatter `type` value into a {@link MemoryType}.
|
|
10
|
+
* @param raw - the value read from a topic file's frontmatter.
|
|
11
|
+
* @returns the matching type, or `undefined` for missing or unknown values.
|
|
12
|
+
*/
|
|
13
|
+
export function parseMemoryType(raw) {
|
|
14
|
+
return typeof raw === 'string'
|
|
15
|
+
? MEMORY_TYPES.find(type => type === raw)
|
|
16
|
+
: undefined;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=types.js.map
|
package/lib/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,sDAAsD;AACtD,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,CAAU,CAAA;AAKjF;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,OAAO,OAAO,GAAG,KAAK,QAAQ;QAC5B,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,GAAG,CAAC;QACzC,CAAC,CAAC,SAAS,CAAA;AACf,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side write-back for memory fork output.
|
|
3
|
+
*
|
|
4
|
+
* The extraction/dream forks inherit the parent session's sandbox policy,
|
|
5
|
+
* under which the memory directory (the harness home) is OUTSIDE the session
|
|
6
|
+
* workspace: every model-side `write` is fenced, and a background job cannot
|
|
7
|
+
* escalate (escalation prompts, and its approval policy is `never`). So the
|
|
8
|
+
* forks return their file set as structured output and the plugin — trusted
|
|
9
|
+
* host code — performs the writes itself, stamping a per-call policy whose
|
|
10
|
+
* writable root IS the memory directory. The sandbox still confines each
|
|
11
|
+
* write behind the filename validation below (defense in depth); nothing
|
|
12
|
+
* outside the memory directory becomes writable.
|
|
13
|
+
*
|
|
14
|
+
* Validation rejects the WHOLE batch on any violation — a partial write would
|
|
15
|
+
* leave the index and its topic files inconsistent with no signal back to the
|
|
16
|
+
* fork that produced them.
|
|
17
|
+
* @module @dsh-cc/memory-consolidation/writeback
|
|
18
|
+
*/
|
|
19
|
+
import type { FileSystem } from '@deepseek-ai/dsh-fs';
|
|
20
|
+
/** One file the fork asks the plugin to write: a flat filename plus full body. */
|
|
21
|
+
export interface MemoryWrite {
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly content: string;
|
|
24
|
+
}
|
|
25
|
+
/** The structured-output contract every memory fork reports. */
|
|
26
|
+
export declare const MEMORY_WRITES_SCHEMA: {
|
|
27
|
+
readonly type: "object";
|
|
28
|
+
readonly properties: {
|
|
29
|
+
readonly writes: {
|
|
30
|
+
readonly type: "array";
|
|
31
|
+
readonly items: {
|
|
32
|
+
readonly type: "object";
|
|
33
|
+
readonly properties: {
|
|
34
|
+
readonly path: {
|
|
35
|
+
readonly type: "string";
|
|
36
|
+
};
|
|
37
|
+
readonly content: {
|
|
38
|
+
readonly type: "string";
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
readonly required: readonly ["path", "content"];
|
|
42
|
+
readonly additionalProperties: false;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
readonly required: readonly ["writes"];
|
|
47
|
+
readonly additionalProperties: false;
|
|
48
|
+
};
|
|
49
|
+
/** At most this many files per fork report. */
|
|
50
|
+
export declare const WRITEBACK_MAX_FILES = 32;
|
|
51
|
+
/** Per-file body cap, in UTF-8 bytes. */
|
|
52
|
+
export declare const WRITEBACK_MAX_FILE_BYTES: number;
|
|
53
|
+
/** Whole-batch body cap, in UTF-8 bytes. */
|
|
54
|
+
export declare const WRITEBACK_MAX_TOTAL_BYTES: number;
|
|
55
|
+
/**
|
|
56
|
+
* The per-call policy stamped on host-side memory writes: confinement is kept
|
|
57
|
+
* (`workspace-write`), but the writable root is the memory directory itself.
|
|
58
|
+
* Mirrors the upstream "an approved escalation stamps a wider mode for one
|
|
59
|
+
* call" mechanism — here the plugin is the approver, and the name validation
|
|
60
|
+
* above has already narrowed the file set.
|
|
61
|
+
*/
|
|
62
|
+
export interface MemoryWritePolicy {
|
|
63
|
+
readonly mode: 'workspace-write';
|
|
64
|
+
readonly workspaceRoot: string;
|
|
65
|
+
}
|
|
66
|
+
/** The policy for host-side writes inside `dir` (also used for the dream lock). */
|
|
67
|
+
export declare function memoryWritePolicy(dir: string): MemoryWritePolicy;
|
|
68
|
+
/**
|
|
69
|
+
* Validate a fork's structured payload into a writable batch. Throws with a
|
|
70
|
+
* precise reason on ANY violation; an empty `writes` array is a valid no-op.
|
|
71
|
+
* @param input - the raw `structured` value captured from the fork.
|
|
72
|
+
* @returns the validated writes.
|
|
73
|
+
*/
|
|
74
|
+
export declare function validateMemoryWrites(input: unknown): MemoryWrite[];
|
|
75
|
+
/**
|
|
76
|
+
* Validate, then write the batch inside the memory directory under the
|
|
77
|
+
* confined per-call policy. Writes are sequential so a mid-batch failure
|
|
78
|
+
* leaves a deterministic prefix; the caller maps any throw to a failed job.
|
|
79
|
+
* @param fs - the filesystem seam.
|
|
80
|
+
* @param dir - the memory directory root.
|
|
81
|
+
* @param writes - an already-validated batch (see {@link validateMemoryWrites}).
|
|
82
|
+
* @returns the filenames written, in batch order.
|
|
83
|
+
*/
|
|
84
|
+
export declare function writeMemoryFiles(fs: FileSystem, dir: string, writes: readonly MemoryWrite[]): Promise<string[]>;
|
|
85
|
+
//# sourceMappingURL=writeback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writeback.d.ts","sourceRoot":"","sources":["../src/writeback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAErD,kFAAkF;AAClF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,gEAAgE;AAChE,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;CAkBvB,CAAA;AAEV,+CAA+C;AAC/C,eAAO,MAAM,mBAAmB,KAAK,CAAA;AACrC,yCAAyC;AACzC,eAAO,MAAM,wBAAwB,QAAY,CAAA;AACjD,4CAA4C;AAC5C,eAAO,MAAM,yBAAyB,QAAa,CAAA;AASnD;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAA;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;CAC/B;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAEhE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,EAAE,CAuClE;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,WAAW,EAAE,GAC7B,OAAO,CAAC,MAAM,EAAE,CAAC,CASnB"}
|
package/lib/writeback.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side write-back for memory fork output.
|
|
3
|
+
*
|
|
4
|
+
* The extraction/dream forks inherit the parent session's sandbox policy,
|
|
5
|
+
* under which the memory directory (the harness home) is OUTSIDE the session
|
|
6
|
+
* workspace: every model-side `write` is fenced, and a background job cannot
|
|
7
|
+
* escalate (escalation prompts, and its approval policy is `never`). So the
|
|
8
|
+
* forks return their file set as structured output and the plugin — trusted
|
|
9
|
+
* host code — performs the writes itself, stamping a per-call policy whose
|
|
10
|
+
* writable root IS the memory directory. The sandbox still confines each
|
|
11
|
+
* write behind the filename validation below (defense in depth); nothing
|
|
12
|
+
* outside the memory directory becomes writable.
|
|
13
|
+
*
|
|
14
|
+
* Validation rejects the WHOLE batch on any violation — a partial write would
|
|
15
|
+
* leave the index and its topic files inconsistent with no signal back to the
|
|
16
|
+
* fork that produced them.
|
|
17
|
+
* @module @dsh-cc/memory-consolidation/writeback
|
|
18
|
+
*/
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
/** The structured-output contract every memory fork reports. */
|
|
21
|
+
export const MEMORY_WRITES_SCHEMA = {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
writes: {
|
|
25
|
+
type: 'array',
|
|
26
|
+
items: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
path: { type: 'string' },
|
|
30
|
+
content: { type: 'string' },
|
|
31
|
+
},
|
|
32
|
+
required: ['path', 'content'],
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ['writes'],
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
};
|
|
40
|
+
/** At most this many files per fork report. */
|
|
41
|
+
export const WRITEBACK_MAX_FILES = 32;
|
|
42
|
+
/** Per-file body cap, in UTF-8 bytes. */
|
|
43
|
+
export const WRITEBACK_MAX_FILE_BYTES = 64 * 1024;
|
|
44
|
+
/** Whole-batch body cap, in UTF-8 bytes. */
|
|
45
|
+
export const WRITEBACK_MAX_TOTAL_BYTES = 256 * 1024;
|
|
46
|
+
/**
|
|
47
|
+
* Flat `.md` filenames only: a leading alphanumeric, then alphanumerics /
|
|
48
|
+
* dots / dashes / underscores. This forbids separators, `..` escapes,
|
|
49
|
+
* absolute paths, and dotfiles (including the consolidation lock) in one rule.
|
|
50
|
+
*/
|
|
51
|
+
const FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*\.md$/;
|
|
52
|
+
/** The policy for host-side writes inside `dir` (also used for the dream lock). */
|
|
53
|
+
export function memoryWritePolicy(dir) {
|
|
54
|
+
return { mode: 'workspace-write', workspaceRoot: dir };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate a fork's structured payload into a writable batch. Throws with a
|
|
58
|
+
* precise reason on ANY violation; an empty `writes` array is a valid no-op.
|
|
59
|
+
* @param input - the raw `structured` value captured from the fork.
|
|
60
|
+
* @returns the validated writes.
|
|
61
|
+
*/
|
|
62
|
+
export function validateMemoryWrites(input) {
|
|
63
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
64
|
+
throw new Error('memory fork output must be an object with a "writes" array');
|
|
65
|
+
}
|
|
66
|
+
const raw = input.writes;
|
|
67
|
+
if (!Array.isArray(raw)) {
|
|
68
|
+
throw new Error('memory fork output must be an object with a "writes" array');
|
|
69
|
+
}
|
|
70
|
+
if (raw.length > WRITEBACK_MAX_FILES) {
|
|
71
|
+
throw new Error(`memory fork reported ${raw.length} files, over the ${WRITEBACK_MAX_FILES} cap`);
|
|
72
|
+
}
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
let total = 0;
|
|
75
|
+
const writes = raw.map((entry, i) => {
|
|
76
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
77
|
+
throw new Error(`memory write #${i} must be an object with path/content strings`);
|
|
78
|
+
}
|
|
79
|
+
const { path, content } = entry;
|
|
80
|
+
if (typeof path !== 'string' || typeof content !== 'string') {
|
|
81
|
+
throw new Error(`memory write #${i} must be an object with path/content strings`);
|
|
82
|
+
}
|
|
83
|
+
if (!FILE_NAME.test(path)) {
|
|
84
|
+
throw new Error(`invalid memory filename "${path}": expected a flat .md name`);
|
|
85
|
+
}
|
|
86
|
+
if (seen.has(path)) {
|
|
87
|
+
throw new Error(`duplicate memory filename "${path}" in one batch`);
|
|
88
|
+
}
|
|
89
|
+
seen.add(path);
|
|
90
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
91
|
+
if (bytes > WRITEBACK_MAX_FILE_BYTES) {
|
|
92
|
+
throw new Error(`memory file "${path}" is ${bytes} bytes, over the ${WRITEBACK_MAX_FILE_BYTES} cap`);
|
|
93
|
+
}
|
|
94
|
+
total += bytes;
|
|
95
|
+
return { path, content };
|
|
96
|
+
});
|
|
97
|
+
if (total > WRITEBACK_MAX_TOTAL_BYTES) {
|
|
98
|
+
throw new Error(`memory batch is ${total} bytes, over the ${WRITEBACK_MAX_TOTAL_BYTES} cap`);
|
|
99
|
+
}
|
|
100
|
+
return writes;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Validate, then write the batch inside the memory directory under the
|
|
104
|
+
* confined per-call policy. Writes are sequential so a mid-batch failure
|
|
105
|
+
* leaves a deterministic prefix; the caller maps any throw to a failed job.
|
|
106
|
+
* @param fs - the filesystem seam.
|
|
107
|
+
* @param dir - the memory directory root.
|
|
108
|
+
* @param writes - an already-validated batch (see {@link validateMemoryWrites}).
|
|
109
|
+
* @returns the filenames written, in batch order.
|
|
110
|
+
*/
|
|
111
|
+
export async function writeMemoryFiles(fs, dir, writes) {
|
|
112
|
+
const policy = memoryWritePolicy(dir);
|
|
113
|
+
const written = [];
|
|
114
|
+
for (const { path, content } of writes) {
|
|
115
|
+
const target = await fs.resolve(join(dir, path));
|
|
116
|
+
await fs.writeText(target, content, undefined, undefined, policy);
|
|
117
|
+
written.push(path);
|
|
118
|
+
}
|
|
119
|
+
return written;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=writeback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writeback.js","sourceRoot":"","sources":["../src/writeback.ts"],"names":[],"mappings":"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"}
|