@team-harness/memory-algorithms 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +27 -0
  2. package/README.md +203 -0
  3. package/dist/contracts.d.ts +178 -0
  4. package/dist/contracts.js +1 -0
  5. package/dist/index.d.ts +14 -0
  6. package/dist/index.js +6 -0
  7. package/dist/runtime/documents.d.ts +15 -0
  8. package/dist/runtime/documents.js +209 -0
  9. package/dist/runtime/l1.d.ts +10 -0
  10. package/dist/runtime/l1.js +172 -0
  11. package/dist/runtime/run.d.ts +44 -0
  12. package/dist/runtime/run.js +191 -0
  13. package/dist/runtime/skill-workspace.d.ts +70 -0
  14. package/dist/runtime/skill-workspace.js +156 -0
  15. package/dist/runtime/skills.d.ts +9 -0
  16. package/dist/runtime/skills.js +48 -0
  17. package/dist/runtime/telemetry.d.ts +10 -0
  18. package/dist/runtime/telemetry.js +5 -0
  19. package/dist/runtime/tools.d.ts +15 -0
  20. package/dist/runtime/tools.js +5 -0
  21. package/dist/upstream/config.d.ts +1 -0
  22. package/dist/upstream/config.js +1 -0
  23. package/dist/upstream/core/conversation/l0-recorder.d.ts +6 -0
  24. package/dist/upstream/core/conversation/l0-recorder.js +1 -0
  25. package/dist/upstream/core/memory-prompt/composer.d.ts +6 -0
  26. package/dist/upstream/core/memory-prompt/composer.js +33 -0
  27. package/dist/upstream/core/memory-prompt/types.d.ts +103 -0
  28. package/dist/upstream/core/memory-prompt/types.js +21 -0
  29. package/dist/upstream/core/prompts/l1-dedup.d.ts +33 -0
  30. package/dist/upstream/core/prompts/l1-dedup.js +202 -0
  31. package/dist/upstream/core/prompts/l1-extraction.d.ts +24 -0
  32. package/dist/upstream/core/prompts/l1-extraction.js +400 -0
  33. package/dist/upstream/core/prompts/persona-generation.d.ts +29 -0
  34. package/dist/upstream/core/prompts/persona-generation.js +284 -0
  35. package/dist/upstream/core/prompts/scene-extraction.d.ts +40 -0
  36. package/dist/upstream/core/prompts/scene-extraction.js +534 -0
  37. package/dist/upstream/core/record/l1-dedup.d.ts +10 -0
  38. package/dist/upstream/core/record/l1-dedup.js +108 -0
  39. package/dist/upstream/core/record/l1-extractor.d.ts +33 -0
  40. package/dist/upstream/core/record/l1-extractor.js +128 -0
  41. package/dist/upstream/core/record/l1-writer.d.ts +95 -0
  42. package/dist/upstream/core/record/l1-writer.js +1 -0
  43. package/dist/upstream/core/scene/filename-normalizer.d.ts +6 -0
  44. package/dist/upstream/core/scene/filename-normalizer.js +30 -0
  45. package/dist/upstream/core/scene/scene-format.d.ts +26 -0
  46. package/dist/upstream/core/scene/scene-format.js +53 -0
  47. package/dist/upstream/core/scene/scene-index.d.ts +7 -0
  48. package/dist/upstream/core/scene/scene-index.js +1 -0
  49. package/dist/upstream/core/scene/scene-navigation.d.ts +66 -0
  50. package/dist/upstream/core/scene/scene-navigation.js +107 -0
  51. package/dist/upstream/core/skill/conversation-add/message-compressor.d.ts +47 -0
  52. package/dist/upstream/core/skill/conversation-add/message-compressor.js +58 -0
  53. package/dist/upstream/core/skill/conversation-add/oversize-strategy.d.ts +41 -0
  54. package/dist/upstream/core/skill/conversation-add/oversize-strategy.js +100 -0
  55. package/dist/upstream/core/skill/prompts/skill-review-prompt.d.ts +39 -0
  56. package/dist/upstream/core/skill/prompts/skill-review-prompt.js +197 -0
  57. package/dist/upstream/core/skill/skill-extractor.d.ts +146 -0
  58. package/dist/upstream/core/skill/skill-extractor.js +432 -0
  59. package/dist/upstream/core/skill/skill-format.d.ts +46 -0
  60. package/dist/upstream/core/skill/skill-format.js +191 -0
  61. package/dist/upstream/core/skill/skill-tools.d.ts +75 -0
  62. package/dist/upstream/core/skill/skill-tools.js +193 -0
  63. package/dist/upstream/core/skill/types.d.ts +324 -0
  64. package/dist/upstream/core/skill/types.js +7 -0
  65. package/dist/upstream/utils/sanitize.d.ts +96 -0
  66. package/dist/upstream/utils/sanitize.js +359 -0
  67. package/package.json +28 -0
  68. package/upstream/baseline.json +426 -0
  69. package/upstream/changes.md +81 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Text sanitization for memory pipeline (capture & recall).
3
+ * Removes injected tags, gateway metadata, media noise, etc.
4
+ */
5
+ /**
6
+ * Clean text for the memory pipeline: remove injected tags, metadata,
7
+ * timestamps, media markers and base64 image data.
8
+ *
9
+ * Used by both capture (L0 recording) and recall (query cleaning) paths.
10
+ */
11
+ export declare function sanitizeText(text: string): string;
12
+ /**
13
+ * Strip fenced code blocks from assistant replies before L0 capture.
14
+ *
15
+ * AI responses often contain large code snippets (```...```) that dilute
16
+ * the semantic signal for embedding and memory extraction. This function
17
+ * removes only the code block content while preserving surrounding
18
+ * natural-language explanations.
19
+ *
20
+ * Only applied to `role=assistant` messages in the L0 capture path —
21
+ * user messages and recall queries are NOT affected.
22
+ */
23
+ export declare function stripCodeBlocks(text: string): string;
24
+ /**
25
+ * L0 capture filter — intentionally **permissive**.
26
+ *
27
+ * L0 is the raw conversation archive. We want to preserve as much user input
28
+ * as possible so that downstream stages (L1 extraction, search, analytics)
29
+ * have the full picture. Only messages that are *structurally* useless are
30
+ * dropped here:
31
+ * - Empty / whitespace-only text
32
+ * - Framework-internal noise (bootstrap, session reset, NO_REPLY, …)
33
+ * - Slash commands (/new, /reset, …)
34
+ *
35
+ * Content-quality filters (length, symbols, prompt injection) are deferred
36
+ * to {@link shouldExtractL1}.
37
+ */
38
+ export declare function shouldCaptureL0(text: string): boolean;
39
+ /**
40
+ * L1 extraction filter — **strict** quality gate.
41
+ *
42
+ * Applied when L0 messages are fed into the LLM extraction pipeline.
43
+ * Filters out content that is too short, too long, purely symbolic,
44
+ * or looks like a prompt-injection attack — none of which should
45
+ * become structured memories.
46
+ *
47
+ * This function is a superset of {@link shouldCaptureL0}: anything
48
+ * rejected by L0 is also rejected here, plus additional quality checks.
49
+ */
50
+ export declare function shouldExtractL1(text: string): boolean;
51
+ /**
52
+ * @deprecated Use {@link shouldExtractL1} (strict) or {@link shouldCaptureL0} (permissive) instead.
53
+ *
54
+ * Kept as an alias of `shouldExtractL1` for backward compatibility.
55
+ */
56
+ export declare const shouldCapture: typeof shouldExtractL1;
57
+ /**
58
+ * Detect likely prompt-injection / jailbreak attempts.
59
+ *
60
+ * Normalises whitespace before matching to defeat trivial obfuscation
61
+ * (e.g. extra spaces / newlines between keywords).
62
+ */
63
+ export declare function looksLikePromptInjection(text: string): boolean;
64
+ /**
65
+ * Pick up to `max` recent unique texts.
66
+ */
67
+ export declare function pickRecentUnique(texts: string[], max: number): string[];
68
+ /**
69
+ * Escape XML-like tags in text to prevent tag injection attacks.
70
+ *
71
+ * When memory content or persona text is injected into XML-delimited sections
72
+ * (e.g. `<user-persona>...</user-persona>`), a malicious user could craft content
73
+ * containing `</user-persona>` to break out of the section boundary.
74
+ *
75
+ * This function escapes `<` and `>` in known dangerous patterns (closing tags
76
+ * that match our injection boundaries) so the content cannot prematurely close
77
+ * the XML section.
78
+ */
79
+ export declare function escapeXmlTags(text: string): string;
80
+ /**
81
+ * Sanitize a raw JSON string from LLM output so that `JSON.parse` won't throw
82
+ * "Bad control character in string literal".
83
+ *
84
+ * Per RFC 8259 §7, U+0000–U+001F MUST be escaped inside JSON string literals.
85
+ * LLMs sometimes produce unescaped control characters (raw newlines, tabs, etc.)
86
+ * inside string values.
87
+ *
88
+ * Strategy (two-phase):
89
+ * 1. **Precise pass** — walk through JSON string literals (delimited by `"`)
90
+ * and escape any unescaped U+0000–U+001F inside them to `\uXXXX` form,
91
+ * while leaving structural whitespace (between values) untouched.
92
+ * 2. **Fallback** — if the precise pass still fails `JSON.parse`, fall back to
93
+ * a simple global strip of rare control chars (\x00–\x08, \x0b, \x0c,
94
+ * \x0e–\x1f) which are almost never meaningful in natural-language content.
95
+ */
96
+ export declare function sanitizeJsonForParse(raw: string): string;
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Text sanitization for memory pipeline (capture & recall).
3
+ * Removes injected tags, gateway metadata, media noise, etc.
4
+ */
5
+ /**
6
+ * Clean text for the memory pipeline: remove injected tags, metadata,
7
+ * timestamps, media markers and base64 image data.
8
+ *
9
+ * Used by both capture (L0 recording) and recall (query cleaning) paths.
10
+ */
11
+ export function sanitizeText(text) {
12
+ let cleaned = text;
13
+ // Remove injected memory context tags (prevent feedback loops)
14
+ cleaned = cleaned.replace(/<relevant-memories>[\s\S]*?<\/relevant-memories>/g, "");
15
+ cleaned = cleaned.replace(/<user-persona>[\s\S]*?<\/user-persona>/g, "");
16
+ cleaned = cleaned.replace(/<relevant-scenes>[\s\S]*?<\/relevant-scenes>/g, "");
17
+ cleaned = cleaned.replace(/<scene-navigation>[\s\S]*?<\/scene-navigation>/g, "");
18
+ // Remove offload-injected task context blocks (MMD mermaid diagrams)
19
+ cleaned = cleaned.replace(/<current_task_context>[\s\S]*?<\/current_task_context>/g, "");
20
+ cleaned = cleaned.replace(/<history_task_context[\s\S]*?<\/history_task_context>/g, "");
21
+ // Remove framework-injected inbound metadata blocks (from inbound-meta.ts buildInboundUserContextPrefix).
22
+ // These are "label:\n```json\n...\n```" blocks that the framework prepends to user messages.
23
+ // Pattern matches all known block labels:
24
+ // - Conversation info (untrusted metadata):
25
+ // - Sender (untrusted metadata):
26
+ // - Thread starter (untrusted, for context):
27
+ // - Replied message (untrusted, for context):
28
+ // - Forwarded message context (untrusted metadata):
29
+ // - Chat history since last reply (untrusted, for context):
30
+ cleaned = cleaned.replace(/(?:Conversation info|Sender|Thread starter|Replied message|Forwarded message context|Chat history since last reply)\s*\(untrusted[\s\S]*?\):\s*```json\s*[\s\S]*?```/g, "");
31
+ // Remove conversation metadata JSON blocks (legacy pattern)
32
+ cleaned = cleaned.replace(/```json\s*\{[\s\S]*?"session[\s\S]*?\}\s*```/g, "");
33
+ // Remove framework reply directive tags: [[reply_to_current]], [[reply_to_xxx]], etc.
34
+ cleaned = cleaned.replace(/\[\[reply_to[^\]]*\]\]\s*/g, "");
35
+ // Remove injected skill-selection wrappers, e.g. ¥¥[... ]¥¥
36
+ cleaned = cleaned.replace(/¥¥\[[\s\S]*?\]¥¥/g, "");
37
+ // Remove line-leading timestamps, e.g. "[Tue 2026-03-24 03:48 UTC]"
38
+ // or "[Tue 2026-03-24 20:21 GMT+8]", "[Thu 2026-03-24 01:51 GMT+5:30]"
39
+ // Matches brackets containing word chars, digits, hyphens, colons, plus signs,
40
+ // and spaces — the '+' is needed for timezone offsets like GMT+8, GMT+5:30.
41
+ cleaned = cleaned.replace(/^\[[\w\d\-:+ ]+\]\s*/gm, "");
42
+ // Remove gateway media-attachment markers:
43
+ // [media attached: /path/to/file.png (image/png) | /path/to/file.png]
44
+ cleaned = cleaned.replace(/\[media attached:[^\]]*\]\s*/g, "");
45
+ // Remove gateway image-reply instructions injected after media attachments.
46
+ // Starts with "To send an image back" and ends before the next real content.
47
+ cleaned = cleaned.replace(/To send an image back,[\s\S]*?(?:Keep caption in the text body\.)\s*/g, "");
48
+ // Remove "System: [timestamp] Exec completed ..." blocks appended by the framework.
49
+ cleaned = cleaned.replace(/^System:\s*\[[\s\S]*?$/gm, "");
50
+ // Remove inline base64 image data URIs (e.g. data:image/png;base64,iVBOR...)
51
+ // Replace with empty string (not a placeholder) so that pure-image messages
52
+ // become empty after sanitization and are naturally filtered by length checks.
53
+ cleaned = cleaned.replace(/data:image\/[a-z+]+;base64,[A-Za-z0-9+/=]+/gi, "");
54
+ // Remove null chars + compress whitespace
55
+ cleaned = cleaned.replace(/\0/g, "").replace(/\n{3,}/g, "\n\n").trim();
56
+ return cleaned;
57
+ }
58
+ /**
59
+ * Strip fenced code blocks from assistant replies before L0 capture.
60
+ *
61
+ * AI responses often contain large code snippets (```...```) that dilute
62
+ * the semantic signal for embedding and memory extraction. This function
63
+ * removes only the code block content while preserving surrounding
64
+ * natural-language explanations.
65
+ *
66
+ * Only applied to `role=assistant` messages in the L0 capture path —
67
+ * user messages and recall queries are NOT affected.
68
+ */
69
+ export function stripCodeBlocks(text) {
70
+ return text.replace(/```[^\n]*\n[\s\S]*?```/g, "").replace(/\n{3,}/g, "\n\n").trim();
71
+ }
72
+ // ============================
73
+ // L0 / L1 Capture & Extraction Filters
74
+ // ============================
75
+ /**
76
+ * L0 capture filter — intentionally **permissive**.
77
+ *
78
+ * L0 is the raw conversation archive. We want to preserve as much user input
79
+ * as possible so that downstream stages (L1 extraction, search, analytics)
80
+ * have the full picture. Only messages that are *structurally* useless are
81
+ * dropped here:
82
+ * - Empty / whitespace-only text
83
+ * - Framework-internal noise (bootstrap, session reset, NO_REPLY, …)
84
+ * - Slash commands (/new, /reset, …)
85
+ *
86
+ * Content-quality filters (length, symbols, prompt injection) are deferred
87
+ * to {@link shouldExtractL1}.
88
+ */
89
+ export function shouldCaptureL0(text) {
90
+ if (!text || !text.trim())
91
+ return false;
92
+ // Filter framework-internal / bootstrap noise messages
93
+ if (isFrameworkNoise(text))
94
+ return false;
95
+ // Slash commands are framework directives, not user content
96
+ if (text.startsWith("/"))
97
+ return false;
98
+ return true;
99
+ }
100
+ /**
101
+ * L1 extraction filter — **strict** quality gate.
102
+ *
103
+ * Applied when L0 messages are fed into the LLM extraction pipeline.
104
+ * Filters out content that is too short, too long, purely symbolic,
105
+ * or looks like a prompt-injection attack — none of which should
106
+ * become structured memories.
107
+ *
108
+ * This function is a superset of {@link shouldCaptureL0}: anything
109
+ * rejected by L0 is also rejected here, plus additional quality checks.
110
+ */
111
+ export function shouldExtractL1(text) {
112
+ // First apply the same structural filters as L0
113
+ if (!shouldCaptureL0(text))
114
+ return false;
115
+ // ── Length filters ──
116
+ // const isCJK = /[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/.test(text);
117
+ // if (isCJK && text.length < 2) return false;
118
+ // if (!isCJK && text.length < 2) return false;
119
+ // if (text.length > 5000) return false;
120
+ // ── Content-quality filters ──
121
+ // Match strings composed entirely of non-word, non-space, non-CJK characters (1–5 chars).
122
+ if (/^[^\w\s\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]{1,5}$/.test(text))
123
+ return false;
124
+ if (/^[??]+$/.test(text))
125
+ return false;
126
+ // ── Security filters ──
127
+ // Reject prompt-injection payloads — prevent malicious content from being
128
+ // persisted into structured memory and re-injected on future recalls.
129
+ // if (looksLikePromptInjection(text)) return false;
130
+ return true;
131
+ }
132
+ /**
133
+ * @deprecated Use {@link shouldExtractL1} (strict) or {@link shouldCaptureL0} (permissive) instead.
134
+ *
135
+ * Kept as an alias of `shouldExtractL1` for backward compatibility.
136
+ */
137
+ export const shouldCapture = shouldExtractL1;
138
+ // ============================
139
+ // Prompt Injection Detection
140
+ // ============================
141
+ /**
142
+ * Known prompt-injection / jailbreak patterns.
143
+ *
144
+ * Covers:
145
+ * 1. Instruction override — "ignore all previous instructions", etc.
146
+ * 2. Role hijack — "you are now DAN", "act as root", etc.
147
+ * 3. System/developer boundary probing — "system prompt", "developer message"
148
+ * 4. XML/tag injection — opening tags that match our context boundaries
149
+ * 5. Tool/command invocation tricks — "run command X", "execute tool Y"
150
+ * 6. Multi-language variants — Chinese prompt-injection patterns
151
+ */
152
+ const PROMPT_INJECTION_PATTERNS = [
153
+ // ── Instruction override ──
154
+ /ignore\b.{0,30}\b(instructions|rules|guidelines)/i,
155
+ /disregard\b.{0,30}\b(instructions|rules|guidelines)/i,
156
+ /forget\b.{0,30}\b(instructions|rules|context)/i,
157
+ /override\b.{0,30}\b(instructions|rules|guidelines|safety)/i,
158
+ // ── Role hijack ──
159
+ /you are now (?!going|about|ready)/i, // "you are now DAN" but not "you are now going to..."
160
+ /act as (?:if you are |if you were )?(?:a |an )?(?:root|admin|unrestricted|unfiltered|jailbroken)/i,
161
+ /enter (?:DAN|jailbreak|god|sudo|developer|dev|debug|unrestricted|unfiltered) mode/i,
162
+ /switch to (?:DAN|jailbreak|god|sudo|developer|dev|debug|unrestricted|unfiltered) mode/i,
163
+ // ── System boundary probing ──
164
+ /(?:show|reveal|print|output|display|repeat|leak|dump|give)\b.{0,20}\bsystem prompt/i,
165
+ /reveal (?:your |the )?(system|hidden|secret|internal) (?:prompt|instructions|rules)/i,
166
+ /what (?:are|is) your (?:system|hidden|original|initial) (?:prompt|instructions|rules)/i,
167
+ // ── XML/tag injection (our context boundaries) ──
168
+ /<\s*(system|assistant|developer|tool|function|relevant-memories)\b/i,
169
+ // ── Tool/command invocation tricks ──
170
+ /\b(run|execute|call|invoke)\b.{0,40}\b(tool|command|function|shell)\b/i,
171
+ // ── Chinese variants ──
172
+ /忽略(?:所有|之前|以上|先前)?(?:的)?(?:指令|规则|指示|说明)/,
173
+ /无视(?:所有|之前|以上)?(?:的)?(?:指令|规则|限制)/,
174
+ /(?:显示|输出|告诉我|给我看)(?:你的)?(?:系统|初始|隐藏)?(?:提示词|指令|规则|prompt)/,
175
+ /你(?:现在|从现在开始)是/, // "你现在是 DAN"
176
+ ];
177
+ /**
178
+ * Detect likely prompt-injection / jailbreak attempts.
179
+ *
180
+ * Normalises whitespace before matching to defeat trivial obfuscation
181
+ * (e.g. extra spaces / newlines between keywords).
182
+ */
183
+ export function looksLikePromptInjection(text) {
184
+ const normalized = text.replace(/\s+/g, " ").trim();
185
+ if (!normalized)
186
+ return false;
187
+ return PROMPT_INJECTION_PATTERNS.some((pattern) => pattern.test(normalized));
188
+ }
189
+ /**
190
+ * Detect framework-injected noise messages that should never be captured.
191
+ *
192
+ * These include:
193
+ * - "(session bootstrap)" — synthetic user turn for Google turn-order compliance
194
+ * - Session startup instructions from /new or /reset
195
+ * - "✅ New session started" — AI's ack of session startup (no user-meaningful content)
196
+ * - Pre-compaction memory flush prompts (system-to-agent instructions, not user content)
197
+ * - AI's NO_REPLY ack of memory flush (no user-meaningful content)
198
+ */
199
+ function isFrameworkNoise(text) {
200
+ const t = text.trim();
201
+ // Google turn-order bootstrap placeholder
202
+ if (t === "(session bootstrap)")
203
+ return true;
204
+ // Framework session-reset instruction (starts with "A new session was started via /new or /reset")
205
+ if (t.startsWith("A new session was started via"))
206
+ return true;
207
+ // AI's pure ack of session startup: "✅ New session started · model: ..."
208
+ if (/^✅\s*New session started/.test(t))
209
+ return true;
210
+ // Pre-compaction memory flush prompt injected by the framework as a synthetic
211
+ // user turn. This is an internal system-to-agent instruction, NOT real user
212
+ // content. Capturing it would pollute L0/L1 memories with framework directives.
213
+ if (t.startsWith("Pre-compaction memory flush"))
214
+ return true;
215
+ // AI's NO_REPLY response to memory flush (or other silent-reply scenarios).
216
+ // A bare "NO_REPLY" (with optional whitespace) carries no user-meaningful content.
217
+ if (/^NO_REPLY\s*$/.test(t))
218
+ return true;
219
+ return false;
220
+ }
221
+ /**
222
+ * Pick up to `max` recent unique texts.
223
+ */
224
+ export function pickRecentUnique(texts, max) {
225
+ const seen = new Set();
226
+ const result = [];
227
+ for (let i = texts.length - 1; i >= 0 && result.length < max; i--) {
228
+ const t = texts[i];
229
+ if (!seen.has(t)) {
230
+ seen.add(t);
231
+ result.push(t);
232
+ }
233
+ }
234
+ return result.reverse();
235
+ }
236
+ // ============================
237
+ // LLM Safety Utilities
238
+ // ============================
239
+ /**
240
+ * Escape XML-like tags in text to prevent tag injection attacks.
241
+ *
242
+ * When memory content or persona text is injected into XML-delimited sections
243
+ * (e.g. `<user-persona>...</user-persona>`), a malicious user could craft content
244
+ * containing `</user-persona>` to break out of the section boundary.
245
+ *
246
+ * This function escapes `<` and `>` in known dangerous patterns (closing tags
247
+ * that match our injection boundaries) so the content cannot prematurely close
248
+ * the XML section.
249
+ */
250
+ export function escapeXmlTags(text) {
251
+ // Escape closing tags that match our injection section boundaries
252
+ return text.replace(/<\/?(?:user-persona|relevant-memories|scene-navigation|relevant-scenes|memory-tools-guide|system|assistant)>/gi, (match) => match.replace(/</g, "&lt;").replace(/>/g, "&gt;"));
253
+ }
254
+ // ============================
255
+ // JSON Sanitization for LLM Output
256
+ // ============================
257
+ /**
258
+ * Sanitize a raw JSON string from LLM output so that `JSON.parse` won't throw
259
+ * "Bad control character in string literal".
260
+ *
261
+ * Per RFC 8259 §7, U+0000–U+001F MUST be escaped inside JSON string literals.
262
+ * LLMs sometimes produce unescaped control characters (raw newlines, tabs, etc.)
263
+ * inside string values.
264
+ *
265
+ * Strategy (two-phase):
266
+ * 1. **Precise pass** — walk through JSON string literals (delimited by `"`)
267
+ * and escape any unescaped U+0000–U+001F inside them to `\uXXXX` form,
268
+ * while leaving structural whitespace (between values) untouched.
269
+ * 2. **Fallback** — if the precise pass still fails `JSON.parse`, fall back to
270
+ * a simple global strip of rare control chars (\x00–\x08, \x0b, \x0c,
271
+ * \x0e–\x1f) which are almost never meaningful in natural-language content.
272
+ */
273
+ export function sanitizeJsonForParse(raw) {
274
+ // Phase 1: Escape control characters inside JSON string literals.
275
+ // We walk the string character-by-character to properly handle escape sequences.
276
+ const escaped = escapeControlCharsInJsonStrings(raw);
277
+ try {
278
+ JSON.parse(escaped);
279
+ return escaped;
280
+ }
281
+ catch {
282
+ // Phase 1 didn't fully fix it — fall through to phase 2
283
+ }
284
+ // Phase 2: Brute-force strip of rare control chars that have no textual meaning.
285
+ // Preserves \t (\x09), \n (\x0a), \r (\x0d) which are common structural whitespace.
286
+ // NOTE: We strip from `escaped` (Phase 1 result) rather than `raw`, so that any
287
+ // control-character escaping Phase 1 performed is preserved even when the JSON has
288
+ // other issues (e.g. trailing commas) that cause the Phase 1 parse to fail.
289
+ const stripped = escaped.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "");
290
+ return stripped;
291
+ }
292
+ /**
293
+ * Walk through a JSON text and escape U+0000–U+001F control characters that
294
+ * appear *inside* JSON string literals (between unescaped `"` delimiters).
295
+ *
296
+ * Characters that already have short escape sequences (\n, \r, \t, \b, \f)
297
+ * are mapped to those; others become \uXXXX.
298
+ *
299
+ * Structural whitespace outside string literals is left untouched.
300
+ */
301
+ function escapeControlCharsInJsonStrings(text) {
302
+ const SHORT_ESCAPES = {
303
+ 0x08: "\\b", // backspace
304
+ 0x09: "\\t", // tab
305
+ 0x0a: "\\n", // line feed
306
+ 0x0c: "\\f", // form feed
307
+ 0x0d: "\\r", // carriage return
308
+ };
309
+ const out = [];
310
+ let inString = false;
311
+ let i = 0;
312
+ while (i < text.length) {
313
+ const ch = text[i];
314
+ const code = ch.charCodeAt(0);
315
+ if (inString) {
316
+ if (ch === "\\" && i + 1 < text.length) {
317
+ // Already-escaped sequence — copy both characters verbatim
318
+ out.push(ch, text[i + 1]);
319
+ i += 2;
320
+ continue;
321
+ }
322
+ if (ch === '"') {
323
+ // End of string literal
324
+ out.push(ch);
325
+ inString = false;
326
+ i++;
327
+ continue;
328
+ }
329
+ if (code <= 0x1f) {
330
+ // Unescaped control character inside string — escape it
331
+ const short = SHORT_ESCAPES[code];
332
+ if (short) {
333
+ out.push(short);
334
+ }
335
+ else {
336
+ out.push("\\u" + code.toString(16).padStart(4, "0"));
337
+ }
338
+ i++;
339
+ continue;
340
+ }
341
+ // Normal character inside string
342
+ out.push(ch);
343
+ i++;
344
+ }
345
+ else {
346
+ // Outside string literal
347
+ if (ch === '"') {
348
+ out.push(ch);
349
+ inString = true;
350
+ i++;
351
+ continue;
352
+ }
353
+ // Structural character (including whitespace) — pass through
354
+ out.push(ch);
355
+ i++;
356
+ }
357
+ }
358
+ return out.join("");
359
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@team-harness/memory-algorithms",
3
+ "version": "0.1.0",
4
+ "description": "Host-independent L1/L2/L3 and Skill extraction derived from TencentDB-Agent-Memory",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
8
+ "repository": { "type": "git", "url": "git+https://github.com/team-harness/memory-algorithms.git" },
9
+ "engines": { "node": ">=22" },
10
+ "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
11
+ "files": ["dist", "LICENSE", "README.md", "upstream/baseline.json", "upstream/changes.md"],
12
+ "scripts": {
13
+ "build": "tsc -p tsconfig.json",
14
+ "test": "npm run build && node --test tests/*.test.mjs",
15
+ "test:package": "node scripts/verify-package.mjs",
16
+ "test:evaluation": "npm run build && node --test evaluation/long-chain.test.mjs evaluation/driver.test.mjs evaluation/tape.test.mjs evaluation/loader.test.mjs",
17
+ "test:differential": "npm run build && node --import ./evaluation/upstream-loader.mjs --test evaluation/differential.test.mjs",
18
+ "eval:live": "npm run build && node evaluation/live.mjs --live",
19
+ "eval:paired": "npm run build && MEMORY_PARITY_DETERMINISTIC=1 node --import ./evaluation/upstream-loader.mjs evaluation/paired.mjs --live",
20
+ "test:paired": "npm run build && MEMORY_PARITY_DETERMINISTIC=1 node --import ./evaluation/upstream-loader.mjs evaluation/paired.mjs",
21
+ "sync:verify": "npm run upstream:check && npm test && npm run test:evaluation && npm run test:differential && npm run test:paired && npm run test:package",
22
+ "sync:drill": "npm run build && node scripts/sync-drill.mjs",
23
+ "upstream:check": "node scripts/upstream-check.mjs",
24
+ "prepack": "npm run build"
25
+ },
26
+ "dependencies": { "ajv": "^8.17.1", "yaml": "^2.8.3" },
27
+ "devDependencies": { "@types/node": "^22.0.0", "typescript": "^5.9.3" }
28
+ }