@oortmem/core 0.4.0 → 0.5.1
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/config.d.ts +9 -0
- package/dist/config.js +25 -2
- package/dist/consolidate.d.ts +33 -8
- package/dist/consolidate.js +108 -45
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -48,12 +48,21 @@ export declare const OortConfig: z.ZodObject<{
|
|
|
48
48
|
id: z.ZodOptional<z.ZodString>;
|
|
49
49
|
private: z.ZodDefault<z.ZodBoolean>;
|
|
50
50
|
}, z.core.$strict>>;
|
|
51
|
+
serve: z.ZodDefault<z.ZodObject<{
|
|
52
|
+
autostart: z.ZodDefault<z.ZodBoolean>;
|
|
53
|
+
idleMinutes: z.ZodDefault<z.ZodNumber>;
|
|
54
|
+
}, z.core.$strict>>;
|
|
51
55
|
}, z.core.$strict>;
|
|
52
56
|
export type OortConfig = z.infer<typeof OortConfig>;
|
|
53
57
|
export type ConfigSource = 'project' | 'home' | 'default';
|
|
54
58
|
export type ResolvedConfig = OortConfig & {
|
|
55
59
|
home: string;
|
|
56
60
|
sources: Record<string, ConfigSource>;
|
|
61
|
+
/**
|
|
62
|
+
* Keys a project's `oort.config.json` set that only the home config may set (`serve`: a
|
|
63
|
+
* repository must not start processes on this machine). Ignored; absent when there are none.
|
|
64
|
+
*/
|
|
65
|
+
ignoredProjectKeys?: string[];
|
|
57
66
|
};
|
|
58
67
|
/**
|
|
59
68
|
* Oort's home directory, always absolute: `OORT_HOME` resolved against the
|
package/dist/config.js
CHANGED
|
@@ -99,6 +99,18 @@ const project = z
|
|
|
99
99
|
private: z.boolean().default(false),
|
|
100
100
|
})
|
|
101
101
|
.default({ private: false });
|
|
102
|
+
/**
|
|
103
|
+
* `oort serve`, the local daemon the hooks talk to (CON-18). Read from the home config only: a
|
|
104
|
+
* repository's `oort.config.json` cannot start processes on this machine.
|
|
105
|
+
*/
|
|
106
|
+
const serve = z
|
|
107
|
+
.strictObject({
|
|
108
|
+
/** SessionStart starts a detached daemon when none is running. */
|
|
109
|
+
autostart: z.boolean().default(false),
|
|
110
|
+
/** The daemon exits after this many minutes without a hook request. */
|
|
111
|
+
idleMinutes: z.number().int().min(1).max(1440).default(30),
|
|
112
|
+
})
|
|
113
|
+
.default({ autostart: false, idleMinutes: 30 });
|
|
102
114
|
export const OortConfig = z.strictObject({
|
|
103
115
|
embed,
|
|
104
116
|
rerank,
|
|
@@ -107,7 +119,10 @@ export const OortConfig = z.strictObject({
|
|
|
107
119
|
activation,
|
|
108
120
|
recall,
|
|
109
121
|
project,
|
|
122
|
+
serve,
|
|
110
123
|
});
|
|
124
|
+
/** Keys read from the home config only; a project file's value is ignored (and reported). */
|
|
125
|
+
const HOME_ONLY_KEYS = new Set(['serve']);
|
|
111
126
|
const CONFIG_KEYS = Object.keys(OortConfig.shape);
|
|
112
127
|
function findProjectConfig(cwd) {
|
|
113
128
|
let dir = path.resolve(cwd);
|
|
@@ -175,8 +190,11 @@ export function resolveConfig(cwd, env = process.env, opts = {}) {
|
|
|
175
190
|
const homeRaw = fs.existsSync(homeFile) ? readConfigFile(homeFile) : {};
|
|
176
191
|
const merged = {};
|
|
177
192
|
const sources = {};
|
|
193
|
+
const ignored = [];
|
|
178
194
|
for (const key of CONFIG_KEYS) {
|
|
179
|
-
if (key in projectRaw)
|
|
195
|
+
if (key in projectRaw && HOME_ONLY_KEYS.has(key))
|
|
196
|
+
ignored.push(key);
|
|
197
|
+
if (key in projectRaw && !HOME_ONLY_KEYS.has(key)) {
|
|
180
198
|
merged[key] = projectRaw[key];
|
|
181
199
|
sources[key] = 'project';
|
|
182
200
|
}
|
|
@@ -188,6 +206,11 @@ export function resolveConfig(cwd, env = process.env, opts = {}) {
|
|
|
188
206
|
sources[key] = 'default';
|
|
189
207
|
}
|
|
190
208
|
}
|
|
191
|
-
return {
|
|
209
|
+
return {
|
|
210
|
+
...OortConfig.parse(merged),
|
|
211
|
+
home,
|
|
212
|
+
sources,
|
|
213
|
+
...(ignored.length > 0 && { ignoredProjectKeys: ignored }),
|
|
214
|
+
};
|
|
192
215
|
}
|
|
193
216
|
//# sourceMappingURL=config.js.map
|
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
|
|
@@ -40,10 +61,14 @@ export declare const COLD_EVICT_BATCH = 500;
|
|
|
40
61
|
*
|
|
41
62
|
* Each node the scope's grant covers (the person's Planet, this agent's Star)
|
|
42
63
|
* is consolidated on its own, so the Planet's tiers do not depend on which of
|
|
43
|
-
* the person's agents ran consolidation.
|
|
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).
|
|
44
67
|
*/
|
|
45
|
-
export declare function consolidate(scope: ScopeFilter, deps:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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,18 +1,21 @@
|
|
|
1
1
|
import { activation, tierFor } from './activation.js';
|
|
2
|
-
import { nodeKey } from './address.js';
|
|
3
|
-
import { localGrant, narrowGrant, readableNodes } from './grant.js';
|
|
2
|
+
import { addressOf, nodeKey } from './address.js';
|
|
3
|
+
import { assertGrant, localGrant, narrowGrant, readableNodes } from './grant.js';
|
|
4
4
|
import { isCurrent } from './model.js';
|
|
5
5
|
/** Evictions per `coldPutMany` call: one store transaction and one directory sync each. */
|
|
6
6
|
export const COLD_EVICT_BATCH = 500;
|
|
7
7
|
const RANK = { cold: 0, warm: 1, hot: 2 };
|
|
8
|
+
const LISTING = { validOnly: false, embeddings: false };
|
|
8
9
|
/**
|
|
9
10
|
* Background consolidation. In order:
|
|
10
11
|
* 1. Expire: memories with `validTo <= now` and no `expiredAt` get
|
|
11
12
|
* `expiredAt = now`, `endReason = endReason ?? "expired"` (`expireMany`).
|
|
12
13
|
* 2. Rank current memories by ACT-R activation (ties: newer `updatedAt`
|
|
13
14
|
* first) and assign tiers via `tierFor`; changes are `setTier`d and new
|
|
14
|
-
* cold memories are collected and `coldPutMany`'d in batches of `COLD_EVICT_BATCH
|
|
15
|
-
*
|
|
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).
|
|
16
19
|
*
|
|
17
20
|
* Every write is targeted (expiry writes only the expiry fields, a tier change
|
|
18
21
|
* only the tier, an eviction only the tier and the cold copy) and guarded by
|
|
@@ -27,73 +30,132 @@ const RANK = { cold: 0, warm: 1, hot: 2 };
|
|
|
27
30
|
*
|
|
28
31
|
* Each node the scope's grant covers (the person's Planet, this agent's Star)
|
|
29
32
|
* is consolidated on its own, so the Planet's tiers do not depend on which of
|
|
30
|
-
* the person's agents ran consolidation.
|
|
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).
|
|
31
36
|
*/
|
|
32
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);
|
|
33
47
|
const { store, config } = deps;
|
|
34
|
-
const full = localGrant(scope);
|
|
35
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
|
+
};
|
|
36
56
|
const result = {
|
|
37
57
|
promoted: [],
|
|
38
58
|
demoted: [],
|
|
39
59
|
evicted: [],
|
|
40
60
|
expired: [],
|
|
41
61
|
skipped: 0,
|
|
62
|
+
complete: true,
|
|
42
63
|
};
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
}
|
|
47
70
|
}
|
|
48
71
|
return result;
|
|
49
72
|
}
|
|
50
|
-
|
|
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);
|
|
51
83
|
// 1. Expire.
|
|
52
|
-
const due =
|
|
84
|
+
const due = all.filter((m) => m.validTo !== null && m.expiredAt === null && Date.parse(m.validTo) <= now.getTime());
|
|
53
85
|
if (due.length > 0) {
|
|
86
|
+
if (run.stop())
|
|
87
|
+
return false;
|
|
54
88
|
const skipped = new Set((await store.expireMany(grant, due, now)).skipped);
|
|
55
89
|
for (const m of due)
|
|
56
90
|
if (!skipped.has(m.id))
|
|
57
91
|
result.expired.push(m.id);
|
|
58
92
|
result.skipped += skipped.size;
|
|
93
|
+
if (run.stop())
|
|
94
|
+
return false;
|
|
95
|
+
all = await store.list(grant, LISTING);
|
|
59
96
|
}
|
|
60
|
-
// 2. Rank and assign current memories.
|
|
61
|
-
const current =
|
|
62
|
-
const scored = current.map((m) => ({
|
|
63
|
-
m,
|
|
64
|
-
a: activation(m.accessLog, m.salience, now, config.activation),
|
|
65
|
-
}));
|
|
66
|
-
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));
|
|
67
99
|
const currentIds = new Set(current.map((m) => m.id));
|
|
68
|
-
const
|
|
69
|
-
for (const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
evictions.push(m);
|
|
77
|
-
}
|
|
78
|
-
else if (await store.setTier(grant, m.id, target, m)) {
|
|
79
|
-
(RANK[target] > RANK[m.tier] ? result.promoted : result.demoted).push(m.id);
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
result.skipped++;
|
|
83
|
-
}
|
|
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);
|
|
84
108
|
}
|
|
85
|
-
// 3. Non-current memories go straight to cold
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}
|
|
90
137
|
}
|
|
91
|
-
|
|
138
|
+
return evictAll(run, grant, evictions, result);
|
|
92
139
|
}
|
|
93
|
-
async function evictAll(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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;
|
|
97
159
|
const skipped = new Set((await store.coldPutMany(grant, batch)).skipped);
|
|
98
160
|
for (const m of batch) {
|
|
99
161
|
if (skipped.has(m.id))
|
|
@@ -103,5 +165,6 @@ async function evictAll(store, grant, evictions, result) {
|
|
|
103
165
|
}
|
|
104
166
|
result.skipped += skipped.size;
|
|
105
167
|
}
|
|
168
|
+
return true;
|
|
106
169
|
}
|
|
107
170
|
//# sourceMappingURL=consolidate.js.map
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED