@oortmem/core 0.3.0 → 0.5.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({
@@ -1,5 +1,6 @@
1
1
  import type { MemoryStore, ScopeFilter } from './adapters.js';
2
2
  import type { ResolvedConfig } from './config.js';
3
+ import { type Grant } from './grant.js';
3
4
  export interface ConsolidateResult {
4
5
  /** Ids whose tier moved up (warm to hot). */
5
6
  promoted: string[];
@@ -15,6 +16,24 @@ export interface ConsolidateResult {
15
16
  * The record is left as the other writer made it; the next run reconsiders it.
16
17
  */
17
18
  skipped: number;
19
+ /**
20
+ * False when `shouldStop` cut the run short. Every write made so far stands (each is atomic and
21
+ * guarded), and a later run picks up the rest: consolidation recomputes everything from the
22
+ * store, so nothing is lost by stopping between two writes.
23
+ */
24
+ complete: boolean;
25
+ }
26
+ export interface ConsolidateDeps {
27
+ store: MemoryStore;
28
+ config: ResolvedConfig;
29
+ now?: Date;
30
+ /**
31
+ * Checked before each store call that lists or writes: once it returns true, consolidation
32
+ * starts nothing new and resolves with `complete: false`. A time budget (the hook drain's).
33
+ */
34
+ shouldStop?: () => boolean;
35
+ /** Evictions per `coldPutMany` call (default `COLD_EVICT_BATCH`); smaller holds the write lock for less. */
36
+ evictBatch?: number;
18
37
  }
19
38
  /** Evictions per `coldPutMany` call: one store transaction and one directory sync each. */
20
39
  export declare const COLD_EVICT_BATCH = 500;
@@ -24,8 +43,10 @@ export declare const COLD_EVICT_BATCH = 500;
24
43
  * `expiredAt = now`, `endReason = endReason ?? "expired"` (`expireMany`).
25
44
  * 2. Rank current memories by ACT-R activation (ties: newer `updatedAt`
26
45
  * first) and assign tiers via `tierFor`; changes are `setTier`d and new
27
- * cold memories are collected and `coldPutMany`'d in batches of `COLD_EVICT_BATCH`.
28
- * 3. Non-current memories (superseded, expired, corrected) go straight to cold.
46
+ * cold memories are collected and `coldPutMany`'d in batches of `COLD_EVICT_BATCH`
47
+ * (each read whole first, since the listing leaves embeddings out).
48
+ * 3. Non-current memories (superseded, expired, corrected) go straight to cold
49
+ * (evicted ahead of the stale current ones).
29
50
  *
30
51
  * Every write is targeted (expiry writes only the expiry fields, a tier change
31
52
  * only the tier, an eviction only the tier and the cold copy) and guarded by
@@ -37,9 +58,17 @@ export declare const COLD_EVICT_BATCH = 500;
37
58
  * left in its current tier (hot or warm); consolidate never promotes cold to warm/hot, since
38
59
  * hydration is recall's job.
39
60
  * Idempotent for a fixed `now`.
61
+ *
62
+ * Each node the scope's grant covers (the person's Planet, this agent's Star)
63
+ * is consolidated on its own, so the Planet's tiers do not depend on which of
64
+ * the person's agents ran consolidation. Inside a node each project, and the
65
+ * global memories (no project), is ranked on its own (`hotTopN` per project),
66
+ * so a global memory's tier does not depend on which project ran it (CON-48).
40
67
  */
41
- export declare function consolidate(scope: ScopeFilter, deps: {
42
- store: MemoryStore;
43
- config: ResolvedConfig;
44
- now?: Date;
45
- }): Promise<ConsolidateResult>;
68
+ export declare function consolidate(scope: ScopeFilter, deps: ConsolidateDeps): Promise<ConsolidateResult>;
69
+ /**
70
+ * `consolidate` over every node `grant` both reads and writes: a Star agent's grant reads the
71
+ * Planet but does not write it, so the Planet is left alone. With `principalGrant` it covers every
72
+ * project of the person, which is what the hook drain runs.
73
+ */
74
+ export declare function consolidateGrant(grant: Grant, deps: ConsolidateDeps): Promise<ConsolidateResult>;
@@ -1,17 +1,21 @@
1
1
  import { activation, tierFor } from './activation.js';
2
- import { localGrant } from './grant.js';
2
+ import { addressOf, nodeKey } from './address.js';
3
+ import { assertGrant, 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;
6
7
  const RANK = { cold: 0, warm: 1, hot: 2 };
8
+ const LISTING = { validOnly: false, embeddings: false };
7
9
  /**
8
10
  * Background consolidation. In order:
9
11
  * 1. Expire: memories with `validTo <= now` and no `expiredAt` get
10
12
  * `expiredAt = now`, `endReason = endReason ?? "expired"` (`expireMany`).
11
13
  * 2. Rank current memories by ACT-R activation (ties: newer `updatedAt`
12
14
  * first) and assign tiers via `tierFor`; changes are `setTier`d and new
13
- * cold memories are collected and `coldPutMany`'d in batches of `COLD_EVICT_BATCH`.
14
- * 3. Non-current memories (superseded, expired, corrected) go straight to cold.
15
+ * cold memories are collected and `coldPutMany`'d in batches of `COLD_EVICT_BATCH`
16
+ * (each read whole first, since the listing leaves embeddings out).
17
+ * 3. Non-current memories (superseded, expired, corrected) go straight to cold
18
+ * (evicted ahead of the stale current ones).
15
19
  *
16
20
  * Every write is targeted (expiry writes only the expiry fields, a tier change
17
21
  * only the tier, an eviction only the tier and the cold copy) and guarded by
@@ -23,65 +27,135 @@ const RANK = { cold: 0, warm: 1, hot: 2 };
23
27
  * left in its current tier (hot or warm); consolidate never promotes cold to warm/hot, since
24
28
  * hydration is recall's job.
25
29
  * Idempotent for a fixed `now`.
30
+ *
31
+ * Each node the scope's grant covers (the person's Planet, this agent's Star)
32
+ * is consolidated on its own, so the Planet's tiers do not depend on which of
33
+ * the person's agents ran consolidation. Inside a node each project, and the
34
+ * global memories (no project), is ranked on its own (`hotTopN` per project),
35
+ * so a global memory's tier does not depend on which project ran it (CON-48).
26
36
  */
27
37
  export async function consolidate(scope, deps) {
38
+ return consolidateGrant(localGrant(scope), deps);
39
+ }
40
+ /**
41
+ * `consolidate` over every node `grant` both reads and writes: a Star agent's grant reads the
42
+ * Planet but does not write it, so the Planet is left alone. With `principalGrant` it covers every
43
+ * project of the person, which is what the hook drain runs.
44
+ */
45
+ export async function consolidateGrant(grant, deps) {
46
+ assertGrant(grant);
28
47
  const { store, config } = deps;
29
- const grant = localGrant(scope);
30
48
  const now = deps.now ?? new Date();
49
+ const run = {
50
+ store,
51
+ config,
52
+ now,
53
+ stop: deps.shouldStop ?? (() => false),
54
+ evictBatch: Math.max(1, deps.evictBatch ?? COLD_EVICT_BATCH),
55
+ };
31
56
  const result = {
32
57
  promoted: [],
33
58
  demoted: [],
34
59
  evicted: [],
35
60
  expired: [],
36
61
  skipped: 0,
62
+ complete: true,
37
63
  };
64
+ const writable = new Set(grant.writeNodes.map((p) => nodeKey(p.node)));
65
+ for (const node of readableNodes(grant).filter((n) => writable.has(nodeKey(n)))) {
66
+ if (!(await consolidateNode(run, narrowGrant(grant, [node]), result))) {
67
+ result.complete = false;
68
+ break;
69
+ }
70
+ }
71
+ return result;
72
+ }
73
+ /** A memory's project inside its node (`null`: global). */
74
+ const projectOf = (m) => addressOf(m).projectId ?? null;
75
+ /** One node; false when `run.stop` cut it short. */
76
+ async function consolidateNode(run, grant, result) {
77
+ const { store, config, now } = run;
78
+ if (run.stop())
79
+ return false;
80
+ // One listing serves every step (a second one only after expiry changed records). Embeddings are
81
+ // read only for the memories evicted (a fraction of the cost of listing them all).
82
+ let all = await store.list(grant, LISTING);
38
83
  // 1. Expire.
39
- const due = (await store.list(grant, { validOnly: false })).filter((m) => m.validTo !== null && m.expiredAt === null && Date.parse(m.validTo) <= now.getTime());
84
+ const due = all.filter((m) => m.validTo !== null && m.expiredAt === null && Date.parse(m.validTo) <= now.getTime());
40
85
  if (due.length > 0) {
86
+ if (run.stop())
87
+ return false;
41
88
  const skipped = new Set((await store.expireMany(grant, due, now)).skipped);
42
89
  for (const m of due)
43
90
  if (!skipped.has(m.id))
44
91
  result.expired.push(m.id);
45
92
  result.skipped += skipped.size;
93
+ if (run.stop())
94
+ return false;
95
+ all = await store.list(grant, LISTING);
46
96
  }
47
- // 2. Rank and assign current memories.
48
- const current = (await store.list(grant, { validOnly: true })).filter((m) => isCurrent(m, now));
49
- const scored = current.map((m) => ({
50
- m,
51
- a: activation(m.accessLog, m.salience, now, config.activation),
52
- }));
53
- scored.sort((x, y) => y.a - x.a || (x.m.updatedAt < y.m.updatedAt ? 1 : x.m.updatedAt > y.m.updatedAt ? -1 : 0));
97
+ // 2. Rank and assign current memories, each project (and global) on its own.
98
+ const current = all.filter((m) => isCurrent(m, now));
54
99
  const currentIds = new Set(current.map((m) => m.id));
55
- const evictions = [];
56
- for (const [rank, { m, a }] of scored.entries()) {
57
- const target = tierFor(a, rank, m.pinned, config.activation);
58
- if (target === m.tier)
59
- continue;
60
- if (m.tier === 'cold')
61
- continue; // never promote out of cold here
62
- if (target === 'cold') {
63
- evictions.push(m);
64
- }
65
- else if (await store.setTier(grant, m.id, target, m)) {
66
- (RANK[target] > RANK[m.tier] ? result.promoted : result.demoted).push(m.id);
67
- }
68
- else {
69
- result.skipped++;
70
- }
100
+ const byProject = new Map();
101
+ for (const m of current) {
102
+ const key = projectOf(m);
103
+ const list = byProject.get(key);
104
+ if (list === undefined)
105
+ byProject.set(key, [m]);
106
+ else
107
+ list.push(m);
71
108
  }
72
- // 3. Non-current memories go straight to cold (unless pinned or already cold).
73
- for (const m of await store.list(grant, { validOnly: false })) {
74
- if (currentIds.has(m.id) || m.pinned || m.tier === 'cold')
75
- continue;
76
- evictions.push(m);
109
+ // 3 first. Non-current memories (superseded, expired) go straight to cold, unless pinned or
110
+ // already cold; they lead the evictions, so a run cut short has moved them first.
111
+ const evictions = all.filter((m) => !currentIds.has(m.id) && !m.pinned && m.tier !== 'cold');
112
+ for (const members of byProject.values()) {
113
+ const scored = members.map((m) => ({
114
+ m,
115
+ a: activation(m.accessLog, m.salience, now, config.activation),
116
+ }));
117
+ scored.sort((x, y) => y.a - x.a || (x.m.updatedAt < y.m.updatedAt ? 1 : x.m.updatedAt > y.m.updatedAt ? -1 : 0));
118
+ for (const [rank, { m, a }] of scored.entries()) {
119
+ const target = tierFor(a, rank, m.pinned, config.activation);
120
+ if (target === m.tier)
121
+ continue;
122
+ if (m.tier === 'cold')
123
+ continue; // never promote out of cold here
124
+ if (target === 'cold') {
125
+ evictions.push(m);
126
+ continue;
127
+ }
128
+ if (run.stop())
129
+ return false;
130
+ if (await store.setTier(grant, m.id, target, m)) {
131
+ (RANK[target] > RANK[m.tier] ? result.promoted : result.demoted).push(m.id);
132
+ }
133
+ else {
134
+ result.skipped++;
135
+ }
136
+ }
77
137
  }
78
- await evictAll(store, grant, evictions, result);
79
- return result;
138
+ return evictAll(run, grant, evictions, result);
80
139
  }
81
- async function evictAll(store, grant, evictions, result) {
82
- for (let i = 0; i < evictions.length; i += COLD_EVICT_BATCH) {
83
- const batch = evictions.slice(i, i + COLD_EVICT_BATCH);
84
- // Each entry is the snapshot it was chosen from: the store's guard checks it.
140
+ async function evictAll(run, grant, evictions, result) {
141
+ const { store } = run;
142
+ for (let i = 0; i < evictions.length; i += run.evictBatch) {
143
+ if (run.stop())
144
+ return false;
145
+ // The cold copy keeps the embedding the listing left out: read each record whole, and drop
146
+ // any that changed since it was chosen. The store's guard checks the rest at commit.
147
+ const batch = [];
148
+ for (const chosen of evictions.slice(i, i + run.evictBatch)) {
149
+ const whole = await store.get(grant, chosen.id);
150
+ if (whole !== null && whole.updatedAt === chosen.updatedAt && whole.tier === chosen.tier) {
151
+ batch.push(whole);
152
+ }
153
+ else {
154
+ result.skipped++;
155
+ }
156
+ }
157
+ if (batch.length === 0)
158
+ continue;
85
159
  const skipped = new Set((await store.coldPutMany(grant, batch)).skipped);
86
160
  for (const m of batch) {
87
161
  if (skipped.has(m.id))
@@ -91,5 +165,6 @@ async function evictAll(store, grant, evictions, result) {
91
165
  }
92
166
  result.skipped += skipped.size;
93
167
  }
168
+ return true;
94
169
  }
95
170
  //# sourceMappingURL=consolidate.js.map
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
  /**