relembra 1.0.0-beta.4 → 1.0.0-beta.6
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/README.md +34 -13
- package/dist/frozen.d.ts +4 -0
- package/dist/frozen.js +11 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +2 -0
- package/dist/optmem.js +0 -4
- package/dist/pi-durable/index.js +12 -11
- package/dist/postgres/index.js +1 -11
- package/dist/relembra.d.ts +6 -0
- package/dist/relembra.js +5 -3
- package/dist/sqlite/database.d.ts +2 -0
- package/dist/sqlite/database.js +11 -4
- package/dist/sqlite/index.js +1 -11
- package/dist/testing/memory-store.js +1 -8
- package/dist/transfer.js +1 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/FORMAT.md +2 -2
- package/package.json +1 -1
- package/dist/cli/args.d.ts +0 -26
- package/dist/cli/args.js +0 -109
- package/dist/cli/commands.d.ts +0 -40
- package/dist/cli/commands.js +0 -114
- package/dist/cli/confirm.d.ts +0 -13
- package/dist/cli/confirm.js +0 -80
- package/dist/cli/errors.d.ts +0 -44
- package/dist/cli/errors.js +0 -109
- package/dist/cli/main.d.ts +0 -3
- package/dist/cli/main.js +0 -768
- package/dist/cli/mcp-lines.d.ts +0 -40
- package/dist/cli/mcp-lines.js +0 -87
- package/dist/cli/mcp.d.ts +0 -42
- package/dist/cli/mcp.js +0 -570
- package/dist/cli/scope.d.ts +0 -70
- package/dist/cli/scope.js +0 -325
- package/dist/cli/stdin.d.ts +0 -13
- package/dist/cli/stdin.js +0 -68
- package/dist/cli/wake.d.ts +0 -25
- package/dist/cli/wake.js +0 -46
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ and storage (an adapter). relembra enforces the rules:
|
|
|
25
25
|
- **Measured.** An evaluation harness with leak, scope, retrieval and wake-coverage gates, plus
|
|
26
26
|
controls that must fail. Results: [docs/eval/](docs/eval/).
|
|
27
27
|
|
|
28
|
-
Version: 1.0.0-beta.
|
|
28
|
+
Version: 1.0.0-beta.6, a beta ([changelog](CHANGELOG.md),
|
|
29
29
|
[releases](https://github.com/mneves75/relembra/releases)). Release procedure:
|
|
30
30
|
[docs/RELEASE.md](docs/RELEASE.md).
|
|
31
31
|
Design: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
@@ -57,7 +57,7 @@ Entry points:
|
|
|
57
57
|
|
|
58
58
|
| Import | What |
|
|
59
59
|
|---|---|
|
|
60
|
-
| `relembra` | `createRelembra`, the error types and the domain types |
|
|
60
|
+
| `relembra` | `createRelembra`, `advanceFence`, `exportJsonl`, `importJsonl`, `importOptmem`, `integrationOpId`, `identityEgress`, the error types and the domain types |
|
|
61
61
|
| `relembra/sqlite` | `openSqliteStore`, the SQLite adapter (`node:sqlite`) |
|
|
62
62
|
| `relembra/postgres` | `openPostgresStore`, the Postgres adapter over a SQL client the host passes |
|
|
63
63
|
| `relembra/pi-durable` | `createMemoryExtension`, memory tools for a pi-durable Harness |
|
|
@@ -226,7 +226,7 @@ On Windows, check the hash with `Get-FileHash` and copy the binary onto `PATH` a
|
|
|
226
226
|
|
|
227
227
|
Before replacing an installed executable, retain a backup and its hash. Verify installed bytes
|
|
228
228
|
against the release checksum and export metadata from a disposable store
|
|
229
|
-
(`relembra: "1.0.0-beta.
|
|
229
|
+
(`relembra: "1.0.0-beta.6"`); there is no `--version` flag. The [release procedure](docs/RELEASE.md) covers isolated checks and
|
|
230
230
|
rollback without opening a live memory store. After verification, `relembra init` prints the
|
|
231
231
|
session hook for your agent.
|
|
232
232
|
|
|
@@ -238,8 +238,11 @@ Commands, flags, exit codes and the JSON envelope: [docs/CLI.md](docs/CLI.md).
|
|
|
238
238
|
|
|
239
239
|
## MCP
|
|
240
240
|
|
|
241
|
-
`relembra mcp` is
|
|
242
|
-
|
|
241
|
+
`relembra mcp` is a dual-era MCP server over stdio supporting 2026-07-28, 2025-11-25,
|
|
242
|
+
2025-06-18, 2025-03-26 and 2024-11-05. Modern requests carry their version and capabilities in
|
|
243
|
+
`_meta` and stay stateless, even after a legacy `initialize` in the same process. Legacy clients
|
|
244
|
+
negotiate once with `initialize`: a supported legacy version is returned unchanged; any other
|
|
245
|
+
string falls back to 2025-11-25. It is a subcommand of the same binary,
|
|
243
246
|
so it takes its scope, principal and store from the launch environment exactly as the CLI does,
|
|
244
247
|
and no tool argument changes them. It writes at `agent`
|
|
245
248
|
trust at most and never asks for owner trust. Its tools are `note`, `find`, `brief`, `wake` and
|
|
@@ -251,7 +254,7 @@ project's directory or with `RELEMBRA_SCOPE` set. The contract (methods, tools,
|
|
|
251
254
|
framing, pending requests) is in [docs/CLI.md](docs/CLI.md#relembra-mcp).
|
|
252
255
|
|
|
253
256
|
The server holds at most 64 requests queued or running. One more gets the error `-31000`,
|
|
254
|
-
`data: { maxPending: 64 }`, at once and is never queued. The revision defines no overload code and
|
|
257
|
+
`data: { maxPending: 64 }`, at once and is never queued. The modern revision defines no overload code and
|
|
255
258
|
reserves `-32020` to `-32099` for its own, so `-31000` sits outside `-32768` to `-32000`, as its
|
|
256
259
|
Error Codes section asks. A client that stops reading its responses stops being read.
|
|
257
260
|
|
|
@@ -259,11 +262,18 @@ Qualified with the official MCP TypeScript SDK client, `@modelcontextprotocol/cl
|
|
|
259
262
|
(with `@modelcontextprotocol/core` 2.4.0), pinned to protocol revision 2026-07-28
|
|
260
263
|
(`versionNegotiation: { mode: { pin: "2026-07-28" } }`): `test/cli/mcp-sdk.test.ts` runs a stdio
|
|
261
264
|
round trip through it against the built binary. It is a development dependency only; the library
|
|
262
|
-
and the binary have no runtime dependencies.
|
|
263
|
-
|
|
264
|
-
names the supported version.
|
|
265
|
+
and the binary have no runtime dependencies. The motivating initialize-based clients are codex
|
|
266
|
+
0.166.0 (2025-06-18) and opencode 1.19.5 (2025-11-25); the SDK test also covers legacy negotiation.
|
|
265
267
|
|
|
266
|
-
|
|
268
|
+
Legacy results omit modern result and cache metadata. The 2024-11-05 tool definitions contain
|
|
269
|
+
only `name`, `description` and `inputSchema`; 2025-03-26 adds annotations with `annotations.title`
|
|
270
|
+
and alone accepts JSON-RPC batches. From 2025-06-18, tools also have top-level titles and output
|
|
271
|
+
schemas where defined, results may carry structured content, and `serverInfo` has a title.
|
|
272
|
+
Only 2025-11-25 returns input-schema failures as tool execution errors; earlier revisions and
|
|
273
|
+
modern requests return `-32602`. Legacy sessions support `ping`, `tools/list` and `tools/call`.
|
|
274
|
+
A modern `_meta` naming another version gets `-32022` with all five supported versions.
|
|
275
|
+
|
|
276
|
+
Implemented from these pages of the specification:
|
|
267
277
|
|
|
268
278
|
- [Key Changes](https://modelcontextprotocol.io/specification/2026-07-28/changelog): no
|
|
269
279
|
`initialize` handshake, per-request `_meta`, required `server/discover`, `resultType`, `ttlMs`
|
|
@@ -273,8 +283,19 @@ Implemented from these pages of the specification (read 2026-10-31):
|
|
|
273
283
|
("Per-request protocol fields", "Per-response protocol fields").
|
|
274
284
|
- [Versioning and Compatibility](https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning):
|
|
275
285
|
Protocol Version Negotiation, and "Backward Compatibility with Initialization-Based Versions"
|
|
276
|
-
for
|
|
286
|
+
for dual-era operation. The revision has no lifecycle page:
|
|
277
287
|
`/specification/2026-07-28/basic/lifecycle` redirects here.
|
|
288
|
+
- Legacy lifecycle: [2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle),
|
|
289
|
+
[2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle),
|
|
290
|
+
[2025-03-26](https://modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle) and
|
|
291
|
+
[2024-11-05](https://modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle):
|
|
292
|
+
initialization and version negotiation.
|
|
293
|
+
- Legacy key changes: [2025-03-26](https://modelcontextprotocol.io/specification/2025-03-26/changelog)
|
|
294
|
+
adds annotations and requires batching;
|
|
295
|
+
[2025-06-18](https://modelcontextprotocol.io/specification/2025-06-18/changelog) adds structured
|
|
296
|
+
output, output schemas and titles, and removes batching;
|
|
297
|
+
[2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25/changelog) makes input
|
|
298
|
+
validation failures tool execution errors (SEP-1303).
|
|
278
299
|
- [stdio](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio): the
|
|
279
300
|
framing, stdout and stderr rules, Cancellation and Shutdown.
|
|
280
301
|
- [Cancellation](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation).
|
|
@@ -291,8 +312,8 @@ Implemented from these pages of the specification (read 2026-10-31):
|
|
|
291
312
|
|
|
292
313
|
Where these pages go beyond the ticket, the server follows them: cache fields go on
|
|
293
314
|
`server/discover` and `tools/list` only (the Caching page lists no other operation this server
|
|
294
|
-
offers), `serverInfo` is in every result's `_meta` (a SHOULD)
|
|
295
|
-
|
|
315
|
+
offers), and `serverInfo` is in every modern result's `_meta` (a SHOULD). Legacy results follow
|
|
316
|
+
their negotiated revision; initialize params never change the environment's scope or principal.
|
|
296
317
|
|
|
297
318
|
The 2026-07-28 revision has no per-call identifier a server could derive an op id from, and the
|
|
298
319
|
JSON-RPC id changes on every retry. So instead of the plan's `[host, conversation, task, call]`
|
package/dist/frozen.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Frozen, detached copies, as the stores hand values out: a caller cannot change what is stored. */
|
|
2
|
+
import type { Event, NewEvent } from "./model.js";
|
|
3
|
+
export declare function deepFreeze<T>(value: T): T;
|
|
4
|
+
export declare function detachEvent(event: NewEvent, seq: number): Event;
|
package/dist/frozen.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function deepFreeze(value) {
|
|
2
|
+
if (typeof value === "object" && value !== null) {
|
|
3
|
+
for (const child of Object.values(value))
|
|
4
|
+
deepFreeze(child);
|
|
5
|
+
Object.freeze(value);
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
export function detachEvent(event, seq) {
|
|
10
|
+
return deepFreeze(structuredClone({ ...event, seq }));
|
|
11
|
+
}
|
package/dist/model.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type OpId = string;
|
|
|
12
12
|
* model. `agent`: model-authored and unreviewed. `untrusted`: known to come from external content.
|
|
13
13
|
*/
|
|
14
14
|
export type Trust = "owner" | "agent" | "untrusted";
|
|
15
|
+
/** Every trust tier, highest first. */
|
|
16
|
+
export declare const TRUSTS: readonly Trust[];
|
|
15
17
|
/**
|
|
16
18
|
* A writer's authority as the host grants it: a fencing key and the epoch this writer holds. The
|
|
17
19
|
* store keeps the highest epoch it has seen per key. A write whose epoch is lower commits
|
package/dist/model.js
CHANGED
package/dist/optmem.js
CHANGED
|
@@ -9,7 +9,6 @@ import { findGuardViolation, MAX_MEMORY_BYTES } from "./guard.js";
|
|
|
9
9
|
import { EVENT_FORMAT, } from "./model.js";
|
|
10
10
|
import { systemClock } from "./ports.js";
|
|
11
11
|
import { acceptanceTime, checkFence, checkName, checkPrincipal, checkProvenance, enforceFence, lowerTrust, owns, trustCeiling, } from "./relembra.js";
|
|
12
|
-
import { MAX_SCOPE_BYTES, utf8Bytes } from "./render.js";
|
|
13
12
|
import { MAX_IMPORT_BYTES, MAX_IMPORT_EVENTS, } from "./transfer.js";
|
|
14
13
|
const STORE_NAME = /^[A-Za-z0-9._-]{1,64}$/;
|
|
15
14
|
const NEWLINE = 0x0a;
|
|
@@ -48,9 +47,6 @@ export async function importOptmem(store, principal, input) {
|
|
|
48
47
|
throw new ScopeDeniedError("own");
|
|
49
48
|
if (!principal.writable.includes(scope))
|
|
50
49
|
throw new ScopeDeniedError("write");
|
|
51
|
-
if (typeof scope !== "string" || scope === "" || utf8Bytes(scope) > MAX_SCOPE_BYTES) {
|
|
52
|
-
throw new RangeError(`a scope must be 1 to ${MAX_SCOPE_BYTES} UTF-8 bytes`);
|
|
53
|
-
}
|
|
54
50
|
checkName("scope", scope);
|
|
55
51
|
if (typeof name !== "string" || !STORE_NAME.test(name)) {
|
|
56
52
|
throw new RangeError("an OptMem store name is 1 to 64 letters, digits, '.', '_' or '-'");
|
package/dist/pi-durable/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { MAX_MEMORY_BYTES } from "../guard.js";
|
|
1
2
|
import { integrationOpId } from "../ops.js";
|
|
2
|
-
import { checkCarrier } from "../relembra.js";
|
|
3
|
+
import { checkCarrier, MAX_FIND_LIMIT, MAX_QUERY_LENGTH, } from "../relembra.js";
|
|
3
4
|
export const MEMORY_EXTENSION_NAME = "relembra";
|
|
4
5
|
/**
|
|
5
6
|
* The key of relembra's system prompt section. Section keys are shared by every extension (a later
|
|
@@ -8,9 +9,8 @@ export const MEMORY_EXTENSION_NAME = "relembra";
|
|
|
8
9
|
export const MEMORY_SECTION_KEY = "relembra_memory";
|
|
9
10
|
/** What the memory section shows when the wake cannot be rendered: no memory text at all. */
|
|
10
11
|
export const WAKE_UNAVAILABLE = '<wake unavailable="true"/>';
|
|
12
|
+
/** The most an argument may hold before the guard reads it; the guard's own limit is far lower. */
|
|
11
13
|
const MAX_TEXT = 4_000;
|
|
12
|
-
const MAX_QUERY = 1_000;
|
|
13
|
-
const MAX_LIMIT = 100;
|
|
14
14
|
/** pi-durable's generation task; hooks match tasks by this name. */
|
|
15
15
|
const GENERATION_TASK = "pi.generation";
|
|
16
16
|
/** The kind of every carrier this extension records. */
|
|
@@ -35,7 +35,8 @@ export function createMemoryExtension(options) {
|
|
|
35
35
|
const resolve = async (conversationId) => options.resolve(conversationId);
|
|
36
36
|
const note = {
|
|
37
37
|
name: "memory_note",
|
|
38
|
-
description: "Save one durable fact for later sessions: a decision, preference or lesson, as one line
|
|
38
|
+
description: "Save one durable fact for later sessions: a decision, preference or lesson, as one line " +
|
|
39
|
+
`of at most ${MAX_MEMORY_BYTES} bytes.`,
|
|
39
40
|
parameters: {
|
|
40
41
|
type: "object",
|
|
41
42
|
required: ["text"],
|
|
@@ -66,13 +67,13 @@ export function createMemoryExtension(options) {
|
|
|
66
67
|
type: "object",
|
|
67
68
|
required: ["query"],
|
|
68
69
|
properties: {
|
|
69
|
-
query: { type: "string", minLength: 1, maxLength:
|
|
70
|
-
limit: { type: "integer", minimum: 1, maximum:
|
|
70
|
+
query: { type: "string", minLength: 1, maxLength: MAX_QUERY_LENGTH },
|
|
71
|
+
limit: { type: "integer", minimum: 1, maximum: MAX_FIND_LIMIT },
|
|
71
72
|
},
|
|
72
73
|
additionalProperties: false,
|
|
73
74
|
},
|
|
74
75
|
replay: "safe",
|
|
75
|
-
prepareArguments: strictArguments("memory_find", "query",
|
|
76
|
+
prepareArguments: strictArguments("memory_find", "query", MAX_QUERY_LENGTH, true),
|
|
76
77
|
async execute(args, api) {
|
|
77
78
|
const { context } = await resolve(api.conversationId);
|
|
78
79
|
const limit = args.limit;
|
|
@@ -90,11 +91,11 @@ export function createMemoryExtension(options) {
|
|
|
90
91
|
parameters: {
|
|
91
92
|
type: "object",
|
|
92
93
|
required: ["topic"],
|
|
93
|
-
properties: { topic: { type: "string", minLength: 1, maxLength:
|
|
94
|
+
properties: { topic: { type: "string", minLength: 1, maxLength: MAX_QUERY_LENGTH } },
|
|
94
95
|
additionalProperties: false,
|
|
95
96
|
},
|
|
96
97
|
replay: "safe",
|
|
97
|
-
prepareArguments: strictArguments("memory_brief", "topic",
|
|
98
|
+
prepareArguments: strictArguments("memory_brief", "topic", MAX_QUERY_LENGTH),
|
|
98
99
|
async execute(args, api) {
|
|
99
100
|
const { context } = await resolve(api.conversationId);
|
|
100
101
|
const rendering = await relembra.brief(context, {
|
|
@@ -178,7 +179,7 @@ function withWake(messages, wake) {
|
|
|
178
179
|
*/
|
|
179
180
|
function strictArguments(tool, field, max, limit = false) {
|
|
180
181
|
const message = `${tool}: invalid arguments: ${field} must be a string of 1 to ${max} characters` +
|
|
181
|
-
`${limit ? `, limit an optional integer from 1 to ${
|
|
182
|
+
`${limit ? `, limit an optional integer from 1 to ${MAX_FIND_LIMIT}` : ""}, and nothing else`;
|
|
182
183
|
const allowed = new Set(limit ? [field, "limit"] : [field]);
|
|
183
184
|
return (args) => {
|
|
184
185
|
if (typeof args !== "object" || args === null || Array.isArray(args))
|
|
@@ -193,7 +194,7 @@ function strictArguments(tool, field, max, limit = false) {
|
|
|
193
194
|
if (!limit || given.limit === undefined)
|
|
194
195
|
return { [field]: value };
|
|
195
196
|
const count = given.limit;
|
|
196
|
-
if (!Number.isInteger(count) || count < 1 || count >
|
|
197
|
+
if (!Number.isInteger(count) || count < 1 || count > MAX_FIND_LIMIT) {
|
|
197
198
|
throw new Error(message);
|
|
198
199
|
}
|
|
199
200
|
return { [field]: value, limit: count };
|
package/dist/postgres/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StoreBusyError } from "../errors.js";
|
|
2
|
+
import { detachEvent } from "../frozen.js";
|
|
2
3
|
import { redactedEvent, } from "../model.js";
|
|
3
4
|
import { sqlState } from "./client.js";
|
|
4
5
|
import { MIGRATIONS, migrate } from "./migrations.js";
|
|
@@ -431,9 +432,6 @@ function pause(attempts) {
|
|
|
431
432
|
const ceiling = Math.min(2 ** attempts, 64);
|
|
432
433
|
return new Promise((done) => setTimeout(done, ceiling / 2 + Math.random() * ceiling));
|
|
433
434
|
}
|
|
434
|
-
function detachEvent(event, seq) {
|
|
435
|
-
return deepFreeze(structuredClone({ ...event, seq }));
|
|
436
|
-
}
|
|
437
435
|
function memoryRow(memory) {
|
|
438
436
|
return [
|
|
439
437
|
memory.id,
|
|
@@ -482,11 +480,3 @@ function opFromRow(row) {
|
|
|
482
480
|
seq: Number(row.seq),
|
|
483
481
|
};
|
|
484
482
|
}
|
|
485
|
-
function deepFreeze(value) {
|
|
486
|
-
if (typeof value === "object" && value !== null) {
|
|
487
|
-
for (const child of Object.values(value))
|
|
488
|
-
deepFreeze(child);
|
|
489
|
-
Object.freeze(value);
|
|
490
|
-
}
|
|
491
|
-
return value;
|
|
492
|
-
}
|
package/dist/relembra.d.ts
CHANGED
|
@@ -306,6 +306,10 @@ export interface BriefInput extends RenderingTarget {
|
|
|
306
306
|
/** The most UTF-8 bytes the rendering may have, frame included. Default 9,500. */
|
|
307
307
|
readonly budgetBytes?: number;
|
|
308
308
|
}
|
|
309
|
+
/** The most results one find may return. */
|
|
310
|
+
export declare const MAX_FIND_LIMIT = 100;
|
|
311
|
+
/** The most characters a find query or a brief topic may have. */
|
|
312
|
+
export declare const MAX_QUERY_LENGTH = 1000;
|
|
309
313
|
export declare function createRelembra<Context>(options: RelembraOptions<Context>): Relembra<Context>;
|
|
310
314
|
/** A rendering's carrier, once both its fields pass the identifier rule; undefined when absent. */
|
|
311
315
|
export declare function checkCarrier(carrier: Carrier | undefined): Carrier | undefined;
|
|
@@ -318,6 +322,8 @@ export declare function advanceFence(store: Store, fence: AuthorityFence): Promi
|
|
|
318
322
|
export declare function enforceFence(tx: StoreTransaction, fence: AuthorityFence): Promise<void>;
|
|
319
323
|
/** Copies the fence once, so an accessor cannot answer differently to the check and the store. */
|
|
320
324
|
export declare function checkFence(fence: AuthorityFence): AuthorityFence;
|
|
325
|
+
/** The most sources one summary may have. */
|
|
326
|
+
export declare const MAX_SUMMARY_SOURCES = 100;
|
|
321
327
|
/**
|
|
322
328
|
* The sources copied once, checked (1 to 100, each a memory id and a positive revision, no memory
|
|
323
329
|
* twice) and sorted by memory id, so the order they were given in changes nothing.
|
package/dist/relembra.js
CHANGED
|
@@ -8,8 +8,10 @@ import { hasTokens, parseQuery, rank, scoreIndex } from "./ranking.js";
|
|
|
8
8
|
import { briefEntries, briefSummaries, checkBudget, coveredIds, DEFAULT_BUDGET_BYTES, findEntries, fitRendering, identityEgress, isSummary, MAX_ID_BYTES, MAX_SCOPE_BYTES, newestFirst, trustOf, utf8Bytes, wakeEntries, wakeOrder, } from "./render.js";
|
|
9
9
|
const DEFAULT_RECALL_LIMIT = 50;
|
|
10
10
|
const DEFAULT_FIND_LIMIT = 10;
|
|
11
|
-
|
|
12
|
-
const
|
|
11
|
+
/** The most results one find may return. */
|
|
12
|
+
export const MAX_FIND_LIMIT = 100;
|
|
13
|
+
/** The most characters a find query or a brief topic may have. */
|
|
14
|
+
export const MAX_QUERY_LENGTH = 1_000;
|
|
13
15
|
export function createRelembra(options) {
|
|
14
16
|
const { store } = options;
|
|
15
17
|
// Every principal the host's policy gives is checked once, before any operation uses it.
|
|
@@ -687,7 +689,7 @@ function checkRetention(given) {
|
|
|
687
689
|
return Object.freeze(rules);
|
|
688
690
|
}
|
|
689
691
|
/** The most sources one summary may have. */
|
|
690
|
-
const MAX_SUMMARY_SOURCES = 100;
|
|
692
|
+
export const MAX_SUMMARY_SOURCES = 100;
|
|
691
693
|
/**
|
|
692
694
|
* The sources copied once, checked (1 to 100, each a memory id and a positive revision, no memory
|
|
693
695
|
* twice) and sorted by memory id, so the order they were given in changes nothing.
|
|
@@ -62,6 +62,8 @@ export declare function storeFolderAccounts(userSid: string): string[];
|
|
|
62
62
|
* chmodded: someone else may already have read it.
|
|
63
63
|
*/
|
|
64
64
|
export declare function checkOwnerOnly(path: string): void;
|
|
65
|
+
/** Whether the store file's header says WAL (bytes 18 and 19 are 2). A new, empty file does not. */
|
|
66
|
+
export declare function walInHeader(path: string): boolean;
|
|
65
67
|
/**
|
|
66
68
|
* Opens the file defensively, refuses a newer schema before changing anything, then switches to
|
|
67
69
|
* WAL with synchronous=FULL and migrates. Busy errors on the way become StoreBusyError.
|
package/dist/sqlite/database.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync } from "node:fs";
|
|
2
|
+
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, } from "node:fs";
|
|
3
3
|
import { dirname, win32 } from "node:path";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
@@ -341,9 +341,16 @@ function switchToWal(db, timeoutMs) {
|
|
|
341
341
|
}
|
|
342
342
|
}
|
|
343
343
|
/** Whether the store file's header says WAL (bytes 18 and 19 are 2). A new, empty file does not. */
|
|
344
|
-
function walInHeader(path) {
|
|
345
|
-
|
|
346
|
-
|
|
344
|
+
export function walInHeader(path) {
|
|
345
|
+
// The header only: the file is the whole store, and every open comes through here.
|
|
346
|
+
const header = new Uint8Array(20);
|
|
347
|
+
const fd = openSync(path, "r");
|
|
348
|
+
try {
|
|
349
|
+
return readSync(fd, header, 0, 20, 0) === 20 && header[18] === 2 && header[19] === 2;
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
closeSync(fd);
|
|
353
|
+
}
|
|
347
354
|
}
|
|
348
355
|
/**
|
|
349
356
|
* Switches a rollback-journal (or new) file to WAL only after checking its schema version, under
|
package/dist/sqlite/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { detachEvent } from "../frozen.js";
|
|
1
2
|
import { redactedEvent, } from "../model.js";
|
|
2
3
|
import { checkBusyTimeout, createOwnerOnly, isBusy, openDatabase, StoreBusyError, } from "./database.js";
|
|
3
4
|
import { termIndex } from "./term-index.js";
|
|
@@ -423,9 +424,6 @@ async function withBusyRetry(timeoutMs, step) {
|
|
|
423
424
|
}
|
|
424
425
|
}
|
|
425
426
|
}
|
|
426
|
-
function detachEvent(event, seq) {
|
|
427
|
-
return deepFreeze(structuredClone({ ...event, seq }));
|
|
428
|
-
}
|
|
429
427
|
function memoryRow(memory) {
|
|
430
428
|
return [
|
|
431
429
|
memory.id,
|
|
@@ -474,11 +472,3 @@ function opFromRow(row) {
|
|
|
474
472
|
seq: row.seq,
|
|
475
473
|
};
|
|
476
474
|
}
|
|
477
|
-
function deepFreeze(value) {
|
|
478
|
-
if (typeof value === "object" && value !== null) {
|
|
479
|
-
for (const child of Object.values(value))
|
|
480
|
-
deepFreeze(child);
|
|
481
|
-
Object.freeze(value);
|
|
482
|
-
}
|
|
483
|
-
return value;
|
|
484
|
-
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deepFreeze } from "../frozen.js";
|
|
1
2
|
import { redactedEvent, } from "../model.js";
|
|
2
3
|
import { documentTerms } from "../ranking.js";
|
|
3
4
|
/** A store held in process memory, for tests and examples. */
|
|
@@ -250,11 +251,3 @@ function opKey(principalId, opId) {
|
|
|
250
251
|
function snapshot(value) {
|
|
251
252
|
return deepFreeze(structuredClone(value));
|
|
252
253
|
}
|
|
253
|
-
function deepFreeze(value) {
|
|
254
|
-
if (typeof value === "object" && value !== null) {
|
|
255
|
-
for (const child of Object.values(value))
|
|
256
|
-
deepFreeze(child);
|
|
257
|
-
Object.freeze(value);
|
|
258
|
-
}
|
|
259
|
-
return value;
|
|
260
|
-
}
|
package/dist/transfer.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { IdentifierError, ImportError, ProvenanceError, ScopeDeniedError, } from "./errors.js";
|
|
7
7
|
import { findGuardViolation, MAX_MEMORY_BYTES } from "./guard.js";
|
|
8
|
-
import { EVENT_FORMAT, LATEST_EVENT_FORMAT, PROMOTE_FORMAT, } from "./model.js";
|
|
8
|
+
import { EVENT_FORMAT, LATEST_EVENT_FORMAT, PROMOTE_FORMAT, TRUSTS, } from "./model.js";
|
|
9
9
|
import { systemClock } from "./ports.js";
|
|
10
10
|
import { acceptanceTime, checkFence, checkPrincipal, checkProvenance, checkSources, enforceCeiling, enforceFence, liveDerived, lowerTrust, MAX_PRINCIPAL_ID_BYTES, owns, redactionPlan, redactWithDerived, retractAll, trustCeiling, } from "./relembra.js";
|
|
11
11
|
import { isSummary, MAX_ID_BYTES, MAX_SCOPE_BYTES, utf8Bytes } from "./render.js";
|
|
@@ -210,7 +210,6 @@ function checkPrincipalId(id, line) {
|
|
|
210
210
|
checkIdentifier("principalId", id, MAX_PRINCIPAL_ID_BYTES, line);
|
|
211
211
|
}
|
|
212
212
|
const CAUSES = ["request", "retention", "source"];
|
|
213
|
-
const TRUSTS = ["owner", "agent", "untrusted"];
|
|
214
213
|
/** One event line, checked field by field; anything else in the line is ignored. */
|
|
215
214
|
function checkEvent(value, line, header, latest) {
|
|
216
215
|
const { scopes } = header;
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** This relembra's version, as package.json gives it. An export's header carries it. */
|
|
2
|
-
export declare const RELEMBRA_VERSION = "1.0.0-beta.
|
|
2
|
+
export declare const RELEMBRA_VERSION = "1.0.0-beta.6";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** This relembra's version, as package.json gives it. An export's header carries it. */
|
|
2
|
-
export const RELEMBRA_VERSION = "1.0.0-beta.
|
|
2
|
+
export const RELEMBRA_VERSION = "1.0.0-beta.6";
|
package/docs/FORMAT.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# relembra storage format
|
|
2
2
|
|
|
3
|
-
Status: event formats 1 and 2, SQLite schema version 6, Postgres schema version 3. Release 1.0.0-beta.
|
|
3
|
+
Status: event formats 1 and 2, SQLite schema version 6, Postgres schema version 3. Release 1.0.0-beta.6.
|
|
4
4
|
|
|
5
5
|
This document is the contract for what relembra stores. A reader that follows it needs no
|
|
6
6
|
relembra code.
|
|
@@ -175,7 +175,7 @@ the events above, so a reader needs only this document.
|
|
|
175
175
|
|---|---|---|
|
|
176
176
|
| `export` | string | Always `relembra`. What makes the file an export. |
|
|
177
177
|
| `format` | integer | The newest event format of the lines after it: `1` when none is a `promote` line, so a format 1 reader still reads the file, and `2` when one is. |
|
|
178
|
-
| `relembra` | string | The relembra version that wrote the file, such as `1.0.0-beta.
|
|
178
|
+
| `relembra` | string | The relembra version that wrote the file, such as `1.0.0-beta.6`. Informational: a reader decides by `format` alone. |
|
|
179
179
|
| `scopes` | array of strings | The scopes exported. Every event's `scope` is one of them. |
|
|
180
180
|
|
|
181
181
|
### Events (lines 2 onward)
|
package/package.json
CHANGED
package/dist/cli/args.d.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { type CommandSpec } from "./commands.js";
|
|
2
|
-
export interface Parsed {
|
|
3
|
-
readonly command: CommandSpec;
|
|
4
|
-
readonly positionals: readonly string[];
|
|
5
|
-
/** Each given flag's values; a boolean flag has one empty value. */
|
|
6
|
-
readonly flags: ReadonlyMap<string, readonly string[]>;
|
|
7
|
-
}
|
|
8
|
-
export type ParseResult = {
|
|
9
|
-
readonly kind: "help";
|
|
10
|
-
} | {
|
|
11
|
-
readonly kind: "command";
|
|
12
|
-
readonly parsed: Parsed;
|
|
13
|
-
};
|
|
14
|
-
/** Whether `--json` appears before any `--`, so even a usage error can answer in JSON. */
|
|
15
|
-
export declare function wantsJson(argv: readonly string[]): boolean;
|
|
16
|
-
/**
|
|
17
|
-
* Parses `argv` against the command table. Every token that starts with `-` before `--` must be
|
|
18
|
-
* a flag the command declares (or a global flag): an unknown flag is a usage error, never
|
|
19
|
-
* ignored, so no spelling of an undeclared option can change what a command does.
|
|
20
|
-
*/
|
|
21
|
-
export declare function parseArgs(argv: readonly string[]): ParseResult;
|
|
22
|
-
/** The single value of a flag, or undefined when it was not given. */
|
|
23
|
-
export declare function flagValue(parsed: Parsed, name: string): string | undefined;
|
|
24
|
-
export declare function hasFlag(parsed: Parsed, name: string): boolean;
|
|
25
|
-
/** A positive integer flag value, at most `max`; undefined when the flag is absent. */
|
|
26
|
-
export declare function intFlag(parsed: Parsed, name: string, max: number): number | undefined;
|
package/dist/cli/args.js
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { COMMANDS } from "./commands.js";
|
|
2
|
-
import { usage } from "./errors.js";
|
|
3
|
-
/** Whether `--json` appears before any `--`, so even a usage error can answer in JSON. */
|
|
4
|
-
export function wantsJson(argv) {
|
|
5
|
-
for (const arg of argv) {
|
|
6
|
-
if (arg === "--")
|
|
7
|
-
return false;
|
|
8
|
-
if (arg === "--json")
|
|
9
|
-
return true;
|
|
10
|
-
}
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Parses `argv` against the command table. Every token that starts with `-` before `--` must be
|
|
15
|
-
* a flag the command declares (or a global flag): an unknown flag is a usage error, never
|
|
16
|
-
* ignored, so no spelling of an undeclared option can change what a command does.
|
|
17
|
-
*/
|
|
18
|
-
export function parseArgs(argv) {
|
|
19
|
-
if (argv.includes("--help") && !argv.slice(0, argv.indexOf("--help")).includes("--")) {
|
|
20
|
-
return { kind: "help" };
|
|
21
|
-
}
|
|
22
|
-
const [name, ...rest] = argv.filter((arg, index) => !(arg === "--json" && !argv.slice(0, index).includes("--")));
|
|
23
|
-
if (name === undefined)
|
|
24
|
-
throw usage("missing-command", "no command given; see relembra --help");
|
|
25
|
-
const command = COMMANDS.find((spec) => spec.name === name);
|
|
26
|
-
if (command === undefined)
|
|
27
|
-
throw usage("unknown-command", `unknown command: ${name.slice(0, 40)}`);
|
|
28
|
-
const positionals = [];
|
|
29
|
-
const flags = new Map();
|
|
30
|
-
for (let index = 0; index < rest.length; index++) {
|
|
31
|
-
const arg = rest[index];
|
|
32
|
-
if (arg === "--") {
|
|
33
|
-
positionals.push(...rest.slice(index + 1));
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
if (!arg.startsWith("-") || arg === "-") {
|
|
37
|
-
positionals.push(arg);
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
const equals = arg.indexOf("=");
|
|
41
|
-
const flagName = equals < 0 ? arg : arg.slice(0, equals);
|
|
42
|
-
const spec = command.flags.find((flag) => flag.name === flagName);
|
|
43
|
-
if (spec === undefined) {
|
|
44
|
-
throw usage("unknown-flag", `${command.name} takes no flag ${flagName.slice(0, 40)}`);
|
|
45
|
-
}
|
|
46
|
-
let value = "";
|
|
47
|
-
if (spec.value) {
|
|
48
|
-
if (equals >= 0)
|
|
49
|
-
value = arg.slice(equals + 1);
|
|
50
|
-
else {
|
|
51
|
-
const next = rest[index + 1];
|
|
52
|
-
if (next === undefined)
|
|
53
|
-
throw usage("missing-value", `${flagName} needs a value`);
|
|
54
|
-
value = next;
|
|
55
|
-
index++;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
else if (equals >= 0) {
|
|
59
|
-
throw usage("unexpected-value", `${flagName} takes no value`);
|
|
60
|
-
}
|
|
61
|
-
const values = flags.get(flagName) ?? [];
|
|
62
|
-
if (values.length > 0 && !spec.repeat) {
|
|
63
|
-
throw usage("repeated-flag", `${flagName} may be given once`);
|
|
64
|
-
}
|
|
65
|
-
flags.set(flagName, [...values, value]);
|
|
66
|
-
}
|
|
67
|
-
checkPositionals(command, positionals);
|
|
68
|
-
return { kind: "command", parsed: { command, positionals, flags } };
|
|
69
|
-
}
|
|
70
|
-
function checkPositionals(command, positionals) {
|
|
71
|
-
const count = positionals.length;
|
|
72
|
-
const max = {
|
|
73
|
-
none: 0,
|
|
74
|
-
id: 1,
|
|
75
|
-
text: 1,
|
|
76
|
-
"id-text": 2,
|
|
77
|
-
words: Number.POSITIVE_INFINITY,
|
|
78
|
-
path: 1,
|
|
79
|
-
}[command.positionals];
|
|
80
|
-
const min = { none: 0, id: 1, text: 0, "id-text": 1, words: 1, path: 0 }[command.positionals];
|
|
81
|
-
if (count > max) {
|
|
82
|
-
throw usage("extra-argument", command.positionals === "text" || command.positionals === "id-text"
|
|
83
|
-
? `${command.name} takes the text as one argument; quote it`
|
|
84
|
-
: `${command.name} takes at most ${max} argument${max === 1 ? "" : "s"}`);
|
|
85
|
-
}
|
|
86
|
-
if (count < min)
|
|
87
|
-
throw usage("missing-argument", `usage: ${command.usage}`);
|
|
88
|
-
}
|
|
89
|
-
/** The single value of a flag, or undefined when it was not given. */
|
|
90
|
-
export function flagValue(parsed, name) {
|
|
91
|
-
return parsed.flags.get(name)?.[0];
|
|
92
|
-
}
|
|
93
|
-
export function hasFlag(parsed, name) {
|
|
94
|
-
return parsed.flags.has(name);
|
|
95
|
-
}
|
|
96
|
-
/** A positive integer flag value, at most `max`; undefined when the flag is absent. */
|
|
97
|
-
export function intFlag(parsed, name, max) {
|
|
98
|
-
const value = flagValue(parsed, name);
|
|
99
|
-
if (value === undefined)
|
|
100
|
-
return undefined;
|
|
101
|
-
if (!/^[0-9]{1,15}$/.test(value)) {
|
|
102
|
-
throw usage("bad-value", `${name} takes a whole number from 1 to ${max}`);
|
|
103
|
-
}
|
|
104
|
-
const number = Number(value);
|
|
105
|
-
if (number < 1 || number > max) {
|
|
106
|
-
throw usage("bad-value", `${name} takes a whole number from 1 to ${max}`);
|
|
107
|
-
}
|
|
108
|
-
return number;
|
|
109
|
-
}
|
package/dist/cli/commands.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The CLI's command table: every command, its arguments and its flags. docs/CLI.md documents
|
|
3
|
-
* exactly this table, and test/cli/contract.test.ts checks the two against each other.
|
|
4
|
-
* No flag names a scope, a home or a user: those come from the environment only.
|
|
5
|
-
*/
|
|
6
|
-
export interface FlagSpec {
|
|
7
|
-
/** `--name`. */
|
|
8
|
-
readonly name: string;
|
|
9
|
-
/** Whether the flag takes a value (`--limit 5`, `--limit=5`). */
|
|
10
|
-
readonly value: boolean;
|
|
11
|
-
/** Whether the flag may be given more than once. */
|
|
12
|
-
readonly repeat?: boolean;
|
|
13
|
-
}
|
|
14
|
-
/** What a command takes as positional arguments. */
|
|
15
|
-
export type Positionals =
|
|
16
|
-
/** None. */
|
|
17
|
-
"none"
|
|
18
|
-
/** Exactly one memory id. */
|
|
19
|
-
| "id"
|
|
20
|
-
/** At most one text argument; stdin when absent. */
|
|
21
|
-
| "text"
|
|
22
|
-
/** A memory id, then at most one text argument; stdin when absent. */
|
|
23
|
-
| "id-text"
|
|
24
|
-
/** One or more words, joined by single spaces. */
|
|
25
|
-
| "words"
|
|
26
|
-
/** At most one file path. */
|
|
27
|
-
| "path";
|
|
28
|
-
export interface CommandSpec {
|
|
29
|
-
readonly name: string;
|
|
30
|
-
readonly positionals: Positionals;
|
|
31
|
-
readonly flags: readonly FlagSpec[];
|
|
32
|
-
/** One line for `relembra --help`. */
|
|
33
|
-
readonly usage: string;
|
|
34
|
-
}
|
|
35
|
-
export declare const COMMANDS: readonly CommandSpec[];
|
|
36
|
-
/** Flags every command takes. */
|
|
37
|
-
export declare const GLOBAL_FLAGS: readonly ["--json", "--help"];
|
|
38
|
-
/** The environment the CLI reads, in the order docs/CLI.md lists it. */
|
|
39
|
-
export declare const ENV_VARS: readonly ["RELEMBRA_HOME", "RELEMBRA_SCOPE", "RELEMBRA_GLOBAL_SCOPE", "RELEMBRA_AGENT", "RELEMBRA_HARNESS", "RELEMBRA_MODEL", "BUN_OPTIONS", "BUN_BYTECODE_ORDER_OUT", "PATH", "HOME", "XDG_CONFIG_HOME", "SystemRoot"];
|
|
40
|
-
export declare function usageText(): string;
|