@hilbras/remembra 3.1.0 → 3.3.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/dist/types.d.ts CHANGED
@@ -2,10 +2,15 @@ import { z } from "zod";
2
2
  /** The four memory types Remembra stores. */
3
3
  export declare const MemoryType: z.ZodEnum<["fact", "decision", "role", "history"]>;
4
4
  export type MemoryType = z.infer<typeof MemoryType>;
5
+ /**
6
+ * Frontmatter schema version. Bump when the Memory format changes and
7
+ * add a migration step in store.ts (missing field in old files = v1).
8
+ */
9
+ export declare const SCHEMA_VERSION = 1;
5
10
  /**
6
11
  * Scope of a memory.
7
12
  * - "global": always relevant (user preferences, roles, general facts)
8
- * - any other string: project/workspace scope (e.g. a repo path or project id)
13
+ * - any other string: project/workspace scope (e.g. a project path or project id)
9
14
  */
10
15
  export type MemoryScope = string;
11
16
  export interface Memory {
@@ -31,10 +36,18 @@ export interface Memory {
31
36
  * `/home/user/project` are fine — path.join keeps them contained.
32
37
  */
33
38
  export declare function isSafeScope(scope: string): boolean;
34
- export declare const StoreInput: z.ZodObject<{
39
+ export declare const storeInputShape: {
40
+ type: z.ZodEnum<["fact", "decision", "role", "history"]>;
41
+ content: z.ZodString;
42
+ scope: z.ZodDefault<z.ZodString>;
43
+ tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
44
+ importance: z.ZodDefault<z.ZodNumber>;
45
+ source: z.ZodOptional<z.ZodString>;
46
+ };
47
+ export declare const StoreInput: z.ZodEffects<z.ZodObject<{
35
48
  type: z.ZodEnum<["fact", "decision", "role", "history"]>;
36
49
  content: z.ZodString;
37
- scope: z.ZodEffects<z.ZodDefault<z.ZodString>, string, string | undefined>;
50
+ scope: z.ZodDefault<z.ZodString>;
38
51
  tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
39
52
  importance: z.ZodDefault<z.ZodNumber>;
40
53
  source: z.ZodOptional<z.ZodString>;
@@ -52,11 +65,30 @@ export declare const StoreInput: z.ZodObject<{
52
65
  tags?: string[] | undefined;
53
66
  importance?: number | undefined;
54
67
  source?: string | undefined;
68
+ }>, {
69
+ type: "fact" | "decision" | "role" | "history";
70
+ content: string;
71
+ scope: string;
72
+ tags: string[];
73
+ importance: number;
74
+ source?: string | undefined;
75
+ }, {
76
+ type: "fact" | "decision" | "role" | "history";
77
+ content: string;
78
+ scope?: string | undefined;
79
+ tags?: string[] | undefined;
80
+ importance?: number | undefined;
81
+ source?: string | undefined;
55
82
  }>;
56
83
  export type StoreInput = z.infer<typeof StoreInput>;
57
- export declare const DigestInput: z.ZodObject<{
84
+ export declare const digestInputShape: {
85
+ transcript: z.ZodString;
86
+ scope: z.ZodOptional<z.ZodString>;
87
+ source: z.ZodOptional<z.ZodString>;
88
+ };
89
+ export declare const DigestInput: z.ZodEffects<z.ZodObject<{
58
90
  transcript: z.ZodString;
59
- scope: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
91
+ scope: z.ZodOptional<z.ZodString>;
60
92
  source: z.ZodOptional<z.ZodString>;
61
93
  }, "strip", z.ZodTypeAny, {
62
94
  transcript: string;
@@ -66,11 +98,157 @@ export declare const DigestInput: z.ZodObject<{
66
98
  transcript: string;
67
99
  scope?: string | undefined;
68
100
  source?: string | undefined;
101
+ }>, {
102
+ transcript: string;
103
+ scope?: string | undefined;
104
+ source?: string | undefined;
105
+ }, {
106
+ transcript: string;
107
+ scope?: string | undefined;
108
+ source?: string | undefined;
69
109
  }>;
70
110
  export type DigestInput = z.infer<typeof DigestInput>;
111
+ export declare const searchInputShape: {
112
+ query: z.ZodOptional<z.ZodString>;
113
+ scope: z.ZodOptional<z.ZodString>;
114
+ type: z.ZodOptional<z.ZodEnum<["fact", "decision", "role", "history"]>>;
115
+ limit: z.ZodOptional<z.ZodNumber>;
116
+ };
117
+ export declare const SearchInput: z.ZodObject<{
118
+ query: z.ZodOptional<z.ZodString>;
119
+ scope: z.ZodOptional<z.ZodString>;
120
+ type: z.ZodOptional<z.ZodEnum<["fact", "decision", "role", "history"]>>;
121
+ limit: z.ZodOptional<z.ZodNumber>;
122
+ }, "strip", z.ZodTypeAny, {
123
+ type?: "fact" | "decision" | "role" | "history" | undefined;
124
+ scope?: string | undefined;
125
+ query?: string | undefined;
126
+ limit?: number | undefined;
127
+ }, {
128
+ type?: "fact" | "decision" | "role" | "history" | undefined;
129
+ scope?: string | undefined;
130
+ query?: string | undefined;
131
+ limit?: number | undefined;
132
+ }>;
133
+ export type SearchInput = z.infer<typeof SearchInput>;
134
+ export declare const listInputShape: {
135
+ scope: z.ZodOptional<z.ZodString>;
136
+ type: z.ZodOptional<z.ZodEnum<["fact", "decision", "role", "history"]>>;
137
+ includeArchived: z.ZodOptional<z.ZodBoolean>;
138
+ };
139
+ export declare const ListInput: z.ZodObject<{
140
+ scope: z.ZodOptional<z.ZodString>;
141
+ type: z.ZodOptional<z.ZodEnum<["fact", "decision", "role", "history"]>>;
142
+ includeArchived: z.ZodOptional<z.ZodBoolean>;
143
+ }, "strip", z.ZodTypeAny, {
144
+ type?: "fact" | "decision" | "role" | "history" | undefined;
145
+ scope?: string | undefined;
146
+ includeArchived?: boolean | undefined;
147
+ }, {
148
+ type?: "fact" | "decision" | "role" | "history" | undefined;
149
+ scope?: string | undefined;
150
+ includeArchived?: boolean | undefined;
151
+ }>;
152
+ export type ListInput = z.infer<typeof ListInput>;
153
+ export declare const forgetInputShape: {
154
+ id: z.ZodString;
155
+ };
156
+ export declare const ForgetInput: z.ZodObject<{
157
+ id: z.ZodString;
158
+ }, "strip", z.ZodTypeAny, {
159
+ id: string;
160
+ }, {
161
+ id: string;
162
+ }>;
163
+ export type ForgetInput = z.infer<typeof ForgetInput>;
164
+ /** Query accepted by the retrieval layer (retrieval.ts). */
71
165
  export interface SearchQuery {
72
166
  query?: string;
73
167
  scope?: string;
74
168
  type?: MemoryType;
75
169
  limit?: number;
76
170
  }
171
+ /** Backup file envelope (remembra export / import). */
172
+ export declare const SNAPSHOT_FORMAT = "remembra-export";
173
+ export declare const SnapshotInput: z.ZodObject<{
174
+ format: z.ZodLiteral<"remembra-export">;
175
+ version: z.ZodNumber;
176
+ exportedAt: z.ZodString;
177
+ memories: z.ZodArray<z.ZodObject<{
178
+ id: z.ZodString;
179
+ type: z.ZodEnum<["fact", "decision", "role", "history"]>;
180
+ content: z.ZodString;
181
+ scope: z.ZodEffects<z.ZodString, string, string>;
182
+ tags: z.ZodArray<z.ZodString, "many">;
183
+ importance: z.ZodNumber;
184
+ createdAt: z.ZodString;
185
+ updatedAt: z.ZodString;
186
+ source: z.ZodOptional<z.ZodString>;
187
+ lastSeen: z.ZodOptional<z.ZodString>;
188
+ archivedAt: z.ZodOptional<z.ZodString>;
189
+ embedding: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
190
+ }, "strip", z.ZodTypeAny, {
191
+ type: "fact" | "decision" | "role" | "history";
192
+ content: string;
193
+ scope: string;
194
+ tags: string[];
195
+ importance: number;
196
+ id: string;
197
+ createdAt: string;
198
+ updatedAt: string;
199
+ source?: string | undefined;
200
+ lastSeen?: string | undefined;
201
+ archivedAt?: string | undefined;
202
+ embedding?: number[] | undefined;
203
+ }, {
204
+ type: "fact" | "decision" | "role" | "history";
205
+ content: string;
206
+ scope: string;
207
+ tags: string[];
208
+ importance: number;
209
+ id: string;
210
+ createdAt: string;
211
+ updatedAt: string;
212
+ source?: string | undefined;
213
+ lastSeen?: string | undefined;
214
+ archivedAt?: string | undefined;
215
+ embedding?: number[] | undefined;
216
+ }>, "many">;
217
+ }, "strip", z.ZodTypeAny, {
218
+ format: "remembra-export";
219
+ version: number;
220
+ exportedAt: string;
221
+ memories: {
222
+ type: "fact" | "decision" | "role" | "history";
223
+ content: string;
224
+ scope: string;
225
+ tags: string[];
226
+ importance: number;
227
+ id: string;
228
+ createdAt: string;
229
+ updatedAt: string;
230
+ source?: string | undefined;
231
+ lastSeen?: string | undefined;
232
+ archivedAt?: string | undefined;
233
+ embedding?: number[] | undefined;
234
+ }[];
235
+ }, {
236
+ format: "remembra-export";
237
+ version: number;
238
+ exportedAt: string;
239
+ memories: {
240
+ type: "fact" | "decision" | "role" | "history";
241
+ content: string;
242
+ scope: string;
243
+ tags: string[];
244
+ importance: number;
245
+ id: string;
246
+ createdAt: string;
247
+ updatedAt: string;
248
+ source?: string | undefined;
249
+ lastSeen?: string | undefined;
250
+ archivedAt?: string | undefined;
251
+ embedding?: number[] | undefined;
252
+ }[];
253
+ }>;
254
+ export type SnapshotInput = z.infer<typeof SnapshotInput>;
package/dist/types.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import { z } from "zod";
2
2
  /** The four memory types Remembra stores. */
3
3
  export const MemoryType = z.enum(["fact", "decision", "role", "history"]);
4
+ /**
5
+ * Frontmatter schema version. Bump when the Memory format changes and
6
+ * add a migration step in store.ts (missing field in old files = v1).
7
+ */
8
+ export const SCHEMA_VERSION = 1;
4
9
  /**
5
10
  * Scope safety: reject `..` path segments so scope can never escape the
6
11
  * storage root (P0 directory traversal fix). Absolute paths like
@@ -12,23 +17,82 @@ export function isSafeScope(scope) {
12
17
  const normalized = scope.replace(/[^a-zA-Z0-9._/-]/g, "_");
13
18
  return !normalized.split("/").some((seg) => seg === "..");
14
19
  }
15
- export const StoreInput = z.object({
16
- type: MemoryType,
17
- content: z.string().min(1),
20
+ // ---------------------------------------------------------------------------
21
+ // Shared input schemas (audit #13: one source of truth).
22
+ // The raw `*Shape` objects feed MCP tool inputSchemas (ZodRawShape);
23
+ // the parsed `*Input` objects validate on every transport.
24
+ // ---------------------------------------------------------------------------
25
+ export const storeInputShape = {
26
+ type: MemoryType.describe("fact | decision | role | history"),
27
+ content: z.string().min(1).describe("The memory itself, written as a standalone statement"),
18
28
  scope: z
19
29
  .string()
20
30
  .default("global")
21
- .refine(isSafeScope, { message: "scope must not contain '..' path segments" }),
22
- tags: z.array(z.string()).default([]),
23
- importance: z.number().int().min(1).max(5).default(3),
24
- source: z.string().optional(),
25
- });
26
- export const DigestInput = z.object({
27
- transcript: z.string().min(1),
28
- scope: z
31
+ .describe("'global' for always-relevant memories, or a project path/id for project-scoped ones"),
32
+ tags: z.array(z.string()).default([]).describe("Keywords that boost retrieval"),
33
+ importance: z
34
+ .number()
35
+ .int()
36
+ .min(1)
37
+ .max(5)
38
+ .default(3)
39
+ .describe("1=minor, 5=critical (default 3)"),
40
+ source: z.string().optional().describe("Originating session or client"),
41
+ };
42
+ export const StoreInput = z
43
+ .object(storeInputShape)
44
+ .refine((v) => isSafeScope(v.scope), { message: "scope must not contain '..' path segments" });
45
+ export const digestInputShape = {
46
+ transcript: z
29
47
  .string()
30
- .refine(isSafeScope, { message: "scope must not contain '..' path segments" })
31
- .optional(),
32
- source: z.string().max(500).optional(),
48
+ .min(1)
49
+ .describe("Conversation transcript or a detailed summary of the session"),
50
+ scope: z.string().optional().describe("Scope for extracted memories (default: global)"),
51
+ source: z.string().optional().describe("Originating session/client"),
52
+ };
53
+ export const DigestInput = z
54
+ .object(digestInputShape)
55
+ .refine((v) => v.scope === undefined || isSafeScope(v.scope), {
56
+ message: "scope must not contain '..' path segments",
57
+ });
58
+ export const searchInputShape = {
59
+ query: z.string().optional().describe("Keywords to match (omit to get a scope/recency-ranked list)"),
60
+ scope: z.string().optional().describe("Current project path or workspace id to filter by"),
61
+ type: MemoryType.optional(),
62
+ limit: z.number().int().min(1).max(50).optional(),
63
+ };
64
+ export const SearchInput = z.object(searchInputShape);
65
+ export const listInputShape = {
66
+ scope: z.string().optional(),
67
+ type: MemoryType.optional(),
68
+ includeArchived: z.boolean().optional().describe("Include archived memories (flagged)"),
69
+ };
70
+ export const ListInput = z.object(listInputShape);
71
+ export const forgetInputShape = {
72
+ id: z.string().describe("Memory id (from memory_store or memory_list)"),
73
+ };
74
+ export const ForgetInput = z.object(forgetInputShape);
75
+ /** Backup file envelope (remembra export / import). */
76
+ export const SNAPSHOT_FORMAT = "remembra-export";
77
+ export const SnapshotInput = z.object({
78
+ format: z.literal(SNAPSHOT_FORMAT),
79
+ version: z.number().int().positive(),
80
+ exportedAt: z.string(),
81
+ memories: z
82
+ .array(z.object({
83
+ id: z.string().regex(/^[a-f0-9]{8,32}$/, "invalid id"),
84
+ type: MemoryType,
85
+ content: z.string().min(1),
86
+ scope: z.string().refine(isSafeScope, { message: "scope must not contain '..'" }),
87
+ tags: z.array(z.string()),
88
+ importance: z.number().int().min(1).max(5),
89
+ createdAt: z.string(),
90
+ updatedAt: z.string(),
91
+ source: z.string().optional(),
92
+ lastSeen: z.string().optional(),
93
+ archivedAt: z.string().optional(),
94
+ embedding: z.array(z.number()).optional(),
95
+ }))
96
+ .max(100_000),
33
97
  });
34
98
  //# sourceMappingURL=types.js.map
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,6CAA6C;AAC7C,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;AA4B1E;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACpC,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC;IAC3D,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,CAAC;IACjC,IAAI,EAAE,UAAU;IAChB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,OAAO,CAAC,QAAQ,CAAC;SACjB,MAAM,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,2CAA2C,EAAE,CAAC;IAChF,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACrC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACrD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC9B,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7B,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,MAAM,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,2CAA2C,EAAE,CAAC;SAC7E,QAAQ,EAAE;IACb,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;CACvC,CAAC,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,6CAA6C;AAC7C,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;AAG1E;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AA2BhC;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,IAAI,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACpC,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC;IAC3D,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC;AAC5D,CAAC;AAED,8EAA8E;AAC9E,yDAAyD;AACzD,qEAAqE;AACrE,2DAA2D;AAC3D,8EAA8E;AAE9E,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,IAAI,EAAE,UAAU,CAAC,QAAQ,CAAC,kCAAkC,CAAC;IAC7D,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,sDAAsD,CAAC;IAC3F,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,OAAO,CAAC,QAAQ,CAAC;SACjB,QAAQ,CAAC,qFAAqF,CAAC;IAClG,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,+BAA+B,CAAC;IAC/E,UAAU,EAAE,CAAC;SACV,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,CAAC,CAAC;SACV,QAAQ,CAAC,iCAAiC,CAAC;IAC9C,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;CACxE,CAAC;AACF,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC;KACxB,MAAM,CAAC,eAAe,CAAC;KACvB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,2CAA2C,EAAE,CAAC,CAAC;AAGjG,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,UAAU,EAAE,CAAC;SACV,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,CAAC,8DAA8D,CAAC;IAC3E,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gDAAgD,CAAC;IACvF,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,4BAA4B,CAAC;CACrE,CAAC;AACF,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC;KACzB,MAAM,CAAC,gBAAgB,CAAC;KACxB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,IAAI,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE;IAC5D,OAAO,EAAE,2CAA2C;CACrD,CAAC,CAAC;AAGL,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,6DAA6D,CAAC;IACpG,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,mDAAmD,CAAC;IAC1F,IAAI,EAAE,UAAU,CAAC,QAAQ,EAAE;IAC3B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;CAClD,CAAC;AACF,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAGtD,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,IAAI,EAAE,UAAU,CAAC,QAAQ,EAAE;IAC3B,eAAe,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qCAAqC,CAAC;CACxF,CAAC;AACF,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;AAGlD,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,8CAA8C,CAAC;CACxE,CAAC;AACF,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAWtD,uDAAuD;AACvD,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAC;AACjD,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC;IAClC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACpC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;IACtB,QAAQ,EAAE,CAAC;SACR,KAAK,CACJ,CAAC,CAAC,MAAM,CAAC;QACP,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,kBAAkB,EAAE,YAAY,CAAC;QACtD,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAC1B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,OAAO,EAAE,6BAA6B,EAAE,CAAC;QACjF,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QACzB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC1C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;QACrB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;QACrB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC7B,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC/B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QACjC,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;KAC1C,CAAC,CACH;SACA,GAAG,CAAC,OAAO,CAAC;CAChB,CAAC,CAAC"}
@@ -0,0 +1,98 @@
1
+ # Architecture
2
+
3
+ How Remembra is put together, and where the extension seams are.
4
+
5
+ ```
6
+ ┌──────────────┐ ┌──────────────┐
7
+ │ MCP tools │ │ HTTP API │ Transports (thin adapters)
8
+ │ (index.ts) │ │ (http.ts) │
9
+ └──────┬───────┘ └──────┬───────┘
10
+ │ both call the same handlers
11
+ ▼ ▼
12
+ ┌──────────────────────────────────┐
13
+ │ MemoryService (service.ts) │ Search orchestration, digest/dedup/merge,
14
+ │ ← depends on MemoryBackend only │ decay lifecycle, snapshots, embed/LLM fail-open
15
+ └──────────────┬───────────────────┘
16
+
17
+ ┌──────────────────────────────────┐
18
+ │ MemoryBackend (backend.ts) │ The storage contract
19
+ └──────────────┬───────────────────┘
20
+
21
+ ┌──────────────────────────────────┐
22
+ │ MemoryStore (store.ts) │ Plain markdown files + frontmatter
23
+ │ .remembra lock · crash recovery │ (no database, by decision — Q5-A)
24
+ └──────────────────────────────────┘
25
+ ```
26
+
27
+ ## MemoryBackend: the swap seam (audit Phase 3)
28
+
29
+ `MemoryService` never touches the filesystem directly — it talks to the
30
+ `MemoryBackend` interface (`src/backend.ts`). Today's implementation is
31
+ `MemoryStore` (markdown files). A future SQLite/vector-DB backend only has to
32
+ satisfy that interface; the tests include an `InMemoryBackend` proving the
33
+ service runs unchanged against a non-file implementation.
34
+
35
+ Contract highlights:
36
+
37
+ - mutations must be safe under same-process **and** cross-process concurrency;
38
+ - a memory id exists in **exactly one tree** (active or archived) at rest;
39
+ - `importMemory` refuses ids that already exist.
40
+
41
+ ## Concurrency model (audit Phase 2: advisory locking)
42
+
43
+ Three layers, from narrowest to widest:
44
+
45
+ | Layer | Mechanism | Guards against |
46
+ |-------|-----------|----------------|
47
+ | Single write | temp file + `rename()` (POSIX-atomic) | torn/corrupt files on crash |
48
+ | One process | FIFO queue inside `MemoryStore` | same-instance async races (touch vs archive, parallel stores) |
49
+ | Many processes | `<root>/.remembra.lock` (`O_EXCL` create) | MCP server vs `remembra maintain` CLI vs a second session |
50
+
51
+ Lock rules:
52
+
53
+ - acquired around every mutating operation (store/update/archive/revive/
54
+ touch/forget/import) **including its read**, so read-modify-write cycles are
55
+ whole;
56
+ - **stale steal**: a lock whose pid is dead (or older than
57
+ `REMEMBRA_LOCK_STALE_MS`, default 10 s) is removed; the `O_EXCL` re-create
58
+ decides the winner;
59
+ - **timeout**: waiting longer than `REMEMBRA_LOCK_TIMEOUT_MS` (default 5 s)
60
+ fails with the typed `LOCK_TIMEOUT` error (HTTP 423);
61
+ - released in a `finally`, verified by pid before unlinking.
62
+
63
+ Reads (`get`/`all`) don't lock — atomic rename means they always see a
64
+ consistent file.
65
+
66
+ ## Crash recovery (audit Phase 3: "journal")
67
+
68
+ Atomic `rename()` already guarantees no half-written memory files, so instead
69
+ of a write-ahead log (which would itself need journaling) Remembra runs a
70
+ one-time **recovery pass** on first store access per process:
71
+
72
+ 1. delete orphaned `*.tmp` files (crash between write and rename);
73
+ 2. reconcile ids found in **both** active and archived trees (crash between
74
+ archive/revive's write and unlink, audit finding #19) — newest `updatedAt`
75
+ wins, ties go to the archived copy.
76
+
77
+ Anything it fixes is logged:
78
+ `Remembra: crash recovery — removed N orphaned temp file(s), reconciled N interrupted move(s)`.
79
+
80
+ ## Error classification (audit Phase 2)
81
+
82
+ All actionable failures are `RemembraError` with a stable `code`
83
+ (`src/errors.ts`): `INVALID_INPUT`, `SNAPSHOT_INVALID`, `SCOPE_ESCAPES_ROOT`,
84
+ `NOT_FOUND`, `CONFLICT`, `LOCK_TIMEOUT`, `IO_ERROR`, `LLM_ERROR`.
85
+
86
+ - **HTTP** maps codes → statuses (400/404/409/423/500/502) and returns
87
+ `{ error, code }` bodies;
88
+ - **MCP tools** return `[CODE] message` text with `isError: true`;
89
+ - raw filesystem failures are wrapped as `IO_ERROR`; Zod failures crossing a
90
+ service boundary become `INVALID_INPUT`/`SNAPSHOT_INVALID` with a field
91
+ summary.
92
+
93
+ ## Schema versioning
94
+
95
+ Every memory file carries `version: <n>` in frontmatter (`SCHEMA_VERSION` in
96
+ `types.ts`). Files without the field (v1–v3.1) parse as v1. To change the
97
+ format: bump the constant, add a migration branch in `parse()`, and cover it
98
+ with a fixture test.
package/docs/clients.md CHANGED
@@ -92,6 +92,9 @@ REMEMBRA_API_KEY="your-secret" remembra --http
92
92
  | `REMEMBRA_ARCHIVE_TTL_DAYS` | `365` | Archived memory → deleted |
93
93
  | `REMEMBRA_HOST` | *(see security.md)* | HTTP bind address (loopback without key) |
94
94
  | `REMEMBRA_MAX_BODY` | `10485760` | Max HTTP request body bytes |
95
+ | `REMEMBRA_LOCK_TIMEOUT_MS` | `5000` | Max wait for the cross-process storage lock |
96
+ | `REMEMBRA_LOCK_STALE_MS` | `10000` | Age after which a lock with a dead/unknown pid is stolen |
97
+ | `REMEMBRA_DEBUG` | *(unset)* | `1` logs the storage root path at startup (off by default: log hygiene) |
95
98
 
96
99
  LLM/embedding key setup: see **[providers.md](providers.md)**.
97
100
 
package/docs/security.md CHANGED
@@ -52,6 +52,9 @@ Retrieved memories should be treated as **data with provenance**, not commands
52
52
  | **Body size limit** | 10 MiB default (`REMEMBRA_MAX_BODY`), `413` on excess — pre-checks `Content-Length` and enforces while streaming |
53
53
  | **Digest validation** | `DigestInput` Zod schema on both MCP and HTTP paths |
54
54
  | **Atomic writes** | temp file + `rename()` (POSIX-atomic) — no half-written memories after a crash |
55
+ | **Advisory locking** | `<root>/.remembra.lock` (`O_EXCL`) + in-process FIFO — cross-process writes serialize; stale locks (dead pid / older than `REMEMBRA_LOCK_STALE_MS`) are stolen; waiters fail with typed `LOCK_TIMEOUT` (HTTP 423) |
56
+ | **Crash recovery** | one-time pass on first access: removes orphaned `*.tmp`, reconciles ids left in both active+archived trees by an interrupted archive/revive |
57
+ | **Structured errors** | every actionable failure has a stable code (`INVALID_INPUT`, `LOCK_TIMEOUT`, `LLM_ERROR`, …) mapped to HTTP statuses / MCP `[CODE]` prefixes |
55
58
  | **ID collisions** | 12-hex IDs (2⁴⁸) + existence check on store |
56
59
  | **Content-Length** | Set on every response |
57
60
 
@@ -70,8 +73,9 @@ remembra --http
70
73
 
71
74
  - [ ] `REMEMBRA_API_KEY` set with a long random value (≥32 bytes)
72
75
  - [ ] TLS terminator in front for any non-loopback exposure
73
- - [ ] `REMEMBRA_HOME` lives on a filesystem you back up (there is **no built-in
74
- backup yet** the files are the only copy)
76
+ - [ ] `REMEMBRA_HOME` lives on a filesystem you back up use
77
+ `remembra export <file>.json` for portable snapshots (or `rsync`/git the
78
+ directory)
75
79
  - [ ] Periodic `memory_list {type: "role"}` audit
76
80
  - [ ] LLM/embedding keys scoped to least privilege
77
81
 
@@ -82,8 +86,9 @@ remembra --http
82
86
  - **No encryption at rest** — files are plaintext markdown (by design: you can
83
87
  read and edit them). Use filesystem-level encryption if needed.
84
88
  - **No PII redaction** — what you store is what's written to disk.
85
- - **No write locking / journal** — single-writer assumption; concurrent writers
86
- from multiple machines are unsupported (atomic writes protect against crashes,
87
- not interleaving).
88
- - **No built-in backup/export** planned; until then, back up `REMEMBRA_HOME`
89
- with your normal file backup (it's plain files, `rsync`/git all work).
89
+ - **Single-writer assumption per store, now cross-process safe** mutations
90
+ take an advisory lockfile (`O_EXCL`, stale-steal, typed `LOCK_TIMEOUT`), so
91
+ an MCP server, the `remembra maintain` CLI, and a session digest can run
92
+ against one store concurrently on one machine. Network filesystems with
93
+ unreliable `O_EXCL` semantics are untested; `remembra export` for backups
94
+ across machines.
package/docs/tools.md CHANGED
@@ -88,6 +88,17 @@ vectors. Roles never decay. Takes no arguments. See [lifecycle.md](lifecycle.md)
88
88
  Returns counts + affected ids. Also available as `POST /maintain` and the
89
89
  `remembra maintain` CLI command.
90
90
 
91
+ ## CLI commands
92
+
93
+ ```bash
94
+ remembra maintain # decay sweep + vector backfill (one-shot, prints JSON)
95
+ remembra export <file>.json # full backup snapshot incl. archived memories
96
+ remembra import <file>.json # restore; validates whole file first (atomic), idempotent
97
+ ```
98
+
99
+ Import skips existing ids and exact-duplicate contents, so running it twice —
100
+ or importing into a machine that already has the data — is always safe.
101
+
91
102
  ---
92
103
 
93
104
  ## Suggested session flow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hilbras/remembra",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "description": "External memory for AI assistants — remember facts, decisions, roles and history across sessions. MCP server for OpenCode, Claude Code, Cline, Kimi Code and more.",
5
5
  "type": "module",
6
6
  "bin": {