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 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.4, a beta ([changelog](CHANGELOG.md),
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.4"`); there is no `--version` flag. The [release procedure](docs/RELEASE.md) covers isolated checks and
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 an MCP server over stdio for clients that speak protocol revision 2026-07-28
242
- (older revisions are refused with the supported list). It is a subcommand of the same binary,
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. A client that speaks only the earlier,
263
- `initialize`-based revisions cannot use this server: `initialize` is refused with `-32022`, which
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
- Implemented from these pages of the specification (read 2026-10-31):
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 the answer to `initialize`. The revision has no lifecycle page:
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), and `initialize` is answered with
295
- `-32022` naming the supported version (a SHOULD for a server that speaks no legacy revision).
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]`
@@ -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
@@ -1,3 +1,5 @@
1
+ /** Every trust tier, highest first. */
2
+ export const TRUSTS = ["owner", "agent", "untrusted"];
1
3
  /**
2
4
  * The event format of the kinds format 1 defines (note, retract, redact, supersede, summarize),
3
5
  * which every event of those kinds carries (docs/FORMAT.md).
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 '-'");
@@ -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: MAX_QUERY },
70
- limit: { type: "integer", minimum: 1, maximum: MAX_LIMIT },
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", MAX_QUERY, true),
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: MAX_QUERY } },
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", MAX_QUERY),
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 ${MAX_LIMIT}` : ""}, and nothing else`;
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 > MAX_LIMIT) {
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 };
@@ -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
- }
@@ -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
- const MAX_FIND_LIMIT = 100;
12
- const MAX_QUERY_LENGTH = 1_000;
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.
@@ -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
- const header = readFileSync(path).subarray(0, 20);
346
- return header.length === 20 && header[18] === 2 && header[19] === 2;
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
@@ -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.4";
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.4";
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.4.
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.4`. Informational: a reader decides by `format` alone. |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relembra",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.6",
4
4
  "description": "A curated, scoped, bounded, durable, erasable and measured memory for AI agents.",
5
5
  "license": "MIT",
6
6
  "author": "mneves75",
@@ -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
- }
@@ -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;