@intentface/latch-memory 0.9.1

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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +163 -0
  3. package/dist/extraction-key.d.ts +20 -0
  4. package/dist/extraction-key.d.ts.map +1 -0
  5. package/dist/extraction-key.js +43 -0
  6. package/dist/extraction-key.js.map +1 -0
  7. package/dist/index-file.d.ts +77 -0
  8. package/dist/index-file.d.ts.map +1 -0
  9. package/dist/index-file.js +169 -0
  10. package/dist/index-file.js.map +1 -0
  11. package/dist/index.d.ts +23 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +23 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/jobs/types.d.ts +133 -0
  16. package/dist/jobs/types.d.ts.map +1 -0
  17. package/dist/jobs/types.js +12 -0
  18. package/dist/jobs/types.js.map +1 -0
  19. package/dist/prompts/consolidator.d.ts +18 -0
  20. package/dist/prompts/consolidator.d.ts.map +1 -0
  21. package/dist/prompts/consolidator.js +76 -0
  22. package/dist/prompts/consolidator.js.map +1 -0
  23. package/dist/prompts/extractor.d.ts +34 -0
  24. package/dist/prompts/extractor.d.ts.map +1 -0
  25. package/dist/prompts/extractor.js +60 -0
  26. package/dist/prompts/extractor.js.map +1 -0
  27. package/dist/prompts/inject.d.ts +20 -0
  28. package/dist/prompts/inject.d.ts.map +1 -0
  29. package/dist/prompts/inject.js +58 -0
  30. package/dist/prompts/inject.js.map +1 -0
  31. package/dist/provider.d.ts +47 -0
  32. package/dist/provider.d.ts.map +1 -0
  33. package/dist/provider.js +101 -0
  34. package/dist/provider.js.map +1 -0
  35. package/dist/sanitize.d.ts +29 -0
  36. package/dist/sanitize.d.ts.map +1 -0
  37. package/dist/sanitize.js +88 -0
  38. package/dist/sanitize.js.map +1 -0
  39. package/dist/schema.d.ts +60 -0
  40. package/dist/schema.d.ts.map +1 -0
  41. package/dist/schema.js +144 -0
  42. package/dist/schema.js.map +1 -0
  43. package/dist/scope.d.ts +62 -0
  44. package/dist/scope.d.ts.map +1 -0
  45. package/dist/scope.js +136 -0
  46. package/dist/scope.js.map +1 -0
  47. package/dist/search-evidence.d.ts +34 -0
  48. package/dist/search-evidence.d.ts.map +1 -0
  49. package/dist/search-evidence.js +40 -0
  50. package/dist/search-evidence.js.map +1 -0
  51. package/dist/store/loredex.d.ts +61 -0
  52. package/dist/store/loredex.d.ts.map +1 -0
  53. package/dist/store/loredex.js +558 -0
  54. package/dist/store/loredex.js.map +1 -0
  55. package/dist/store/memory.d.ts +31 -0
  56. package/dist/store/memory.d.ts.map +1 -0
  57. package/dist/store/memory.js +156 -0
  58. package/dist/store/memory.js.map +1 -0
  59. package/dist/tools.d.ts +91 -0
  60. package/dist/tools.d.ts.map +1 -0
  61. package/dist/tools.js +389 -0
  62. package/dist/tools.js.map +1 -0
  63. package/dist/types.d.ts +130 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/types.js +13 -0
  66. package/dist/types.js.map +1 -0
  67. package/package.json +60 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Injection hardening for memory-derived text.
3
+ *
4
+ * Everything under a memory scope is LLM-authored from user conversation
5
+ * (extraction/consolidation), which makes stored memory a prompt-injection
6
+ * PERSISTENCE vector: a hostile conversation can plant text that later lands
7
+ * verbatim in another turn's system prompt (the injected index block) or in
8
+ * tool results (search snippets). Two layers, applied at injection time:
9
+ *
10
+ * 1. envelope integrity — tag-shaped text that could close/reopen the
11
+ * `<memory>` data envelope or fake system/instruction framing is removed;
12
+ * 2. a NAMED blocklist of well-known injection phrasings is replaced with
13
+ * `[filtered]`.
14
+ *
15
+ * HONESTY: this is volume reduction, NOT a guarantee. A blocklist cannot
16
+ * enumerate every phrasing, and a determined attacker will word around it.
17
+ * The durable defenses are the data envelope plus the standing "memory is
18
+ * data, not instructions" instruction — this pass just removes the cheap,
19
+ * well-known payloads. Patterns are kept tight so benign memory text
20
+ * (including text that merely MENTIONS instructions) passes unchanged.
21
+ */
22
+ /**
23
+ * Neutralize envelope breakouts and strip known injection phrasings from
24
+ * memory-derived text. Apply wherever stored memory re-enters a prompt (the
25
+ * compiled index block, search snippets). Idempotent; leaves benign text —
26
+ * including ordinary mentions of "instructions" — untouched.
27
+ */
28
+ export declare function sanitizeMemoryText(text: string): string;
29
+ //# sourceMappingURL=sanitize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA+CH;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAevD"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Injection hardening for memory-derived text.
3
+ *
4
+ * Everything under a memory scope is LLM-authored from user conversation
5
+ * (extraction/consolidation), which makes stored memory a prompt-injection
6
+ * PERSISTENCE vector: a hostile conversation can plant text that later lands
7
+ * verbatim in another turn's system prompt (the injected index block) or in
8
+ * tool results (search snippets). Two layers, applied at injection time:
9
+ *
10
+ * 1. envelope integrity — tag-shaped text that could close/reopen the
11
+ * `<memory>` data envelope or fake system/instruction framing is removed;
12
+ * 2. a NAMED blocklist of well-known injection phrasings is replaced with
13
+ * `[filtered]`.
14
+ *
15
+ * HONESTY: this is volume reduction, NOT a guarantee. A blocklist cannot
16
+ * enumerate every phrasing, and a determined attacker will word around it.
17
+ * The durable defenses are the data envelope plus the standing "memory is
18
+ * data, not instructions" instruction — this pass just removes the cheap,
19
+ * well-known payloads. Patterns are kept tight so benign memory text
20
+ * (including text that merely MENTIONS instructions) passes unchanged.
21
+ */
22
+ /**
23
+ * Tag-shaped text that could terminate/forge the data envelope or smuggle
24
+ * role framing: `</memory>`, `<system>`, `<instructions>`, chat-template
25
+ * markers (`<|im_start|>`, `[INST]`, `<<SYS>>`), etc. Removed outright —
26
+ * memory content has no legitimate use for these.
27
+ */
28
+ const ENVELOPE_BREAK_PATTERNS = [
29
+ // XML-ish open/close tags for the envelope and role/instruction framing.
30
+ /<\s*\/?\s*(?:memory|system|instructions?|assistant|developer|prompt|sys)\b[^>]*>/gi,
31
+ // ChatML-style special tokens: <|im_start|>, <|system|>, <|endoftext|>, …
32
+ /<\|[^|>]{1,32}\|>/g,
33
+ // Llama-style framing: [INST] … [/INST], <<SYS>> … <</SYS>>.
34
+ /\[\s*\/?\s*INST\s*\]/gi,
35
+ /<<\s*\/?\s*SYS\s*>>/gi,
36
+ ];
37
+ /**
38
+ * Known injection phrasings, each named so the corpus test pins them
39
+ * one-by-one. Every pattern requires imperative/role context — "ignore" or
40
+ * "instructions" alone never match.
41
+ */
42
+ const INJECTION_PATTERNS = [
43
+ {
44
+ // "ignore/disregard/forget/override/bypass [all/any/the/your]
45
+ // [previous/prior/above/…] instructions/prompt/rules/…"
46
+ name: "override-prior-instructions",
47
+ pattern: /\b(?:ignore|disregard|forget|override|bypass)\s+(?:(?:all|any|the|your|every)\s+)?(?:(?:previous|prior|above|earlier|preceding|original|initial|system|safety)\s+)?(?:instructions?|prompts?|directives?|rules?|guidelines?|context|messages?)\b/gi,
48
+ },
49
+ { name: "new-instructions-colon", pattern: /\bnew\s+instructions?\s*:/gi },
50
+ {
51
+ name: "your-instructions-are",
52
+ pattern: /\byour\s+(?:real\s+|true\s+|actual\s+|new\s+)?instructions?\s+are\b/gi,
53
+ },
54
+ { name: "system-role-claim", pattern: /\bsystem\s*:\s*you\s+are\b/gi },
55
+ { name: "you-are-now", pattern: /\byou\s+are\s+now\b/gi },
56
+ { name: "developer-mode", pattern: /\bdeveloper\s+mode\b/gi },
57
+ { name: "dan-do-anything-now", pattern: /\bdo\s+anything\s+now\b/gi },
58
+ { name: "dan-persona", pattern: /\b(?:you\s+are|act\s+as)\s+DAN\b/gi },
59
+ { name: "dan-mode", pattern: /\bDAN\s+mode\b/g },
60
+ { name: "jailbreak-mode", pattern: /\bjailbreak\s+(?:mode|prompt)\b/gi },
61
+ ];
62
+ const FILTERED = "[filtered]";
63
+ /**
64
+ * Neutralize envelope breakouts and strip known injection phrasings from
65
+ * memory-derived text. Apply wherever stored memory re-enters a prompt (the
66
+ * compiled index block, search snippets). Idempotent; leaves benign text —
67
+ * including ordinary mentions of "instructions" — untouched.
68
+ */
69
+ export function sanitizeMemoryText(text) {
70
+ let out = text;
71
+ // To a fixpoint (bounded): stripping can reassemble a payload from its
72
+ // halves ("<sys<system>tem>" → "<system>"), so one pass is bypassable.
73
+ for (let i = 0; i < 8; i++) {
74
+ const before = out;
75
+ // Tags are replaced with a space, not "": removing them outright would
76
+ // splice their neighbors together, both reassembling split tags and
77
+ // fusing words across a stripped tag so phrase patterns miss them.
78
+ for (const pattern of ENVELOPE_BREAK_PATTERNS)
79
+ out = out.replace(pattern, " ");
80
+ for (const { pattern } of INJECTION_PATTERNS)
81
+ out = out.replace(pattern, FILTERED);
82
+ if (out === before)
83
+ return out;
84
+ }
85
+ // Still churning after the bound — refuse to inject what we can't settle.
86
+ return "(memory text withheld: sanitizer did not converge)";
87
+ }
88
+ //# sourceMappingURL=sanitize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;;GAKG;AACH,MAAM,uBAAuB,GAAsB;IACjD,yEAAyE;IACzE,oFAAoF;IACpF,0EAA0E;IAC1E,oBAAoB;IACpB,6DAA6D;IAC7D,wBAAwB;IACxB,uBAAuB;CACxB,CAAC;AAEF;;;;GAIG;AACH,MAAM,kBAAkB,GAAqD;IAC3E;QACE,8DAA8D;QAC9D,yDAAyD;QACzD,IAAI,EAAE,6BAA6B;QACnC,OAAO,EACL,oPAAoP;KACvP;IACD,EAAE,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,6BAA6B,EAAE;IAC1E;QACE,IAAI,EAAE,uBAAuB;QAC7B,OAAO,EAAE,uEAAuE;KACjF;IACD,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,8BAA8B,EAAE;IACtE,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,uBAAuB,EAAE;IACzD,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,wBAAwB,EAAE;IAC7D,EAAE,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE,2BAA2B,EAAE;IACrE,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,oCAAoC,EAAE;IACtE,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,EAAE;IAChD,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,mCAAmC,EAAE;CACzE,CAAC;AAEF,MAAM,QAAQ,GAAG,YAAY,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,IAAI,GAAG,GAAG,IAAI,CAAC;IACf,uEAAuE;IACvE,uEAAuE;IACvE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,GAAG,CAAC;QACnB,uEAAuE;QACvE,oEAAoE;QACpE,mEAAmE;QACnE,KAAK,MAAM,OAAO,IAAI,uBAAuB;YAAE,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAC/E,KAAK,MAAM,EAAE,OAAO,EAAE,IAAI,kBAAkB;YAAE,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACnF,IAAI,GAAG,KAAK,MAAM;YAAE,OAAO,GAAG,CAAC;IACjC,CAAC;IACD,0EAA0E;IAC1E,OAAO,oDAAoD,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,60 @@
1
+ import type { MemoryScope } from "@intentface/latch-core";
2
+ /**
3
+ * Memory index schemas: markdown outlines (top-level `##` headings, optional
4
+ * description bullets) that steer WHAT the background extractor captures per
5
+ * level and HOW the consolidator organizes that level's MEMORY.md.
6
+ *
7
+ * Schemas are ADVISORY: they enter the extraction/consolidation prompts and
8
+ * `memory_write_index` nudges one self-correction round on sections outside
9
+ * the outline (`schemaSectionWarnings`), but a rewrite is never hard-failed
10
+ * over schema drift — sweeps must not wedge on model non-compliance.
11
+ *
12
+ * Two homes:
13
+ * - agent-held levels (`agent-user`, `agent-org`): `MemoryConfig.schemas`.
14
+ * - the cross-agent `user` level: one shared workspace file per connection at
15
+ * `USER_SCHEMA_PATH` — deliberately OUTSIDE the private root (schemas carry
16
+ * no PII, and an org-wide file must resolve identically for every caller;
17
+ * `__private_root` is caller-relative). User memory DATA stays private.
18
+ */
19
+ export declare const MEMORY_SCHEMA_FILENAME = "SCHEMA.md";
20
+ /**
21
+ * Workspace path of the shared, org-wide `user`-level schema file — anchored
22
+ * at the shared root so it resolves to ONE physical file for every caller
23
+ * (see LOREDEX_SHARED_ROOT for the caller-relative resolution hazard).
24
+ */
25
+ export declare const USER_SCHEMA_PATH = "__shared_root/_memory/_user/SCHEMA.md";
26
+ /**
27
+ * The non-negotiable per-level policy floor. Always present in extraction and
28
+ * consolidation prompts, independent of (and above) any configured schema —
29
+ * editing or deleting a schema can never relax it.
30
+ */
31
+ export declare const MEMORY_POLICY_FLOOR: Record<MemoryScope, string>;
32
+ /** Fallback outlines used whenever a level has no configured schema. */
33
+ export declare const DEFAULT_MEMORY_SCHEMAS: Record<MemoryScope, string>;
34
+ /** The section outline of a schema: its top-level `##` headings, in order. */
35
+ export declare function schemaSections(schema: string): string[];
36
+ /**
37
+ * Short stable fingerprint of one schema's normalized text (trailing
38
+ * whitespace and blank-line runs ignored). `"0"` for undefined/blank —
39
+ * "no schema" fingerprints identically everywhere.
40
+ */
41
+ export declare function schemaFingerprint(schema: string | undefined): string;
42
+ /**
43
+ * Combined fingerprint over every participating level and its schema. Folded
44
+ * into extraction idempotency keys: editing any level's schema — or adding/
45
+ * removing a level — changes the key, so tombstoned windows become
46
+ * re-extractable under the new configuration. Order-independent (levels are
47
+ * sorted) so callers need not care about target ordering.
48
+ */
49
+ export declare function schemasFingerprint(targets: ReadonlyArray<{
50
+ level: MemoryScope;
51
+ schema: string | undefined;
52
+ }>): string;
53
+ /**
54
+ * Advisory schema check for an index rewrite: top-level `##` sections in
55
+ * `content` that are neither in the schema's outline nor `## Archive`
56
+ * (case-insensitive). Returns problem strings for the tool's self-correction
57
+ * loop — NEVER used to hard-fail a write.
58
+ */
59
+ export declare function schemaSectionWarnings(schema: string, content: string): string[];
60
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAG1D;;;;;;;;;;;;;;;;GAgBG;AAEH,eAAO,MAAM,sBAAsB,cAAc,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,0CAAiF,CAAC;AAE/G;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAY3D,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CA2B9D,CAAC;AAEF,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAOvD;AAcD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CASpE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,aAAa,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC,GACzE,MAAM,CAMR;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAgB/E"}
package/dist/schema.js ADDED
@@ -0,0 +1,144 @@
1
+ import { LOREDEX_SHARED_ROOT, MEMORY_ROOT_PREFIX } from "./scope.js";
2
+ /**
3
+ * Memory index schemas: markdown outlines (top-level `##` headings, optional
4
+ * description bullets) that steer WHAT the background extractor captures per
5
+ * level and HOW the consolidator organizes that level's MEMORY.md.
6
+ *
7
+ * Schemas are ADVISORY: they enter the extraction/consolidation prompts and
8
+ * `memory_write_index` nudges one self-correction round on sections outside
9
+ * the outline (`schemaSectionWarnings`), but a rewrite is never hard-failed
10
+ * over schema drift — sweeps must not wedge on model non-compliance.
11
+ *
12
+ * Two homes:
13
+ * - agent-held levels (`agent-user`, `agent-org`): `MemoryConfig.schemas`.
14
+ * - the cross-agent `user` level: one shared workspace file per connection at
15
+ * `USER_SCHEMA_PATH` — deliberately OUTSIDE the private root (schemas carry
16
+ * no PII, and an org-wide file must resolve identically for every caller;
17
+ * `__private_root` is caller-relative). User memory DATA stays private.
18
+ */
19
+ export const MEMORY_SCHEMA_FILENAME = "SCHEMA.md";
20
+ /**
21
+ * Workspace path of the shared, org-wide `user`-level schema file — anchored
22
+ * at the shared root so it resolves to ONE physical file for every caller
23
+ * (see LOREDEX_SHARED_ROOT for the caller-relative resolution hazard).
24
+ */
25
+ export const USER_SCHEMA_PATH = `${LOREDEX_SHARED_ROOT}/${MEMORY_ROOT_PREFIX}/_user/${MEMORY_SCHEMA_FILENAME}`;
26
+ /**
27
+ * The non-negotiable per-level policy floor. Always present in extraction and
28
+ * consolidation prompts, independent of (and above) any configured schema —
29
+ * editing or deleting a schema can never relax it.
30
+ */
31
+ export const MEMORY_POLICY_FLOOR = {
32
+ "agent-user": "Private per-user memory: personal detail relevant to this agent is allowed. " +
33
+ "NEVER store credentials, secrets, tokens, one-time codes, or full account/ID numbers.",
34
+ "agent-org": "Organization-shared memory: NO personally identifying information and NO facts " +
35
+ "attributed to named or identifiable individuals — store aggregate patterns, team-level " +
36
+ "observations, process decisions, and org-level facts only.",
37
+ user: "Cross-agent user profile: profile-level facts about the user only (identity, stable " +
38
+ "preferences, expertise, life/work context) — no agent-domain specifics, and never " +
39
+ "credentials, secrets, or account numbers.",
40
+ };
41
+ /** Fallback outlines used whenever a level has no configured schema. */
42
+ export const DEFAULT_MEMORY_SCHEMAS = {
43
+ "agent-user": [
44
+ "## Identity & preferences",
45
+ "- Who the user is to this agent; how they like to work with it",
46
+ "## Projects & context",
47
+ "- Ongoing work, goals, and constraints that persist across sessions",
48
+ "## Working agreements",
49
+ "- Decisions, commitments, and standing instructions",
50
+ "## Open loops",
51
+ "- Unfinished threads future sessions must pick up",
52
+ ].join("\n"),
53
+ "agent-org": [
54
+ "## Team patterns",
55
+ "- Aggregate observations about how teams work and respond (no individuals)",
56
+ "## Process decisions",
57
+ "- Org-level decisions and conventions this agent should honor",
58
+ "## Recurring topics",
59
+ "- Themes that keep coming up across the organization",
60
+ ].join("\n"),
61
+ user: [
62
+ "## Profile",
63
+ "- Who the user is: role, background, expertise level",
64
+ "## Preferences",
65
+ "- Stable cross-agent preferences (tone, language, formats)",
66
+ "## Life & work context",
67
+ "- Durable context any of the user's agents may need",
68
+ ].join("\n"),
69
+ };
70
+ /** The section outline of a schema: its top-level `##` headings, in order. */
71
+ export function schemaSections(schema) {
72
+ const out = [];
73
+ for (const line of schema.split("\n")) {
74
+ const m = /^##(?!#)\s*(.+?)\s*$/.exec(line.trim());
75
+ if (m && m[1])
76
+ out.push(m[1]);
77
+ }
78
+ return out;
79
+ }
80
+ /** FNV-1a 64-bit, hex — deterministic, dependency-free (mirrors extraction-key). */
81
+ function fnv1a64(input) {
82
+ let h = 0xcbf29ce484222325n;
83
+ const prime = 0x100000001b3n;
84
+ const mask = 0xffffffffffffffffn;
85
+ for (let i = 0; i < input.length; i++) {
86
+ h ^= BigInt(input.charCodeAt(i));
87
+ h = (h * prime) & mask;
88
+ }
89
+ return h.toString(16).padStart(16, "0");
90
+ }
91
+ /**
92
+ * Short stable fingerprint of one schema's normalized text (trailing
93
+ * whitespace and blank-line runs ignored). `"0"` for undefined/blank —
94
+ * "no schema" fingerprints identically everywhere.
95
+ */
96
+ export function schemaFingerprint(schema) {
97
+ const normalized = (schema ?? "")
98
+ .split("\n")
99
+ .map((l) => l.trimEnd())
100
+ .join("\n")
101
+ .replace(/\n{2,}/g, "\n")
102
+ .trim();
103
+ if (!normalized)
104
+ return "0";
105
+ return fnv1a64(normalized);
106
+ }
107
+ /**
108
+ * Combined fingerprint over every participating level and its schema. Folded
109
+ * into extraction idempotency keys: editing any level's schema — or adding/
110
+ * removing a level — changes the key, so tombstoned windows become
111
+ * re-extractable under the new configuration. Order-independent (levels are
112
+ * sorted) so callers need not care about target ordering.
113
+ */
114
+ export function schemasFingerprint(targets) {
115
+ const parts = targets
116
+ .map((t) => `${t.level}:${schemaFingerprint(t.schema)}`)
117
+ .sort()
118
+ .join("|");
119
+ return fnv1a64(parts);
120
+ }
121
+ /**
122
+ * Advisory schema check for an index rewrite: top-level `##` sections in
123
+ * `content` that are neither in the schema's outline nor `## Archive`
124
+ * (case-insensitive). Returns problem strings for the tool's self-correction
125
+ * loop — NEVER used to hard-fail a write.
126
+ */
127
+ export function schemaSectionWarnings(schema, content) {
128
+ const allowed = new Set(schemaSections(schema).map((s) => s.toLowerCase()));
129
+ allowed.add("archive");
130
+ if (allowed.size <= 1)
131
+ return []; // no outline configured — nothing to check
132
+ const outline = schemaSections(schema)
133
+ .map((s) => `## ${s}`)
134
+ .join(", ");
135
+ const problems = [];
136
+ for (const section of schemaSections(content)) {
137
+ if (allowed.has(section.toLowerCase()))
138
+ continue;
139
+ problems.push(`unexpected top-level section "## ${section}" — this index's schema sections are: ` +
140
+ `${outline} (plus ## Archive); move its entries under one of those`);
141
+ }
142
+ return problems;
143
+ }
144
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import type { MemoryConfig, MemoryScope } from "@intentface/latch-core";
2
+ import type { ScopeRef } from "./types.js";
3
+ export declare const MEMORY_ROOT_PREFIX = "_memory";
4
+ /**
5
+ * Loredex's per-user private root folder (`is_root=1`, `visibility='private'`,
6
+ * owner-only ACL). Every workspace member has exactly one; the server creates
7
+ * it on join (`ensureUserPrivateRoot`), so it always exists and is never
8
+ * created by us. Folders created under it inherit `private` visibility, which
9
+ * is what makes per-user memory scopes actually private — unlike the shared
10
+ * `_memory/**` tree, where per-user separation is only a folder convention.
11
+ *
12
+ * The path segment is caller-relative: the same literal resolves to a
13
+ * DIFFERENT physical folder per authenticated user. Scope roots under it keep
14
+ * their `u-<id>` leaf anyway — see `scopeRefOf` for why.
15
+ */
16
+ export declare const LOREDEX_PRIVATE_ROOT = "__private_root";
17
+ /**
18
+ * Loredex's workspace-shared root folder (`is_root=1`, `visibility='shared'`,
19
+ * unowned). Exactly one per workspace, provisioned by the server, rendered as
20
+ * "Shared" in the UI — never created by us.
21
+ *
22
+ * Shared memory paths MUST anchor here. Loredex resolves bare path heads
23
+ * caller-relatively (any accessible folder of that name, oldest first), so an
24
+ * unanchored `_memory/...` can silently resolve to the CALLER'S OWN
25
+ * `__private_root/_memory` — fragmenting "org" memory into per-user private
26
+ * copies with no visible error (each user reads their own copy back
27
+ * consistently). Found live 2026-08-13; anchoring the head is the fix on our
28
+ * side of the seam.
29
+ */
30
+ export declare const LOREDEX_SHARED_ROOT = "__shared_root";
31
+ /**
32
+ * Boundary-aware containment check for workspace paths: `path` must sit
33
+ * strictly under `root`, segment-by-segment, with no `.`/`..`/empty segments —
34
+ * so `root/../elsewhere` can't escape and `${root}-evil/x` can't prefix-match.
35
+ */
36
+ export declare function isUnderScopeRoot(root: string, path: string): boolean;
37
+ /**
38
+ * Resolve the scope folder for a caller, or undefined when the scope needs a
39
+ * user but the principal carries none (e.g. an org-only principal hitting a
40
+ * per-user scope) — memory is simply unavailable for that turn.
41
+ */
42
+ export declare function scopeRefOf(args: {
43
+ agent: string;
44
+ memory: MemoryConfig;
45
+ userId?: string;
46
+ }): ScopeRef | undefined;
47
+ /**
48
+ * The levels this agent's memory participates in: `extractTo` normalized —
49
+ * deduplicated, primary scope first, and always containing the primary scope
50
+ * whether or not the stored config lists it. Every multi-level reader
51
+ * (provider injection/search, sweep extraction) derives its level set here so
52
+ * a denormalized stored config can't split behavior.
53
+ */
54
+ export declare function extractionLevelsOf(memory: MemoryConfig): MemoryScope[];
55
+ /**
56
+ * The per-level view of a memory config: same connection and tuning, `scope`
57
+ * swapped to `level` — so `scopeRefOf`/`scopeLabelOf` work per level for free.
58
+ */
59
+ export declare function levelConfigOf(memory: MemoryConfig, level: MemoryScope): MemoryConfig;
60
+ /** Human-readable scope label for prompts and UI. */
61
+ export declare function scopeLabelOf(memory: MemoryConfig): string;
62
+ //# sourceMappingURL=scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../src/scope.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAmC3C,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAE5C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,mBAAmB,CAAC;AAErD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAMpE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,QAAQ,GAAG,SAAS,CAuBvB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW,EAAE,CAMtE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,GAAG,YAAY,CAEpF;AAED,qDAAqD;AACrD,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CASzD"}
package/dist/scope.js ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Scope derivation: pure functions from (agent, memory config, caller
3
+ * identity) to the folder a store lives in. Consolidation and search never
4
+ * cross a scope boundary, so everything downstream keys off `ScopeRef.key`.
5
+ */
6
+ /** FNV-1a 32-bit, hex — a short stable fingerprint (no crypto dependency). */
7
+ function fnv1a(input) {
8
+ let h = 0x811c9dc5;
9
+ for (let i = 0; i < input.length; i++) {
10
+ h ^= input.charCodeAt(i);
11
+ h = Math.imul(h, 0x01000193);
12
+ }
13
+ return (h >>> 0).toString(16).padStart(8, "0");
14
+ }
15
+ /**
16
+ * Keep path segments filesystem/URL-safe and boundary-proof (no `/` or `..`).
17
+ * Sanitization is many-to-one ("a/b" and "a-b" would collapse), and `key` is
18
+ * the storage boundary — so whenever cleaning changed ANYTHING, a short hash
19
+ * of the raw id is appended to keep distinct ids distinct. Already-clean ids
20
+ * (the normal case: kebab agent names, alphanumeric user ids) pass through
21
+ * untouched, so existing scope roots are stable.
22
+ */
23
+ function seg(raw) {
24
+ const cleaned = raw
25
+ .replace(/[^a-zA-Z0-9_-]/g, "-")
26
+ .replace(/-{2,}/g, "-")
27
+ .replace(/^-+|-+$/g, "");
28
+ if (cleaned === raw && cleaned)
29
+ return cleaned;
30
+ return `${cleaned || "x"}-${fnv1a(raw)}`;
31
+ }
32
+ export const MEMORY_ROOT_PREFIX = "_memory";
33
+ /**
34
+ * Loredex's per-user private root folder (`is_root=1`, `visibility='private'`,
35
+ * owner-only ACL). Every workspace member has exactly one; the server creates
36
+ * it on join (`ensureUserPrivateRoot`), so it always exists and is never
37
+ * created by us. Folders created under it inherit `private` visibility, which
38
+ * is what makes per-user memory scopes actually private — unlike the shared
39
+ * `_memory/**` tree, where per-user separation is only a folder convention.
40
+ *
41
+ * The path segment is caller-relative: the same literal resolves to a
42
+ * DIFFERENT physical folder per authenticated user. Scope roots under it keep
43
+ * their `u-<id>` leaf anyway — see `scopeRefOf` for why.
44
+ */
45
+ export const LOREDEX_PRIVATE_ROOT = "__private_root";
46
+ /**
47
+ * Loredex's workspace-shared root folder (`is_root=1`, `visibility='shared'`,
48
+ * unowned). Exactly one per workspace, provisioned by the server, rendered as
49
+ * "Shared" in the UI — never created by us.
50
+ *
51
+ * Shared memory paths MUST anchor here. Loredex resolves bare path heads
52
+ * caller-relatively (any accessible folder of that name, oldest first), so an
53
+ * unanchored `_memory/...` can silently resolve to the CALLER'S OWN
54
+ * `__private_root/_memory` — fragmenting "org" memory into per-user private
55
+ * copies with no visible error (each user reads their own copy back
56
+ * consistently). Found live 2026-08-13; anchoring the head is the fix on our
57
+ * side of the seam.
58
+ */
59
+ export const LOREDEX_SHARED_ROOT = "__shared_root";
60
+ /**
61
+ * Boundary-aware containment check for workspace paths: `path` must sit
62
+ * strictly under `root`, segment-by-segment, with no `.`/`..`/empty segments —
63
+ * so `root/../elsewhere` can't escape and `${root}-evil/x` can't prefix-match.
64
+ */
65
+ export function isUnderScopeRoot(root, path) {
66
+ const rootParts = root.split("/").filter(Boolean);
67
+ const parts = path.split("/");
68
+ if (parts.some((p) => p === "" || p === "." || p === ".."))
69
+ return false;
70
+ if (parts.length <= rootParts.length)
71
+ return false;
72
+ return rootParts.every((p, i) => parts[i] === p);
73
+ }
74
+ /**
75
+ * Resolve the scope folder for a caller, or undefined when the scope needs a
76
+ * user but the principal carries none (e.g. an org-only principal hitting a
77
+ * per-user scope) — memory is simply unavailable for that turn.
78
+ */
79
+ export function scopeRefOf(args) {
80
+ const { agent, memory, userId } = args;
81
+ const needsUser = memory.scope === "agent-user" || memory.scope === "user";
82
+ if (needsUser && !userId)
83
+ return undefined;
84
+ // Per-user scopes are stored under the caller's PRIVATE root so only that
85
+ // user (and connections acting as them) can read them. The layout mirrors
86
+ // the shared tree (`<agent>` per agent, the literal `_user` for the
87
+ // cross-agent scope) so subtrees stay disjoint (hard boundary). The
88
+ // `u-<id>` leaf is kept even though the private root is already per-user:
89
+ // it makes the root globally unique (the key stays `${connection}:${root}`)
90
+ // and, on a workspace-SHARED connection — where every caller resolves the
91
+ // connection identity's one private root — it keeps users' stores from
92
+ // merging into a single MEMORY.md.
93
+ const root = memory.scope === "agent-user"
94
+ ? `${LOREDEX_PRIVATE_ROOT}/${MEMORY_ROOT_PREFIX}/${seg(agent)}/u-${seg(userId)}`
95
+ : memory.scope === "agent-org"
96
+ ? // Anchored at the shared root — an unanchored `_memory/...` head
97
+ // resolves caller-relatively and can land in the caller's private
98
+ // tree (see LOREDEX_SHARED_ROOT).
99
+ `${LOREDEX_SHARED_ROOT}/${MEMORY_ROOT_PREFIX}/${seg(agent)}/org`
100
+ : `${LOREDEX_PRIVATE_ROOT}/${MEMORY_ROOT_PREFIX}/_user/u-${seg(userId)}`;
101
+ return { key: `${memory.connection}:${root}`, connection: memory.connection, root };
102
+ }
103
+ /**
104
+ * The levels this agent's memory participates in: `extractTo` normalized —
105
+ * deduplicated, primary scope first, and always containing the primary scope
106
+ * whether or not the stored config lists it. Every multi-level reader
107
+ * (provider injection/search, sweep extraction) derives its level set here so
108
+ * a denormalized stored config can't split behavior.
109
+ */
110
+ export function extractionLevelsOf(memory) {
111
+ const out = [memory.scope];
112
+ for (const level of memory.extractTo ?? []) {
113
+ if (!out.includes(level))
114
+ out.push(level);
115
+ }
116
+ return out;
117
+ }
118
+ /**
119
+ * The per-level view of a memory config: same connection and tuning, `scope`
120
+ * swapped to `level` — so `scopeRefOf`/`scopeLabelOf` work per level for free.
121
+ */
122
+ export function levelConfigOf(memory, level) {
123
+ return level === memory.scope ? memory : { ...memory, scope: level };
124
+ }
125
+ /** Human-readable scope label for prompts and UI. */
126
+ export function scopeLabelOf(memory) {
127
+ switch (memory.scope) {
128
+ case "agent-user":
129
+ return "this agent's private memory of this user";
130
+ case "agent-org":
131
+ return "this agent's shared memory for the whole organization";
132
+ case "user":
133
+ return "this user's private memory, shared across their agents";
134
+ }
135
+ }
136
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.js","sourceRoot":"","sources":["../src/scope.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { IndexEntry } from "./index-file.js";
2
+ /**
3
+ * Match evidence for `memory_search` results: WHY a result matched, so the
4
+ * agent can judge whether a memory already exists before creating one with
5
+ * `memory_save`.
6
+ *
7
+ * Index entries have no separate "name" field — an entry's `aka:` alias list
8
+ * IS its set of names — so the spec's name-exact/alias-exact collapse into
9
+ * one `alias-exact` level. The one-line fact text plays the "description"
10
+ * role; anything the backend search surfaced without index evidence is
11
+ * `body`.
12
+ */
13
+ export type MatchEvidence = "alias-exact" | "description" | "body";
14
+ /** How safe it is to CREATE a new memory for the searched subject. */
15
+ export type CreateSafety = "exists" | "probable" | "unknown";
16
+ /** Strongest-first ranking for strongest-signal-wins merging. */
17
+ export declare const EVIDENCE_RANK: Record<MatchEvidence, number>;
18
+ /** Normalize for matching: NFKC, lowercase, collapsed whitespace. */
19
+ export declare function normalizeForMatch(text: string): string;
20
+ /**
21
+ * Strongest evidence one ACTIVE index entry offers for `query`, or undefined
22
+ * when it offers none. Exact (normalized) equality against an alias is the
23
+ * strongest signal; a substring hit in the entry's fact text is descriptive
24
+ * evidence. Very short queries (< 3 chars) skip the substring check — they
25
+ * match everything and prove nothing.
26
+ */
27
+ export declare function entryEvidence(query: string, entry: Pick<IndexEntry, "aliases" | "text">): Exclude<MatchEvidence, "body"> | undefined;
28
+ /**
29
+ * Derive the response-level `create_safety` from all result evidence:
30
+ * an exact alias match proves the memory exists; descriptive evidence makes
31
+ * it probable; body-only hits (or none at all) prove nothing either way.
32
+ */
33
+ export declare function createSafetyOf(evidence: readonly MatchEvidence[]): CreateSafety;
34
+ //# sourceMappingURL=search-evidence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-evidence.d.ts","sourceRoot":"","sources":["../src/search-evidence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,aAAa,GAAG,MAAM,CAAC;AAEnE,sEAAsE;AACtE,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAE7D,iEAAiE;AACjE,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAIvD,CAAC;AAEF,qEAAqE;AACrE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,GAAG,MAAM,CAAC,GAC1C,OAAO,CAAC,aAAa,EAAE,MAAM,CAAC,GAAG,SAAS,CAM5C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,YAAY,CAI/E"}
@@ -0,0 +1,40 @@
1
+ /** Strongest-first ranking for strongest-signal-wins merging. */
2
+ export const EVIDENCE_RANK = {
3
+ "alias-exact": 3,
4
+ description: 2,
5
+ body: 1,
6
+ };
7
+ /** Normalize for matching: NFKC, lowercase, collapsed whitespace. */
8
+ export function normalizeForMatch(text) {
9
+ return text.normalize("NFKC").toLowerCase().replace(/\s+/g, " ").trim();
10
+ }
11
+ /**
12
+ * Strongest evidence one ACTIVE index entry offers for `query`, or undefined
13
+ * when it offers none. Exact (normalized) equality against an alias is the
14
+ * strongest signal; a substring hit in the entry's fact text is descriptive
15
+ * evidence. Very short queries (< 3 chars) skip the substring check — they
16
+ * match everything and prove nothing.
17
+ */
18
+ export function entryEvidence(query, entry) {
19
+ const q = normalizeForMatch(query);
20
+ if (!q)
21
+ return undefined;
22
+ if (entry.aliases.some((a) => normalizeForMatch(a) === q))
23
+ return "alias-exact";
24
+ if (q.length >= 3 && normalizeForMatch(entry.text).includes(q))
25
+ return "description";
26
+ return undefined;
27
+ }
28
+ /**
29
+ * Derive the response-level `create_safety` from all result evidence:
30
+ * an exact alias match proves the memory exists; descriptive evidence makes
31
+ * it probable; body-only hits (or none at all) prove nothing either way.
32
+ */
33
+ export function createSafetyOf(evidence) {
34
+ if (evidence.includes("alias-exact"))
35
+ return "exists";
36
+ if (evidence.includes("description"))
37
+ return "probable";
38
+ return "unknown";
39
+ }
40
+ //# sourceMappingURL=search-evidence.js.map