@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.
- package/dist/agents.d.ts +38 -0
- package/dist/agents.js +67 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +7 -0
- package/dist/consolidate.d.ts +36 -7
- package/dist/consolidate.js +114 -39
- package/dist/grant.d.ts +58 -11
- package/dist/grant.js +121 -23
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/isolation.d.ts +26 -3
- package/dist/isolation.js +222 -61
- package/dist/levels.d.ts +79 -0
- package/dist/levels.js +140 -0
- package/dist/observe.d.ts +9 -0
- package/dist/observe.js +27 -3
- package/dist/recall.d.ts +27 -1
- package/dist/recall.js +163 -17
- package/dist/store-conformance.js +69 -16
- package/package.json +1 -1
package/dist/agents.d.ts
ADDED
|
@@ -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({
|
package/dist/consolidate.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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>;
|
package/dist/consolidate.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { activation, tierFor } from './activation.js';
|
|
2
|
-
import {
|
|
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
|
-
*
|
|
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 =
|
|
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 =
|
|
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
|
|
56
|
-
for (const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
return result;
|
|
138
|
+
return evictAll(run, grant, evictions, result);
|
|
80
139
|
}
|
|
81
|
-
async function evictAll(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
|
71
|
-
*
|
|
72
|
-
* `write`
|
|
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
|
/**
|