@oortmem/core 0.3.0 → 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.
@@ -0,0 +1,38 @@
1
+ import { ANONYMOUS_AGENT, type Home } from './grant.js';
2
+ /**
3
+ * Agent identities on one machine (CON-62). Every host Oort is connected to
4
+ * acts as its own agent (`claude-code`, `codex`, `cursor`, …): they share the
5
+ * person's Planet, and each has a Star of its own that no other agent reads.
6
+ * An agent configured as a Star (`oort connect cursor --star`) writes new
7
+ * memories to its Star instead of the Planet. The configuration lives in
8
+ * `$OORT_HOME/agents.json` (0600).
9
+ */
10
+ /**
11
+ * Claude Code's agent id: what every local memory before agents was written as, and what the
12
+ * Claude Code hooks and transcript import always act as. A caller that names no agent is
13
+ * `ANONYMOUS_AGENT` (Planet only), never this.
14
+ */
15
+ export declare const DEFAULT_AGENT = "claude-code";
16
+ export { ANONYMOUS_AGENT };
17
+ /** Agents configured as Stars, under `OORT_HOME`. */
18
+ export declare const AGENTS_FILE = "agents.json";
19
+ /** A lowercase id: letters, digits, `.`, `_` and `-`, at most 64 characters, starting alphanumeric. */
20
+ export declare const AGENT_ID: RegExp;
21
+ /** A valid agent id that is not reserved (`anonymous` names a caller that named none). */
22
+ export declare const isAgentId: (s: string) => boolean;
23
+ export interface AgentSettings {
24
+ /** New memories land in this agent's Star (private to it) instead of the person's Planet. */
25
+ star: boolean;
26
+ }
27
+ /**
28
+ * The agents configured in `home`. A missing or unreadable file means none,
29
+ * so every agent writes to the Planet (as before agents existed).
30
+ */
31
+ export declare function readAgents(home: string): Record<string, AgentSettings>;
32
+ /**
33
+ * Records whether agent `id` keeps a Star (0600). Returns true when that changed anything. An agent
34
+ * that stops keeping one stays listed (`star: false`), so its Star is still found (export).
35
+ */
36
+ export declare function setAgentStar(home: string, id: string, star: boolean): boolean;
37
+ /** Where agent `id`'s new memories land. */
38
+ export declare function agentHome(home: string, id: string): Home;
package/dist/agents.js ADDED
@@ -0,0 +1,67 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { ANONYMOUS_AGENT } from './grant.js';
4
+ import { writePrivateFile } from './private-fs.js';
5
+ /**
6
+ * Agent identities on one machine (CON-62). Every host Oort is connected to
7
+ * acts as its own agent (`claude-code`, `codex`, `cursor`, …): they share the
8
+ * person's Planet, and each has a Star of its own that no other agent reads.
9
+ * An agent configured as a Star (`oort connect cursor --star`) writes new
10
+ * memories to its Star instead of the Planet. The configuration lives in
11
+ * `$OORT_HOME/agents.json` (0600).
12
+ */
13
+ /**
14
+ * Claude Code's agent id: what every local memory before agents was written as, and what the
15
+ * Claude Code hooks and transcript import always act as. A caller that names no agent is
16
+ * `ANONYMOUS_AGENT` (Planet only), never this.
17
+ */
18
+ export const DEFAULT_AGENT = 'claude-code';
19
+ export { ANONYMOUS_AGENT };
20
+ /** Agents configured as Stars, under `OORT_HOME`. */
21
+ export const AGENTS_FILE = 'agents.json';
22
+ /** A lowercase id: letters, digits, `.`, `_` and `-`, at most 64 characters, starting alphanumeric. */
23
+ export const AGENT_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
24
+ /** A valid agent id that is not reserved (`anonymous` names a caller that named none). */
25
+ export const isAgentId = (s) => AGENT_ID.test(s) && s !== ANONYMOUS_AGENT;
26
+ /**
27
+ * The agents configured in `home`. A missing or unreadable file means none,
28
+ * so every agent writes to the Planet (as before agents existed).
29
+ */
30
+ export function readAgents(home) {
31
+ let parsed;
32
+ try {
33
+ parsed = JSON.parse(fs.readFileSync(path.join(home, AGENTS_FILE), 'utf8'));
34
+ }
35
+ catch {
36
+ return {};
37
+ }
38
+ const agents = parsed?.agents;
39
+ if (typeof agents !== 'object' || agents === null || Array.isArray(agents))
40
+ return {};
41
+ const out = {};
42
+ for (const [id, v] of Object.entries(agents)) {
43
+ if (!isAgentId(id) || typeof v !== 'object' || v === null)
44
+ continue;
45
+ out[id] = { star: v.star === true };
46
+ }
47
+ return out;
48
+ }
49
+ /**
50
+ * Records whether agent `id` keeps a Star (0600). Returns true when that changed anything. An agent
51
+ * that stops keeping one stays listed (`star: false`), so its Star is still found (export).
52
+ */
53
+ export function setAgentStar(home, id, star) {
54
+ if (!isAgentId(id))
55
+ throw new RangeError(`not an agent id: ${JSON.stringify(id)}`);
56
+ const agents = readAgents(home);
57
+ if ((agents[id]?.star ?? false) === star)
58
+ return false;
59
+ agents[id] = { star };
60
+ writePrivateFile(path.join(home, AGENTS_FILE), `${JSON.stringify({ version: 1, agents }, null, 2)}\n`);
61
+ return true;
62
+ }
63
+ /** Where agent `id`'s new memories land. */
64
+ export function agentHome(home, id) {
65
+ return readAgents(home)[id]?.star === true ? 'star' : 'planet';
66
+ }
67
+ //# sourceMappingURL=agents.js.map
package/dist/config.d.ts CHANGED
@@ -42,6 +42,7 @@ export declare const OortConfig: z.ZodObject<{
42
42
  rerankBudgetMs: z.ZodDefault<z.ZodNumber>;
43
43
  queryCacheTtlSec: z.ZodDefault<z.ZodNumber>;
44
44
  staleAfterDays: z.ZodDefault<z.ZodNumber>;
45
+ specificityPrior: z.ZodDefault<z.ZodNumber>;
45
46
  }, z.core.$strict>>;
46
47
  project: z.ZodDefault<z.ZodObject<{
47
48
  id: z.ZodOptional<z.ZodString>;
package/dist/config.js CHANGED
@@ -69,6 +69,12 @@ const recall = z
69
69
  * marked "may be outdated" when injected, and listed by `oort revalidate`. 0 turns it off.
70
70
  */
71
71
  staleAfterDays: z.number().int().min(0).default(30),
72
+ /**
73
+ * The specificity prior ε (spec 7.2): when a recall reads several nodes, each fused score is
74
+ * scaled by 1 + ε × specificity / 3 (an agent's Star 3, the Planet 2, a Constellation 1, a
75
+ * Galaxy 0). 0, the default, turns it off: it is turned on only if the eval shows a gain.
76
+ */
77
+ specificityPrior: z.number().min(0).max(1).default(0),
72
78
  })
73
79
  .default({
74
80
  lexicalK: 50,
@@ -84,6 +90,7 @@ const recall = z
84
90
  rerankBudgetMs: 400,
85
91
  queryCacheTtlSec: 600,
86
92
  staleAfterDays: 30,
93
+ specificityPrior: 0,
87
94
  });
88
95
  const project = z
89
96
  .strictObject({
@@ -37,6 +37,10 @@ export declare const COLD_EVICT_BATCH = 500;
37
37
  * left in its current tier (hot or warm); consolidate never promotes cold to warm/hot, since
38
38
  * hydration is recall's job.
39
39
  * Idempotent for a fixed `now`.
40
+ *
41
+ * Each node the scope's grant covers (the person's Planet, this agent's Star)
42
+ * is consolidated on its own, so the Planet's tiers do not depend on which of
43
+ * the person's agents ran consolidation.
40
44
  */
41
45
  export declare function consolidate(scope: ScopeFilter, deps: {
42
46
  store: MemoryStore;
@@ -1,5 +1,6 @@
1
1
  import { activation, tierFor } from './activation.js';
2
- import { localGrant } from './grant.js';
2
+ import { nodeKey } from './address.js';
3
+ import { localGrant, narrowGrant, readableNodes } from './grant.js';
3
4
  import { isCurrent } from './model.js';
4
5
  /** Evictions per `coldPutMany` call: one store transaction and one directory sync each. */
5
6
  export const COLD_EVICT_BATCH = 500;
@@ -23,10 +24,14 @@ const RANK = { cold: 0, warm: 1, hot: 2 };
23
24
  * left in its current tier (hot or warm); consolidate never promotes cold to warm/hot, since
24
25
  * hydration is recall's job.
25
26
  * Idempotent for a fixed `now`.
27
+ *
28
+ * Each node the scope's grant covers (the person's Planet, this agent's Star)
29
+ * is consolidated on its own, so the Planet's tiers do not depend on which of
30
+ * the person's agents ran consolidation.
26
31
  */
27
32
  export async function consolidate(scope, deps) {
28
33
  const { store, config } = deps;
29
- const grant = localGrant(scope);
34
+ const full = localGrant(scope);
30
35
  const now = deps.now ?? new Date();
31
36
  const result = {
32
37
  promoted: [],
@@ -35,6 +40,14 @@ export async function consolidate(scope, deps) {
35
40
  expired: [],
36
41
  skipped: 0,
37
42
  };
43
+ // Only nodes this grant may change: a Star agent's grant reads the Planet but does not write it.
44
+ const writable = new Set(full.writeNodes.map((p) => nodeKey(p.node)));
45
+ for (const node of readableNodes(full).filter((n) => writable.has(nodeKey(n)))) {
46
+ await consolidateNode(store, narrowGrant(full, [node]), config, now, result);
47
+ }
48
+ return result;
49
+ }
50
+ async function consolidateNode(store, grant, config, now, result) {
38
51
  // 1. Expire.
39
52
  const due = (await store.list(grant, { validOnly: false })).filter((m) => m.validTo !== null && m.expiredAt === null && Date.parse(m.validTo) <= now.getTime());
40
53
  if (due.length > 0) {
@@ -76,7 +89,6 @@ export async function consolidate(scope, deps) {
76
89
  evictions.push(m);
77
90
  }
78
91
  await evictAll(store, grant, evictions, result);
79
- return result;
80
92
  }
81
93
  async function evictAll(store, grant, evictions, result) {
82
94
  for (let i = 0; i < evictions.length; i += COLD_EVICT_BATCH) {
package/dist/grant.d.ts CHANGED
@@ -27,15 +27,26 @@ export interface GrantFilter extends Readonly<Principal> {
27
27
  * One node a grant covers (CON-61): the node, the project bindings inside it,
28
28
  * and optionally one author. Matching is structural and exact on every field
29
29
  * (never a prefix). `authorAgentId` restricts the pattern to memories that
30
- * agent wrote; the pre-level constructors (`localGrant`, `principalGrant`)
31
- * set it to the principal's agent, so they see exactly what they saw before
32
- * levels existed.
30
+ * agent wrote. The local constructors (`localGrant`, `principalGrant`) name
31
+ * the principal's Planet with no author restriction (a person's agents share
32
+ * their Planet, CON-62) and the principal's agent's own Star.
33
33
  */
34
34
  export interface NodePattern {
35
35
  readonly node: NodeRef;
36
36
  readonly projectIds: GrantProjects;
37
37
  readonly authorAgentId?: string;
38
38
  }
39
+ /**
40
+ * Where an agent's new memories land by default (its home node, spec 5.1):
41
+ * the person's Planet, or the agent's own Star when it is configured as one.
42
+ */
43
+ export type Home = 'planet' | 'star';
44
+ /**
45
+ * The agent id of a caller that named no agent (an MCP entry written before agents existed, a
46
+ * terminal without `--agent`). It reads and writes the person's Planet only: it has no Star, so
47
+ * it can neither see nor create one. No configured agent may use this id.
48
+ */
49
+ export declare const ANONYMOUS_AGENT = "anonymous";
39
50
  /**
40
51
  * The permission every `MemoryStore` method takes. Opaque: built only by
41
52
  * `localGrant`, `principalGrant` or `nodeGrant`, frozen, and nominal, so a raw
@@ -58,24 +69,48 @@ export declare class Grant {
58
69
  readonly write: GrantFilter;
59
70
  readonly readNodes: readonly NodePattern[];
60
71
  readonly writeNodes: readonly NodePattern[];
72
+ /**
73
+ * The node new memories are written to (the home node): the principal's
74
+ * Planet unless the agent keeps a Star. Engine code that creates a memory
75
+ * (observe, remember) addresses it here; edits of existing memories keep
76
+ * their own address. Covered by `writeNodes` whenever they are not empty.
77
+ */
78
+ readonly home: NodeRef;
61
79
  private constructor();
62
80
  /** @internal Use `localGrant` or `principalGrant`. */
63
- static issue(principal: Principal, read: GrantProjects, write: GrantProjects): Grant;
64
- /** @internal Use `nodeGrant`. */
65
- static issueNodes(principal: Principal, read: readonly NodePattern[], write: readonly NodePattern[]): Grant;
81
+ static issue(principal: Principal, read: GrantProjects, write: GrantProjects, home?: Home): Grant;
82
+ /** @internal Use `nodeGrant` or `narrowGrant`. */
83
+ static issueNodes(principal: Principal, read: readonly NodePattern[], write: readonly NodePattern[], home?: NodeRef): Grant;
66
84
  /** True for a real grant (not a copy, not a look-alike). */
67
85
  static is(x: unknown): x is Grant;
68
86
  }
87
+ /** The Star of a principal's agent (owned by the principal's owner). */
88
+ export declare function starNode(p: Pick<Principal, 'orgId' | 'agentId'>): NodeRef;
69
89
  /**
70
- * The grant for today's single-principal callers: read exactly what `read`
71
- * names (a one-node grant), and write the same unless `opts.write` narrows it.
72
- * `write` must name the same principal as `read`.
90
+ * The grant for one agent of one person (CON-62): read and write the
91
+ * person's Planet (memories any of their agents wrote) and this agent's own
92
+ * Star, in the projects `read` names; `opts.write` may narrow the write side.
93
+ * Another agent's Star is never covered. `opts.home` picks where new memories
94
+ * land: the Planet (default) or the agent's Star; with `star` the Planet is
95
+ * read-only (the agent writes only its Star). `ANONYMOUS_AGENT` gets the
96
+ * Planet alone. `write` must name the same principal as `read`.
97
+ *
98
+ * Before CON-62 the Planet side was restricted to memories the principal's
99
+ * own agent wrote. Every local memory so far was written as `claude-code`,
100
+ * so a local store sees exactly what it saw; nothing is rewritten.
73
101
  */
74
102
  export declare function localGrant(read: ScopeFilter, opts?: {
75
103
  write?: ScopeFilter;
104
+ home?: Home;
105
+ }): Grant;
106
+ /**
107
+ * Every project of `p` (and its global memories), for read and write:
108
+ * maintenance such as export. Covers the same nodes as `localGrant`: the
109
+ * person's Planet and this agent's own Star.
110
+ */
111
+ export declare function principalGrant(p: Principal, opts?: {
112
+ home?: Home;
76
113
  }): Grant;
77
- /** Every project of `p` (and its global memories), for read and write: maintenance such as export. */
78
- export declare function principalGrant(p: Principal): Grant;
79
114
  /**
80
115
  * A grant over explicit node patterns, possibly across owners and levels:
81
116
  * `read` bounds what is seen, `write` (default: `read`) what may change.
@@ -88,7 +123,19 @@ export declare function nodeGrant(opts: {
88
123
  principal: Principal;
89
124
  read: readonly NodePattern[];
90
125
  write?: readonly NodePattern[];
126
+ /** Where new memories land (default: the principal's Planet if written, else the first node written). */
127
+ home?: NodeRef;
91
128
  }): Grant;
129
+ /**
130
+ * `g` narrowed to the nodes `keep` names (by `nodeKey`), on both sides: the
131
+ * grant recall uses for one node's first stage. Never wider than `g`. The
132
+ * home stays when it is kept, else the default for what remains.
133
+ */
134
+ export declare function narrowGrant(g: Grant, keep: readonly NodeRef[]): Grant;
135
+ /** The distinct nodes `g` reads, in the order its patterns name them. */
136
+ export declare function readableNodes(g: Grant): NodeRef[];
137
+ /** The address a new memory gets under `g`: its home node, bound to `projectId` when given. */
138
+ export declare function homeAddress(g: Grant, projectId?: string): Address;
92
139
  /** Rejects anything that is not a real `Grant`; every store calls it first. */
93
140
  export declare function assertGrant(g: unknown): asserts g is Grant;
94
141
  /**
package/dist/grant.js CHANGED
@@ -1,4 +1,10 @@
1
1
  import { addressOf, assertNode, authorOf, nodeKey, nodeOf, planetOf, sameAddress, sameAuthor, } from './address.js';
2
+ /**
3
+ * The agent id of a caller that named no agent (an MCP entry written before agents existed, a
4
+ * terminal without `--agent`). It reads and writes the person's Planet only: it has no Star, so
5
+ * it can neither see nor create one. No configured agent may use this id.
6
+ */
7
+ export const ANONYMOUS_AGENT = 'anonymous';
2
8
  /**
3
9
  * The permission every `MemoryStore` method takes. Opaque: built only by
4
10
  * `localGrant`, `principalGrant` or `nodeGrant`, frozen, and nominal, so a raw
@@ -21,10 +27,18 @@ export class Grant {
21
27
  write;
22
28
  readNodes;
23
29
  writeNodes;
24
- constructor(principal, readNodes, writeNodes) {
30
+ /**
31
+ * The node new memories are written to (the home node): the principal's
32
+ * Planet unless the agent keeps a Star. Engine code that creates a memory
33
+ * (observe, remember) addresses it here; edits of existing memories keep
34
+ * their own address. Covered by `writeNodes` whenever they are not empty.
35
+ */
36
+ home;
37
+ constructor(principal, readNodes, writeNodes, home) {
25
38
  this.principal = Object.freeze({ ...principal });
26
39
  this.readNodes = Object.freeze(readNodes.map(freezePattern));
27
40
  this.writeNodes = Object.freeze(writeNodes.map(freezePattern));
41
+ this.home = Object.freeze(homeNode(principal, this.writeNodes, home));
28
42
  this.read = Object.freeze({
29
43
  ...principal,
30
44
  projectIds: legacyProjects(principal, this.readNodes),
@@ -36,14 +50,27 @@ export class Grant {
36
50
  Object.freeze(this);
37
51
  }
38
52
  /** @internal Use `localGrant` or `principalGrant`. */
39
- static issue(principal, read, write) {
53
+ static issue(principal, read, write, home = 'planet') {
40
54
  checkPrincipal(principal);
41
- return new Grant(principal, [ownPlanet(principal, read)], [ownPlanet(principal, write)]);
55
+ const anonymous = principal.agentId === ANONYMOUS_AGENT;
56
+ if (anonymous && home === 'star')
57
+ throw new RangeError('grant: an anonymous agent has no Star');
58
+ const readSide = anonymous
59
+ ? [ownPlanet(principal, read)]
60
+ : [ownPlanet(principal, read), ownStar(principal, read)];
61
+ // An agent whose home is its Star may read the shared Planet but never change it: its writes
62
+ // (new memories, forget, pin, tiers, confirmations) stay in its Star.
63
+ const writeSide = home === 'star'
64
+ ? [ownStar(principal, write)]
65
+ : anonymous
66
+ ? [ownPlanet(principal, write)]
67
+ : [ownPlanet(principal, write), ownStar(principal, write)];
68
+ return new Grant(principal, readSide, writeSide, home === 'star' ? starNode(principal) : planetNode(principal));
42
69
  }
43
- /** @internal Use `nodeGrant`. */
44
- static issueNodes(principal, read, write) {
70
+ /** @internal Use `nodeGrant` or `narrowGrant`. */
71
+ static issueNodes(principal, read, write, home) {
45
72
  checkPrincipal(principal);
46
- return new Grant(principal, read, write);
73
+ return new Grant(principal, read, write, home);
47
74
  }
48
75
  /** True for a real grant (not a copy, not a look-alike). */
49
76
  static is(x) {
@@ -57,13 +84,41 @@ function checkPrincipal(principal) {
57
84
  }
58
85
  }
59
86
  }
60
- /** The pre-level grant side: the principal's own Planet, written by its own agent. */
87
+ /**
88
+ * The local grant side's Planet: the principal's own Planet, whichever of the
89
+ * person's agents wrote the memory (CON-62; before it, the pattern was
90
+ * restricted to the principal's own agent).
91
+ */
61
92
  function ownPlanet(p, projectIds) {
62
- return {
63
- node: { level: 'user', orgId: p.orgId, customerId: p.customerId },
64
- projectIds,
65
- authorAgentId: p.agentId,
66
- };
93
+ return { node: planetNode(p), projectIds };
94
+ }
95
+ /** The principal's agent's own Star: private to that agent, never another's. */
96
+ function ownStar(p, projectIds) {
97
+ return { node: starNode(p), projectIds };
98
+ }
99
+ /** The Star of a principal's agent (owned by the principal's owner). */
100
+ export function starNode(p) {
101
+ return { level: 'agent', orgId: p.orgId, agentId: p.agentId };
102
+ }
103
+ /**
104
+ * `home` checked against the write side, or the default: the principal's
105
+ * Planet when the write side names it, else the first node written, else the
106
+ * Planet (a read-only grant writes nothing anyway).
107
+ */
108
+ function homeNode(p, write, home) {
109
+ if (home !== undefined) {
110
+ assertNode(home);
111
+ const { level: _l, ...rest } = home;
112
+ const node = { level: home.level, ...rest };
113
+ if (write.length > 0 && !write.some((w) => nodeKey(w.node) === nodeKey(node))) {
114
+ throw new RangeError('grant: home must be a node the grant writes');
115
+ }
116
+ return node;
117
+ }
118
+ const planet = planetNode(p);
119
+ if (write.length === 0 || write.some((w) => nodeKey(w.node) === nodeKey(planet)))
120
+ return planet;
121
+ return write[0].node;
67
122
  }
68
123
  function freezePattern(p) {
69
124
  if (typeof p !== 'object' || p === null)
@@ -83,12 +138,13 @@ function freezePattern(p) {
83
138
  }
84
139
  /**
85
140
  * The projects of `patterns` that a pre-level reader may assume: those of
86
- * patterns naming exactly the principal's Planet, restricted to its agent.
87
- * Never wider than the patterns themselves.
141
+ * patterns naming exactly the principal's Planet and covering its agent's
142
+ * writing (no author restriction, or that agent). Never wider than the
143
+ * patterns themselves.
88
144
  */
89
145
  function legacyProjects(p, patterns) {
90
146
  const own = nodeKey({ level: 'user', orgId: p.orgId, customerId: p.customerId });
91
- const mine = patterns.filter((x) => x.authorAgentId === p.agentId && nodeKey(x.node) === own);
147
+ const mine = patterns.filter((x) => (x.authorAgentId === undefined || x.authorAgentId === p.agentId) && nodeKey(x.node) === own);
92
148
  if (mine.some((x) => x.projectIds === 'all'))
93
149
  return 'all';
94
150
  return Object.freeze([
@@ -111,9 +167,17 @@ function samePrincipal(a, b) {
111
167
  return a.orgId === b.orgId && a.customerId === b.customerId && a.agentId === b.agentId;
112
168
  }
113
169
  /**
114
- * The grant for today's single-principal callers: read exactly what `read`
115
- * names (a one-node grant), and write the same unless `opts.write` narrows it.
116
- * `write` must name the same principal as `read`.
170
+ * The grant for one agent of one person (CON-62): read and write the
171
+ * person's Planet (memories any of their agents wrote) and this agent's own
172
+ * Star, in the projects `read` names; `opts.write` may narrow the write side.
173
+ * Another agent's Star is never covered. `opts.home` picks where new memories
174
+ * land: the Planet (default) or the agent's Star; with `star` the Planet is
175
+ * read-only (the agent writes only its Star). `ANONYMOUS_AGENT` gets the
176
+ * Planet alone. `write` must name the same principal as `read`.
177
+ *
178
+ * Before CON-62 the Planet side was restricted to memories the principal's
179
+ * own agent wrote. Every local memory so far was written as `claude-code`,
180
+ * so a local store sees exactly what it saw; nothing is rewritten.
117
181
  */
118
182
  export function localGrant(read, opts = {}) {
119
183
  const write = opts.write ?? read;
@@ -124,11 +188,15 @@ export function localGrant(read, opts = {}) {
124
188
  if (!samePrincipal(read, write)) {
125
189
  throw new RangeError('localGrant: read and write must name the same org, customer and agent');
126
190
  }
127
- return Grant.issue(read, read.projectIds, write.projectIds);
191
+ return Grant.issue({ orgId: read.orgId, customerId: read.customerId, agentId: read.agentId }, read.projectIds, write.projectIds, opts.home);
128
192
  }
129
- /** Every project of `p` (and its global memories), for read and write: maintenance such as export. */
130
- export function principalGrant(p) {
131
- return Grant.issue(p, 'all', 'all');
193
+ /**
194
+ * Every project of `p` (and its global memories), for read and write:
195
+ * maintenance such as export. Covers the same nodes as `localGrant`: the
196
+ * person's Planet and this agent's own Star.
197
+ */
198
+ export function principalGrant(p, opts = {}) {
199
+ return Grant.issue({ orgId: p.orgId, customerId: p.customerId, agentId: p.agentId }, 'all', 'all', opts.home);
132
200
  }
133
201
  /**
134
202
  * A grant over explicit node patterns, possibly across owners and levels:
@@ -142,7 +210,37 @@ export function nodeGrant(opts) {
142
210
  if (!Array.isArray(opts.read) || (opts.write !== undefined && !Array.isArray(opts.write))) {
143
211
  throw new TypeError('nodeGrant: read and write must be arrays of node patterns');
144
212
  }
145
- return Grant.issueNodes(opts.principal, opts.read, opts.write ?? opts.read);
213
+ return Grant.issueNodes(opts.principal, opts.read, opts.write ?? opts.read, opts.home);
214
+ }
215
+ /**
216
+ * `g` narrowed to the nodes `keep` names (by `nodeKey`), on both sides: the
217
+ * grant recall uses for one node's first stage. Never wider than `g`. The
218
+ * home stays when it is kept, else the default for what remains.
219
+ */
220
+ export function narrowGrant(g, keep) {
221
+ assertGrant(g);
222
+ const keys = new Set(keep.map((n) => nodeKey(n)));
223
+ const read = g.readNodes.filter((p) => keys.has(nodeKey(p.node)));
224
+ const write = g.writeNodes.filter((p) => keys.has(nodeKey(p.node)));
225
+ const home = write.some((p) => nodeKey(p.node) === nodeKey(g.home)) ? g.home : undefined;
226
+ return Grant.issueNodes(g.principal, read, write, home);
227
+ }
228
+ /** The distinct nodes `g` reads, in the order its patterns name them. */
229
+ export function readableNodes(g) {
230
+ assertGrant(g);
231
+ const seen = new Map();
232
+ for (const p of g.readNodes)
233
+ if (!seen.has(nodeKey(p.node)))
234
+ seen.set(nodeKey(p.node), p.node);
235
+ return [...seen.values()];
236
+ }
237
+ /** The address a new memory gets under `g`: its home node, bound to `projectId` when given. */
238
+ export function homeAddress(g, projectId) {
239
+ assertGrant(g);
240
+ return {
241
+ ...g.home,
242
+ ...(projectId !== undefined && { projectId }),
243
+ };
146
244
  }
147
245
  /** Rejects anything that is not a real `Grant`; every store calls it first. */
148
246
  export function assertGrant(g) {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.3.0";
1
+ export declare const VERSION = "0.4.0";
2
2
  export * from './model.js';
3
3
  export * from './address.js';
4
4
  export * from './export-format.js';
@@ -8,6 +8,8 @@ export * from './activation.js';
8
8
  export * from './adapters.js';
9
9
  export * from './embed-cache.js';
10
10
  export * from './grant.js';
11
+ export * from './levels.js';
12
+ export * from './agents.js';
11
13
  export * from './fusion.js';
12
14
  export * from './memory-store.js';
13
15
  export * from './tokenize.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION = '0.3.0';
1
+ export const VERSION = '0.4.0';
2
2
  export * from './model.js';
3
3
  export * from './address.js';
4
4
  export * from './export-format.js';
@@ -8,6 +8,8 @@ export * from './activation.js';
8
8
  export * from './adapters.js';
9
9
  export * from './embed-cache.js';
10
10
  export * from './grant.js';
11
+ export * from './levels.js';
12
+ export * from './agents.js';
11
13
  export * from './fusion.js';
12
14
  export * from './memory-store.js';
13
15
  export * from './tokenize.js';
@@ -10,8 +10,14 @@ import { type Grant } from './grant.js';
10
10
  import type { Memory, Scope } from './model.js';
11
11
  /** A memory for the isolation checks: text, stub and entities all carry `marker`. */
12
12
  export declare function isoMemory(id: string, scope: Scope, marker: string, embedding?: Int8Array): Memory;
13
- /** The narrowest grant for one scope (its principal and its one project or global). */
13
+ /**
14
+ * The narrowest grant for one scope: its Planet, its one project (or global),
15
+ * memories its own agent wrote. (The pre-CON-62 local grant; today's
16
+ * `localGrant` also covers the Planet's other authors and the agent's Star.)
17
+ */
14
18
  export declare function soleGrant(s: Scope): Grant;
19
+ /** The local grant of `ISO_SCOPES.mine`'s agent for its one project: Planet plus its own Star. */
20
+ export declare function defaultGrant(s: Scope): Grant;
15
21
  /** The narrowest grant for one memory at any level: its node, its one project, its author. */
16
22
  export declare function soleGrantFor(m: Pick<Memory, 'scope' | 'address' | 'author'>): Grant;
17
23
  /** `m` moved to `scope`: its address and author dropped, so the scope's Planet is the address. */
@@ -59,13 +65,20 @@ export declare const ISO_SCOPES: {
59
65
  /**
60
66
  * Victims at the other levels, each next to the caller's own Planet: a Planet
61
67
  * memory another of the caller's agents wrote, the caller's agent's own Star,
62
- * an Orbit, a Flyby, a Constellation and a Galaxy root of an organisation, and
63
- * one of the caller's own owner. A pre-level grant sees none of them.
68
+ * another agent's Star (in the caller's project and global), an Orbit, a
69
+ * Flyby, a Constellation and a Galaxy root of an organisation, and one of the
70
+ * caller's own owner. The narrowest grant sees none of them; the local grant
71
+ * sees only those in `DEFAULT_COVERS`.
64
72
  */
65
73
  export declare const ISO_LEVELS: Record<string, {
66
74
  address: Address;
67
75
  author: Author;
68
76
  }>;
77
+ /**
78
+ * The victims the local grant of `ISO_SCOPES.mine` legitimately covers (CON-62): Planet memories
79
+ * the person's other agents wrote, and its agent's own Star. Never another agent's Star.
80
+ */
81
+ export declare const DEFAULT_COVERS: readonly string[];
69
82
  /**
70
83
  * Every `MemoryStore` method, called with a grant for `ISO_SCOPES.mine`
71
84
  * against memories (warm and cold) in each other scope: nothing is seen,
@@ -73,6 +86,10 @@ export declare const ISO_LEVELS: Record<string, {
73
86
  * apart. Also the write-side rules (`GrantError` for an out-of-grant scope
74
87
  * and for an id that exists elsewhere, atomic batches, address changes),
75
88
  * read/write splits, cursor namespaces and rejection of non-grants.
89
+ *
90
+ * Run twice: as the narrowest grant (every victim out of reach), and as the
91
+ * local grant (`localGrant`, Planet plus own Star), for which every victim
92
+ * but `DEFAULT_COVERS` is out of reach, other agents' Stars included.
76
93
  */
77
94
  export declare function isolationViolations(make: () => Promise<MemoryStore>): Promise<string[]>;
78
95
  /** A small seeded PRNG (mulberry32), so a failing seed replays exactly. */
@@ -94,7 +111,13 @@ export declare function seededRng(seed: number): {
94
111
  * `multiNode`: A's grant spans its Planet, its agent's Star and a
95
112
  * Constellation of another owner; B's spans its own Planet and a sibling
96
113
  * Constellation. Same property.
114
+ *
115
+ * `peers` (CON-62): A and B are two agents of one person, each with the local
116
+ * grant (the shared Planet plus its own Star). A writes to the Planet and its
117
+ * Star; B writes only to its own Star and aims every method at A's Star ids.
118
+ * Nothing B does through its Star reaches A.
97
119
  */
98
120
  export declare function nonInterference(make: () => Promise<MemoryStore>, seed: number, steps: number, opts?: {
99
121
  multiNode?: boolean;
122
+ peers?: boolean;
100
123
  }): Promise<string | null>;