@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/CHANGELOG.md +79 -0
- package/README.md +15 -0
- package/dist/backend.d.ts +27 -0
- package/dist/backend.js +2 -0
- package/dist/backend.js.map +1 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.js +60 -0
- package/dist/errors.js.map +1 -0
- package/dist/http.js +16 -7
- package/dist/http.js.map +1 -1
- package/dist/index.js +117 -62
- package/dist/index.js.map +1 -1
- package/dist/service.d.ts +28 -4
- package/dist/service.js +94 -11
- package/dist/service.js.map +1 -1
- package/dist/store.d.ts +39 -5
- package/dist/store.js +343 -85
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +183 -5
- package/dist/types.js +78 -14
- package/dist/types.js.map +1 -1
- package/docs/architecture.md +98 -0
- package/docs/clients.md +3 -0
- package/docs/security.md +12 -7
- package/docs/tools.md +11 -0
- package/package.json +1 -1
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
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
.
|
|
22
|
-
tags: z.array(z.string()).default([]),
|
|
23
|
-
importance: z
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
.
|
|
31
|
-
.
|
|
32
|
-
|
|
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;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -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
|
|
74
|
-
|
|
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
|
-
- **
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
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": {
|