@oortmem/core 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/levels.js ADDED
@@ -0,0 +1,140 @@
1
+ import { addressOf, authorOf, GALAXY_ROOT, nodeKey, nodeOf, spaceName, } from './address.js';
2
+ import { readableNodes } from './grant.js';
3
+ export const INCLUDE_LEVELS = Object.freeze([
4
+ 'self',
5
+ 'planet',
6
+ 'star',
7
+ 'constellation',
8
+ 'galaxy',
9
+ ]);
10
+ export const isIncludeLevel = (s) => INCLUDE_LEVELS.includes(s);
11
+ /** At most this many hive nodes are read by default (spec 7.1: one recall, a bounded fan-out). */
12
+ export const DEFAULT_HIVE_NODES = 4;
13
+ /**
14
+ * A recall asked for a level its grant does not cover. Typed, so callers tell
15
+ * "you may not read that" apart from "nothing matched" (spec 7.1).
16
+ */
17
+ export class ScopeDeniedError extends Error {
18
+ code = 'scope_denied';
19
+ levels;
20
+ constructor(levels) {
21
+ super(`scope_denied: this grant cannot read ${levels.join(', ')}`);
22
+ this.name = 'ScopeDeniedError';
23
+ this.levels = levels;
24
+ }
25
+ }
26
+ /** True when `node` is what `level` names for a caller whose grant is `g`. */
27
+ function names(level, node, g) {
28
+ switch (level) {
29
+ case 'self':
30
+ return nodeKey(node) === nodeKey(g.home);
31
+ case 'planet':
32
+ return node.level === 'user';
33
+ case 'star':
34
+ return (node.level === 'agent' &&
35
+ node.orgId === g.principal.orgId &&
36
+ node.agentId === g.principal.agentId);
37
+ case 'constellation':
38
+ return node.level === 'hive' && node.hiveId !== GALAXY_ROOT;
39
+ case 'galaxy':
40
+ return node.level === 'hive' && node.hiveId === GALAXY_ROOT;
41
+ }
42
+ }
43
+ /**
44
+ * The nodes one recall reads: with `include`, every readable node a named
45
+ * level covers (a level that covers none rejects with `ScopeDeniedError`);
46
+ * without it, every readable node, hive nodes capped at `DEFAULT_HIVE_NODES`
47
+ * in grant order. Never a node outside `g`.
48
+ */
49
+ export function nodesToRead(g, include) {
50
+ const readable = readableNodes(g);
51
+ if (include === undefined || include.length === 0) {
52
+ let hives = 0;
53
+ return readable.filter((n) => n.level !== 'hive' || ++hives <= DEFAULT_HIVE_NODES);
54
+ }
55
+ for (const l of include) {
56
+ if (!isIncludeLevel(l))
57
+ throw new RangeError(`unknown level ${JSON.stringify(l)}`);
58
+ }
59
+ const denied = [...new Set(include)].filter((l) => !readable.some((n) => names(l, n, g)));
60
+ if (denied.length > 0)
61
+ throw new ScopeDeniedError(denied);
62
+ return readable.filter((n) => include.some((l) => names(l, n, g)));
63
+ }
64
+ /**
65
+ * How specific a node is, for the specificity prior and the conflict rule:
66
+ * an agent's own memory (Star, Orbit, Flyby) 3, the person's Planet 2, a
67
+ * Constellation 1, a Galaxy 0. Narrower is higher.
68
+ */
69
+ export function specificity(node) {
70
+ switch (node.level) {
71
+ case 'agent':
72
+ case 'agent-user':
73
+ case 'run':
74
+ return 3;
75
+ case 'user':
76
+ return 2;
77
+ case 'hive':
78
+ return node.hiveId === GALAXY_ROOT ? 0 : 1;
79
+ }
80
+ }
81
+ /**
82
+ * The specificity prior's factor for a node: `1 + ε × specificity / 3`, so
83
+ * with `ε` the narrowest level gains `ε` and a Galaxy nothing. `ε = 0` (the
84
+ * default) leaves every score as it was.
85
+ */
86
+ export function specificityFactor(node, epsilon) {
87
+ return 1 + (epsilon * specificity(node)) / 3;
88
+ }
89
+ /** Brackets in memory text, escaped so the text cannot pass for a code-built label. */
90
+ export function escapeLabelText(text) {
91
+ return text.replace(/[[\]]/g, (c) => `\\${c}`);
92
+ }
93
+ /** An id shown inside a label: control characters dropped, brackets escaped, clipped. */
94
+ function labelId(id) {
95
+ // oxlint-disable-next-line no-control-regex
96
+ const clean = escapeLabelText(id.replace(/[\u0000-\u001f\u007f]/g, ''));
97
+ return clean.length > 40 ? `${clean.slice(0, 39)}…` : clean;
98
+ }
99
+ /** The node part of a label for `node`, as the caller of grant `g` sees it. */
100
+ export function nodeLabel(node, g) {
101
+ switch (node.level) {
102
+ case 'user':
103
+ return node.orgId === g.principal.orgId && node.customerId === g.principal.customerId
104
+ ? 'your Planet'
105
+ : 'a Planet';
106
+ case 'agent':
107
+ return `Star ${labelId(node.agentId)}`;
108
+ case 'agent-user':
109
+ return `Orbit ${labelId(node.agentId)}`;
110
+ case 'run':
111
+ return `Flyby ${labelId(node.runId)}`;
112
+ case 'hive':
113
+ return node.hiveId === GALAXY_ROOT ? 'Galaxy' : `Constellation ${labelId(node.hiveId)}`;
114
+ }
115
+ }
116
+ /** The origin of `m` for the caller of grant `g`. */
117
+ export function originOf(m, g) {
118
+ const node = nodeOf(addressOf(m));
119
+ const label = nodeLabel(node, g);
120
+ const ownStar = node.level === 'agent' &&
121
+ node.orgId === g.principal.orgId &&
122
+ node.agentId === g.principal.agentId;
123
+ return {
124
+ level: node.level,
125
+ space: spaceName(node),
126
+ node: label,
127
+ from: ownStar ? "from this agent's Star" : `from ${label}`,
128
+ home: nodeKey(node) === nodeKey(g.home),
129
+ author: authorOf(m).agentId,
130
+ };
131
+ }
132
+ /** The decision `observe` records on a memory written instead of editing one in another node. */
133
+ export const CONTRADICTS = { stage: 'level', question: 'contradicts' };
134
+ /** The ids of memories in other nodes that `m` contradicts (recorded by `observe`). */
135
+ export function contradicted(m) {
136
+ return m.source.decisions
137
+ .filter((d) => d.stage === CONTRADICTS.stage && d.question === CONTRADICTS.question)
138
+ .map((d) => d.answer);
139
+ }
140
+ //# sourceMappingURL=levels.js.map
@@ -1,5 +1,6 @@
1
1
  import { appendAccess } from './activation.js';
2
- import { assertGrant, canRead, canWrite, cursorKey, filterCovers, GrantError, sameScope, } from './grant.js';
2
+ import { canonicalMemory } from './address.js';
3
+ import { assertGrant, cursorKey, grantReads, grantWrites, GrantError, sameRecordAddress, } from './grant.js';
3
4
  import { isCurrent } from './model.js';
4
5
  import { tokenizeLexical as tokens } from './tokenize.js';
5
6
  function cosine(a, b) {
@@ -46,26 +47,32 @@ export function createMemoryStore(opts = {}) {
46
47
  if (m.tier !== 'cold')
47
48
  cold.delete(m.id);
48
49
  }
49
- function scoped(f) {
50
- return [...main.values()].filter((m) => filterCovers(f, m.scope));
50
+ function scoped(grant) {
51
+ return [...main.values()].filter((m) => grantReads(grant, m));
51
52
  }
52
53
  /** The live record if `grant` may read it; `undefined` for unknown and out-of-grant alike. */
53
54
  function readable(grant, id) {
54
55
  const m = main.get(id);
55
- return m !== undefined && canRead(grant, m.scope) ? m : undefined;
56
+ return m !== undefined && grantReads(grant, m) ? m : undefined;
56
57
  }
57
58
  /** The live record if `grant` may change it. */
58
59
  function writable(grant, id) {
59
60
  const m = main.get(id);
60
- return m !== undefined && canWrite(grant, m.scope) ? m : undefined;
61
+ return m !== undefined && grantWrites(grant, m) ? m : undefined;
61
62
  }
62
- /** Every check `upsert` makes, before anything is written. */
63
- function checkWrite(grant, m) {
64
- if (!canWrite(grant, m.scope))
63
+ /**
64
+ * Every check `upsert` makes, before anything is written: a legal address
65
+ * that matches the scope (`RangeError` otherwise), inside `grant.write`,
66
+ * and the same address, author and scope as any record already under the id.
67
+ * Returns the record as stored.
68
+ */
69
+ function checkWrite(grant, m, existing) {
70
+ const c = canonicalMemory(m);
71
+ if (!grantWrites(grant, c))
65
72
  throw new GrantError();
66
- const existing = main.get(m.id);
67
- if (existing !== undefined && !sameScope(existing.scope, m.scope))
73
+ if (existing !== undefined && !sameRecordAddress(existing, c))
68
74
  throw new GrantError();
75
+ return c;
69
76
  }
70
77
  /** True while the live record still matches the snapshot a guarded write was computed from. */
71
78
  function unchanged(live, snap) {
@@ -116,8 +123,7 @@ export function createMemoryStore(opts = {}) {
116
123
  open();
117
124
  assertGrant(grant);
118
125
  const c = structuredClone(m);
119
- checkWrite(grant, c);
120
- put(c);
126
+ put(checkWrite(grant, c, main.get(c.id)));
121
127
  },
122
128
  /** Atomic trivially: checks and clones every record before writing any. */
123
129
  async upsertMany(grant, ms) {
@@ -127,15 +133,13 @@ export function createMemoryStore(opts = {}) {
127
133
  // Later entries may move an id the batch itself created: check against
128
134
  // the batch's own earlier entries as well as the store.
129
135
  const batch = new Map();
136
+ const checked = [];
130
137
  for (const c of cs) {
131
- if (!canWrite(grant, c.scope))
132
- throw new GrantError();
133
- const existing = batch.get(c.id) ?? main.get(c.id);
134
- if (existing !== undefined && !sameScope(existing.scope, c.scope))
135
- throw new GrantError();
136
- batch.set(c.id, c);
138
+ const ok = checkWrite(grant, c, batch.get(c.id) ?? main.get(c.id));
139
+ batch.set(c.id, ok);
140
+ checked.push(ok);
137
141
  }
138
- for (const c of cs)
142
+ for (const c of checked)
139
143
  put(c);
140
144
  },
141
145
  async get(grant, id) {
@@ -164,7 +168,7 @@ export function createMemoryStore(opts = {}) {
164
168
  if (query.size === 0)
165
169
  return [];
166
170
  const scored = [];
167
- for (const m of scoped(grant.read)) {
171
+ for (const m of scoped(grant)) {
168
172
  const fields = m.tier === 'cold' ? [m.stub, ...m.entities] : [m.text, m.stub, ...m.entities];
169
173
  const doc = new Set(tokens(fields.join(' ')));
170
174
  let score = 0;
@@ -180,7 +184,7 @@ export function createMemoryStore(opts = {}) {
180
184
  open();
181
185
  assertGrant(grant);
182
186
  const scored = [];
183
- for (const m of scoped(grant.read)) {
187
+ for (const m of scoped(grant)) {
184
188
  if (m.tier === 'cold' || m.embedding === undefined)
185
189
  continue;
186
190
  if (m.embedding.length !== v.length) {
@@ -204,7 +208,7 @@ export function createMemoryStore(opts = {}) {
204
208
  assertGrant(grant);
205
209
  const validOnly = listOpts.validOnly ?? true;
206
210
  const now = clock();
207
- return scoped(grant.read)
211
+ return scoped(grant)
208
212
  .filter((m) => !validOnly || isCurrent(m, now))
209
213
  .filter((m) => listOpts.tier === undefined || m.tier === listOpts.tier)
210
214
  .filter((m) => listOpts.pinned !== true || m.pinned)
@@ -225,7 +229,7 @@ export function createMemoryStore(opts = {}) {
225
229
  if (readable(grant, id) === undefined)
226
230
  return null;
227
231
  const m = cold.get(id);
228
- return m === undefined || !canRead(grant, m.scope) ? null : structuredClone(m);
232
+ return m === undefined || !grantReads(grant, m) ? null : structuredClone(m);
229
233
  },
230
234
  async setTier(grant, id, tier, snapshot) {
231
235
  open();
package/dist/model.d.ts CHANGED
@@ -1,5 +1,12 @@
1
+ import type { Address, Author, Provenance } from './address.js';
1
2
  export type MemoryType = 'semantic' | 'episodic' | 'procedural' | 'preference';
2
3
  export type Tier = 'hot' | 'warm' | 'cold';
4
+ /**
5
+ * The pre-level view of where a memory lives. For a Planet memory (every
6
+ * memory written before levels) it is the whole story: the Planet is
7
+ * (org, customer, project) and `agentId` is the author. For other levels see
8
+ * `scopeOf` in `address.ts`. Grants are checked against `address` and `author`.
9
+ */
3
10
  export interface Scope {
4
11
  orgId: string;
5
12
  customerId: string;
@@ -38,6 +45,16 @@ export interface MemorySource {
38
45
  export interface Memory {
39
46
  id: string;
40
47
  scope: Scope;
48
+ /**
49
+ * Where the memory lives. Absent: the Planet `scope` names, which is where
50
+ * every memory written before levels lives, so stores return it only for
51
+ * other levels. Read it with `addressOf(m)`.
52
+ */
53
+ address?: Address;
54
+ /** Who wrote it. Absent: the agent `scope.agentId` names. Read it with `authorOf(m)`. */
55
+ author?: Author;
56
+ /** Where a shared copy came from; absent (or null) on ordinary memories. */
57
+ provenance?: Provenance | null;
41
58
  type: MemoryType;
42
59
  text: string;
43
60
  stub: string;
package/dist/observe.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { DecisionController, Embedder, MemoryStore, MemoryWriter, Observation } from './adapters.js';
2
2
  import type { ResolvedConfig } from './config.js';
3
+ import { type Home } from './grant.js';
3
4
  import type { Decision, Scope } from './model.js';
4
5
  export type MemoryOutcomeKind = 'skipped' | 'reinforced' | 'added' | 'updated' | 'superseded' | 'corrected';
5
6
  /** What happened to one memory the turn produced. */
@@ -29,6 +30,14 @@ export interface ObserveDeps {
29
30
  now?: Date;
30
31
  /** Recorded on `source.harness`; defaults to "unknown". */
31
32
  harness?: string;
33
+ /**
34
+ * Where new memories land (spec 5.1): the person's Planet (default) or, for an agent configured
35
+ * as a Star, `scope.agentId`'s own Star. Candidates come from both. A memory is only ever
36
+ * superseded, corrected or refined in the home node, and an agent whose home is its Star never
37
+ * writes the Planet at all: a Planet memory it re-observes gets an access record (activation),
38
+ * never a confirmation or an edit.
39
+ */
40
+ home?: Home;
32
41
  }
33
42
  /**
34
43
  * Gate-stage reinforcement match floor. When the gate says "do not store", a
package/dist/observe.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { localGrant } from './grant.js';
2
+ import { addressed, addressOf, nodeKey, nodeOf } from './address.js';
3
+ import { grantWrites, homeAddress, localGrant } from './grant.js';
4
+ import { CONTRADICTS } from './levels.js';
3
5
  import { capObservation } from './cap.js';
4
6
  import { scrubObservation } from './scrub.js';
5
7
  import { checkWriterDate, factInterval, utcOffset } from './temporal.js';
@@ -125,11 +127,30 @@ async function apply(a) {
125
127
  if (target === undefined)
126
128
  return { outcome: 'skipped' };
127
129
  await store.recordAccess(grant, [target.id], now);
128
- await confirm(store, grant, target.id, now.toISOString());
130
+ // Confirming writes the memory: only where this agent may write (a Star agent reinforces a
131
+ // Planet memory's activation but never edits it).
132
+ if (grantWrites(grant, target))
133
+ await confirm(store, grant, target.id, now.toISOString());
129
134
  return { outcome: 'reinforced', memoryId: target.id };
130
135
  }
131
136
  let action = best.action === 'UPDATE' ? 'REFINE' : best.action;
132
137
  let prior = action === 'ADD' ? undefined : candidates.find((c) => c.id === best.candidateId);
138
+ if (prior !== undefined && nodeKey(nodeOf(addressOf(prior))) !== nodeKey(grant.home)) {
139
+ // The prior lives in another node (the Planet, for an agent writing to its Star, or the
140
+ // reverse). A write never edits a memory outside its home node: the observation becomes a
141
+ // memory of its own there. A contradiction is recorded, so recall can rank the narrower of the
142
+ // two first and label the other (spec 7.2). Reinforcing it (above) stays allowed.
143
+ decisions.push({
144
+ stage: 'level',
145
+ question: 'other-node',
146
+ answer: 'add-instead',
147
+ probability: 1,
148
+ });
149
+ if (action === 'SUPERSEDE' || action === 'CORRECT') {
150
+ decisions.push({ ...CONTRADICTS, answer: prior.id, probability: best.probability });
151
+ }
152
+ prior = undefined;
153
+ }
133
154
  if (prior !== undefined && (prior.scope.projectId ?? null) !== (scope.projectId ?? null)) {
134
155
  // Candidates include global memories. A project's observation must not end or
135
156
  // rewrite a fact another scope (every project, for a global one) relies on, so it
@@ -235,6 +256,9 @@ async function apply(a) {
235
256
  const base = {
236
257
  id,
237
258
  scope,
259
+ // A memory at another level than the Planet carries its address and author.
260
+ ...(grant.home.level !== 'user' &&
261
+ addressed(homeAddress(grant, scope.projectId), { agentId: scope.agentId })),
238
262
  type: w.type,
239
263
  text: w.text,
240
264
  stub: w.stub,
@@ -418,7 +442,7 @@ export async function observe(raw, scope, deps) {
418
442
  const obs = capObservation(scrubbed.obs);
419
443
  const { store, embed, decide, write } = deps;
420
444
  const now = deps.now ?? new Date();
421
- const grant = localGrant(filterFor(scope));
445
+ const grant = localGrant(filterFor(scope), deps.home === undefined ? {} : { home: deps.home });
422
446
  const g = await decide.gate(obs);
423
447
  const text = `${obs.userText}\n${obs.assistantText}`;
424
448
  if (g.shouldStore < GATE_THRESHOLD) {
package/dist/recall.d.ts CHANGED
@@ -1,10 +1,25 @@
1
1
  import type { Embedder, MemoryStore, Reranker, ScopeFilter } from './adapters.js';
2
2
  import type { ResolvedConfig } from './config.js';
3
+ import { type Grant, type Home } from './grant.js';
4
+ import { type IncludeLevel, type MemoryOrigin } from './levels.js';
3
5
  import { type Memory } from './model.js';
4
6
  import { type RecallMode } from './temporal.js';
5
7
  export interface RecallQuery {
6
8
  text: string;
7
- scope: ScopeFilter;
9
+ /**
10
+ * Who recalls, as a pre-level filter: read with `localGrant(scope, { home })`, the person's
11
+ * Planet plus this agent's own Star in these projects. Required unless `grant` is given.
12
+ */
13
+ scope?: ScopeFilter;
14
+ /** The grant to read with, instead of one built from `scope` (multi-node grants, tests). */
15
+ grant?: Grant;
16
+ /** With `scope`: where this agent's memories land (`star` for an agent configured as a Star). */
17
+ home?: Home;
18
+ /**
19
+ * Which levels to read (spec 7.1), intersected with the grant. A level the grant does not cover
20
+ * rejects with `ScopeDeniedError`. Default: every node the grant reads.
21
+ */
22
+ include?: IncludeLevel[];
8
23
  budgetChars?: number;
9
24
  /** ISO timestamp: recall what was true then (valid time). Implies `mode: 'as-of'`. */
10
25
  asOf?: string;
@@ -44,6 +59,8 @@ export interface RecalledMemory extends Memory {
44
59
  /** A volatile fact nobody confirmed within `recall.staleAfterDays`. */
45
60
  stale?: true;
46
61
  };
62
+ /** Where it came from: level, node, author, and whether a narrower memory overrides it. */
63
+ origin?: MemoryOrigin;
47
64
  }
48
65
  export type Degradation = 'no-embedder' | 'no-reranker' | 'embed-failed' | 'rerank-failed'
49
66
  /** The reranker returned no ids: the fused order stands. */
@@ -75,6 +92,8 @@ export interface RecallResult {
75
92
  embedSkipped?: 'lexical-confident';
76
93
  /** The temporal view this recall read. Always set by `recall`. */
77
94
  temporal?: RecallTemporal;
95
+ /** The nodes this recall read, as labels show them (`your Planet`, `Star cursor`). */
96
+ read?: string[];
78
97
  timings: Record<string, number>;
79
98
  }
80
99
  export interface RecallDeps {
@@ -107,3 +126,10 @@ export declare function fitForInjection<M extends RecalledMemory>(memories: M[],
107
126
  kept: M[];
108
127
  lines: string[];
109
128
  };
129
+ /**
130
+ * Memory text with any `<oort-…` or `</oort-…` tag opening written as `&lt;`, so a memory cannot
131
+ * close the `<oort-memory>` block (or open another) and have what follows read as the prompt.
132
+ */
133
+ export declare function escapeBlockTags(text: string): string;
134
+ /** The share of an injection budget hive memories may take (spec 7.2). */
135
+ export declare const HIVE_QUOTA = 0.4;