@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.
- 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 +4 -0
- package/dist/consolidate.js +15 -3
- 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/isolation.js
CHANGED
|
@@ -34,8 +34,25 @@ export function isoMemory(id, scope, marker, embedding) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
const principal = (s) => ({ orgId: s.orgId, customerId: s.customerId, agentId: s.agentId });
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* The narrowest grant for one scope: its Planet, its one project (or global),
|
|
39
|
+
* memories its own agent wrote. (The pre-CON-62 local grant; today's
|
|
40
|
+
* `localGrant` also covers the Planet's other authors and the agent's Star.)
|
|
41
|
+
*/
|
|
38
42
|
export function soleGrant(s) {
|
|
43
|
+
return nodeGrant({
|
|
44
|
+
principal: principal(s),
|
|
45
|
+
read: [
|
|
46
|
+
{
|
|
47
|
+
node: { level: 'user', orgId: s.orgId, customerId: s.customerId },
|
|
48
|
+
projectIds: [s.projectId ?? null],
|
|
49
|
+
authorAgentId: s.agentId,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** The local grant of `ISO_SCOPES.mine`'s agent for its one project: Planet plus its own Star. */
|
|
55
|
+
export function defaultGrant(s) {
|
|
39
56
|
return localGrant({ ...principal(s), projectIds: [s.projectId ?? null] });
|
|
40
57
|
}
|
|
41
58
|
/** The narrowest grant for one memory at any level: its node, its one project, its author. */
|
|
@@ -92,8 +109,10 @@ const ISO_PRINCIPAL = { orgId: 'local', customerId: 'self', agentId: 'claude-cod
|
|
|
92
109
|
/**
|
|
93
110
|
* Victims at the other levels, each next to the caller's own Planet: a Planet
|
|
94
111
|
* memory another of the caller's agents wrote, the caller's agent's own Star,
|
|
95
|
-
*
|
|
96
|
-
*
|
|
112
|
+
* another agent's Star (in the caller's project and global), an Orbit, a
|
|
113
|
+
* Flyby, a Constellation and a Galaxy root of an organisation, and one of the
|
|
114
|
+
* caller's own owner. The narrowest grant sees none of them; the local grant
|
|
115
|
+
* sees only those in `DEFAULT_COVERS`.
|
|
97
116
|
*/
|
|
98
117
|
export const ISO_LEVELS = {
|
|
99
118
|
planetOtherAuthor: {
|
|
@@ -104,6 +123,14 @@ export const ISO_LEVELS = {
|
|
|
104
123
|
address: { level: 'agent', orgId: 'local', agentId: 'claude-code', projectId: 'pa' },
|
|
105
124
|
author: { agentId: 'claude-code' },
|
|
106
125
|
},
|
|
126
|
+
otherStar: {
|
|
127
|
+
address: { level: 'agent', orgId: 'local', agentId: 'cursor', projectId: 'pa' },
|
|
128
|
+
author: { agentId: 'cursor' },
|
|
129
|
+
},
|
|
130
|
+
otherStarGlobal: {
|
|
131
|
+
address: { level: 'agent', orgId: 'local', agentId: 'cursor' },
|
|
132
|
+
author: { agentId: 'cursor' },
|
|
133
|
+
},
|
|
107
134
|
orbit: {
|
|
108
135
|
address: { level: 'agent-user', orgId: 'local', agentId: 'claude-code', customerId: 'self' },
|
|
109
136
|
author: { agentId: 'claude-code' },
|
|
@@ -132,6 +159,15 @@ export const ISO_LEVELS = {
|
|
|
132
159
|
author: { agentId: 'claude-code' },
|
|
133
160
|
},
|
|
134
161
|
};
|
|
162
|
+
/**
|
|
163
|
+
* The victims the local grant of `ISO_SCOPES.mine` legitimately covers (CON-62): Planet memories
|
|
164
|
+
* the person's other agents wrote, and its agent's own Star. Never another agent's Star.
|
|
165
|
+
*/
|
|
166
|
+
export const DEFAULT_COVERS = Object.freeze([
|
|
167
|
+
'otherAgent',
|
|
168
|
+
'planetOtherAuthor',
|
|
169
|
+
'star',
|
|
170
|
+
]);
|
|
135
171
|
/**
|
|
136
172
|
* Every `MemoryStore` method, called with a grant for `ISO_SCOPES.mine`
|
|
137
173
|
* against memories (warm and cold) in each other scope: nothing is seen,
|
|
@@ -139,6 +175,10 @@ export const ISO_LEVELS = {
|
|
|
139
175
|
* apart. Also the write-side rules (`GrantError` for an out-of-grant scope
|
|
140
176
|
* and for an id that exists elsewhere, atomic batches, address changes),
|
|
141
177
|
* read/write splits, cursor namespaces and rejection of non-grants.
|
|
178
|
+
*
|
|
179
|
+
* Run twice: as the narrowest grant (every victim out of reach), and as the
|
|
180
|
+
* local grant (`localGrant`, Planet plus own Star), for which every victim
|
|
181
|
+
* but `DEFAULT_COVERS` is out of reach, other agents' Stars included.
|
|
142
182
|
*/
|
|
143
183
|
export async function isolationViolations(make) {
|
|
144
184
|
const out = [];
|
|
@@ -176,62 +216,12 @@ export async function isolationViolations(make) {
|
|
|
176
216
|
}
|
|
177
217
|
}
|
|
178
218
|
const snapshot = async () => Promise.all(victims.map(async (v) => `${render(await store.get(v.g, v.m.id))}|${render(await store.coldGet(v.g, v.m.id))}`));
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if ((await store.coldGet(mine, id)) !== null)
|
|
186
|
-
fail('coldGet returned it');
|
|
187
|
-
const lex = await store.lexical(mine, 'shared canary', 50);
|
|
188
|
-
if (lex.some((h) => h.id === id))
|
|
189
|
-
fail('lexical returned it');
|
|
190
|
-
const den = await store.dense(mine, v.m.embedding ?? vec(1), 50);
|
|
191
|
-
if (den.some((h) => h.id === id))
|
|
192
|
-
fail('dense returned it');
|
|
193
|
-
for (const validOnly of [true, false]) {
|
|
194
|
-
if ((await store.list(mine, { validOnly })).some((m) => m.id === id))
|
|
195
|
-
fail('list returned it');
|
|
196
|
-
}
|
|
197
|
-
await store.recordAccess(mine, [id], new Date(NOW));
|
|
198
|
-
if ((await store.setTier(mine, id, 'hot')) !== false)
|
|
199
|
-
fail('setTier reported a write');
|
|
200
|
-
if ((await store.setTier(mine, id, 'hot', v.m)) !== false)
|
|
201
|
-
fail('guarded setTier reported a write');
|
|
202
|
-
const exp = await store.expireMany(mine, [v.m], new Date(NOW));
|
|
203
|
-
if (!exp.skipped.includes(id))
|
|
204
|
-
fail('expireMany did not skip it');
|
|
205
|
-
for (const r of [await store.coldPut(mine, v.m), await store.coldPutMany(mine, [v.m])]) {
|
|
206
|
-
if (!r.skipped.includes(id))
|
|
207
|
-
fail('coldPut did not skip it');
|
|
208
|
-
}
|
|
209
|
-
await store.forget(mine, id);
|
|
210
|
-
const direct = await settle(store.upsert(mine, v.m));
|
|
211
|
-
if (!('err' in direct) || !(direct.err instanceof GrantError)) {
|
|
212
|
-
fail('upsert of its own scope was not refused with GrantError');
|
|
213
|
-
}
|
|
214
|
-
const moved = await settle(store.upsert(mine, movedTo(v.m, ISO_SCOPES.mine)));
|
|
215
|
-
if (!('err' in moved) || !(moved.err instanceof GrantError)) {
|
|
216
|
-
fail('upsert moving its id into my scope was not refused with GrantError');
|
|
217
|
-
}
|
|
218
|
-
else if ('err' in direct &&
|
|
219
|
-
direct.err.message !== moved.err.message) {
|
|
220
|
-
fail('the two refusals have different messages');
|
|
221
|
-
}
|
|
222
|
-
const batch = await settle(store.upsertMany(mine, [
|
|
223
|
-
isoMemory(`batch-${id}`, ISO_SCOPES.mine, 'batch'),
|
|
224
|
-
movedTo(v.m, ISO_SCOPES.mine),
|
|
225
|
-
]));
|
|
226
|
-
if (!('err' in batch))
|
|
227
|
-
fail('a batch reaching it was not refused');
|
|
228
|
-
if ((await store.get(mine, `batch-${id}`)) !== null)
|
|
229
|
-
fail('a refused batch wrote part of itself');
|
|
230
|
-
}
|
|
231
|
-
const after = await snapshot();
|
|
232
|
-
for (const [i, v] of victims.entries()) {
|
|
233
|
-
if (before[i] !== after[i])
|
|
234
|
-
out.push(`${v.name}: changed through a grant that cannot see it`);
|
|
219
|
+
for (const [caller, grant, covered] of [
|
|
220
|
+
['narrowest', mine, []],
|
|
221
|
+
['local', defaultGrant(ISO_SCOPES.mine), DEFAULT_COVERS],
|
|
222
|
+
]) {
|
|
223
|
+
const reach = victims.filter((v) => !covered.some((c) => v.name === `victim-${c}-warm` || v.name === `victim-${c}-cold`));
|
|
224
|
+
out.push(...(await victimViolations(store, caller, grant, reach, snapshot, victims)));
|
|
235
225
|
}
|
|
236
226
|
// An address change of my own record is refused too, and leaves it as it was.
|
|
237
227
|
const own = await store.get(mine, 'mine-1');
|
|
@@ -312,6 +302,68 @@ export async function isolationViolations(make) {
|
|
|
312
302
|
}
|
|
313
303
|
return out;
|
|
314
304
|
}
|
|
305
|
+
/** One caller's pass over the victims it must not reach: nothing seen, nothing changed. */
|
|
306
|
+
async function victimViolations(store, caller, mine, victims, snapshot, all) {
|
|
307
|
+
const out = [];
|
|
308
|
+
const vec = (n) => Int8Array.from([n, 1, 1, 1]);
|
|
309
|
+
const before = await snapshot();
|
|
310
|
+
for (const v of victims) {
|
|
311
|
+
const id = v.m.id;
|
|
312
|
+
const fail = (what) => out.push(`${caller}: ${v.name}: ${what}`);
|
|
313
|
+
if ((await store.get(mine, id)) !== null)
|
|
314
|
+
fail('get returned it');
|
|
315
|
+
if ((await store.coldGet(mine, id)) !== null)
|
|
316
|
+
fail('coldGet returned it');
|
|
317
|
+
const lex = await store.lexical(mine, 'shared canary', 50);
|
|
318
|
+
if (lex.some((h) => h.id === id))
|
|
319
|
+
fail('lexical returned it');
|
|
320
|
+
const den = await store.dense(mine, v.m.embedding ?? vec(1), 50);
|
|
321
|
+
if (den.some((h) => h.id === id))
|
|
322
|
+
fail('dense returned it');
|
|
323
|
+
for (const validOnly of [true, false]) {
|
|
324
|
+
if ((await store.list(mine, { validOnly })).some((m) => m.id === id))
|
|
325
|
+
fail('list returned it');
|
|
326
|
+
}
|
|
327
|
+
await store.recordAccess(mine, [id], new Date(NOW));
|
|
328
|
+
if ((await store.setTier(mine, id, 'hot')) !== false)
|
|
329
|
+
fail('setTier reported a write');
|
|
330
|
+
if ((await store.setTier(mine, id, 'hot', v.m)) !== false)
|
|
331
|
+
fail('guarded setTier reported a write');
|
|
332
|
+
const exp = await store.expireMany(mine, [v.m], new Date(NOW));
|
|
333
|
+
if (!exp.skipped.includes(id))
|
|
334
|
+
fail('expireMany did not skip it');
|
|
335
|
+
for (const r of [await store.coldPut(mine, v.m), await store.coldPutMany(mine, [v.m])]) {
|
|
336
|
+
if (!r.skipped.includes(id))
|
|
337
|
+
fail('coldPut did not skip it');
|
|
338
|
+
}
|
|
339
|
+
await store.forget(mine, id);
|
|
340
|
+
const direct = await settle(store.upsert(mine, v.m));
|
|
341
|
+
if (!('err' in direct) || !(direct.err instanceof GrantError)) {
|
|
342
|
+
fail('upsert of its own scope was not refused with GrantError');
|
|
343
|
+
}
|
|
344
|
+
const moved = await settle(store.upsert(mine, movedTo(v.m, ISO_SCOPES.mine)));
|
|
345
|
+
if (!('err' in moved) || !(moved.err instanceof GrantError)) {
|
|
346
|
+
fail('upsert moving its id into my scope was not refused with GrantError');
|
|
347
|
+
}
|
|
348
|
+
else if ('err' in direct && direct.err.message !== moved.err.message) {
|
|
349
|
+
fail('the two refusals have different messages');
|
|
350
|
+
}
|
|
351
|
+
const batch = await settle(store.upsertMany(mine, [
|
|
352
|
+
isoMemory(`batch-${id}`, ISO_SCOPES.mine, 'batch'),
|
|
353
|
+
movedTo(v.m, ISO_SCOPES.mine),
|
|
354
|
+
]));
|
|
355
|
+
if (!('err' in batch))
|
|
356
|
+
fail('a batch reaching it was not refused');
|
|
357
|
+
if ((await store.get(mine, `batch-${id}`)) !== null)
|
|
358
|
+
fail('a refused batch wrote part of itself');
|
|
359
|
+
}
|
|
360
|
+
const after = await snapshot();
|
|
361
|
+
for (const [i, v] of all.entries()) {
|
|
362
|
+
if (before[i] !== after[i])
|
|
363
|
+
out.push(`${caller}: ${v.name}: changed through a grant that cannot see it`);
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
315
367
|
/** A small seeded PRNG (mulberry32), so a failing seed replays exactly. */
|
|
316
368
|
export function seededRng(seed) {
|
|
317
369
|
let a = seed >>> 0;
|
|
@@ -336,7 +388,10 @@ const WORDS = ['orbit', 'comet', 'nebula', 'pulsar', 'quasar', 'photon', 'kuiper
|
|
|
336
388
|
* keys (callers use UUIDs), so a tenant that reuses another tenant's id is
|
|
337
389
|
* refused, which is a collision, not a leak of content.
|
|
338
390
|
*/
|
|
339
|
-
function opSequence(seed, steps,
|
|
391
|
+
function opSequence(seed, steps, mode) {
|
|
392
|
+
if (mode === 'peers')
|
|
393
|
+
return peerSequence(seed, steps);
|
|
394
|
+
const multiNode = mode === 'multi';
|
|
340
395
|
const rng = seededRng(seed);
|
|
341
396
|
const planetA = ['pa', 'pa2', null].map((projectId) => (id, t, e) => isoMemory(id, projectId === null ? ISO_SCOPES.global : { ...ISO_SCOPES.mine, projectId }, t, e));
|
|
342
397
|
// With multiNode, A also reads and writes a Constellation of another owner and its
|
|
@@ -453,6 +508,107 @@ function opSequence(seed, steps, multiNode) {
|
|
|
453
508
|
}
|
|
454
509
|
return ops;
|
|
455
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Two agents of one person (see `nonInterference`'s `peers`): A is `claude-code`, B is `cursor`,
|
|
513
|
+
* each with its local grant over projects pa, pa2 and global.
|
|
514
|
+
*/
|
|
515
|
+
function peerSequence(seed, steps) {
|
|
516
|
+
const rng = seededRng(seed);
|
|
517
|
+
const person = { orgId: 'local', customerId: 'self' };
|
|
518
|
+
const projects = ['pa', 'pa2', null];
|
|
519
|
+
const aGrant = localGrant({ ...person, agentId: 'claude-code', projectIds: [...projects] });
|
|
520
|
+
const bGrant = localGrant({ ...person, agentId: 'cursor', projectIds: [...projects] });
|
|
521
|
+
const star = (agentId, projectId) => ({
|
|
522
|
+
level: 'agent',
|
|
523
|
+
orgId: 'local',
|
|
524
|
+
agentId,
|
|
525
|
+
...(projectId !== null && { projectId }),
|
|
526
|
+
});
|
|
527
|
+
const ops = [];
|
|
528
|
+
const aIds = [];
|
|
529
|
+
const aStarIds = [];
|
|
530
|
+
const bIds = [];
|
|
531
|
+
const text = () => `${rng.pick(WORDS)} ${rng.pick(WORDS)} ${rng.pick(WORDS)}`;
|
|
532
|
+
const emb = () => Int8Array.from([rng.int(9) - 4, rng.int(9) - 4, rng.int(9) - 4, 1]);
|
|
533
|
+
for (let i = 0; i < steps; i++) {
|
|
534
|
+
const who = rng.int(2) === 0 ? 'A' : 'B';
|
|
535
|
+
const grant = who === 'A' ? aGrant : bGrant;
|
|
536
|
+
const own = who === 'A' ? aIds : bIds;
|
|
537
|
+
// B aims at A's Star ids half the time (A's Planet is B's to share); A only at its own.
|
|
538
|
+
const target = () => who === 'B' && aStarIds.length > 0 && rng.int(2) === 0
|
|
539
|
+
? rng.pick(aStarIds)
|
|
540
|
+
: own.length > 0
|
|
541
|
+
? rng.pick(own)
|
|
542
|
+
: 'none';
|
|
543
|
+
const kind = rng.int(12);
|
|
544
|
+
if (kind <= 2 || own.length === 0) {
|
|
545
|
+
const id = `${who}-${i}`;
|
|
546
|
+
own.push(id);
|
|
547
|
+
const projectId = rng.pick(projects);
|
|
548
|
+
let m;
|
|
549
|
+
if (who === 'B') {
|
|
550
|
+
m = isoAt(id, star('cursor', projectId), { agentId: 'cursor' }, text(), emb());
|
|
551
|
+
}
|
|
552
|
+
else if (rng.int(2) === 0) {
|
|
553
|
+
aStarIds.push(id);
|
|
554
|
+
m = isoAt(id, star('claude-code', projectId), { agentId: 'claude-code' }, text(), emb());
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
const scope = {
|
|
558
|
+
...person,
|
|
559
|
+
agentId: 'claude-code',
|
|
560
|
+
...(projectId !== null && { projectId }),
|
|
561
|
+
};
|
|
562
|
+
m = isoMemory(id, scope, text(), emb());
|
|
563
|
+
}
|
|
564
|
+
const salted = { ...m, salience: rng.int(10) / 10 };
|
|
565
|
+
ops.push({ who, run: (s) => s.upsert(grant, salted) });
|
|
566
|
+
}
|
|
567
|
+
else if (kind === 3) {
|
|
568
|
+
const id = target();
|
|
569
|
+
ops.push({ who, run: async (s) => render(await s.get(grant, id)) });
|
|
570
|
+
}
|
|
571
|
+
else if (kind === 4) {
|
|
572
|
+
const q = text();
|
|
573
|
+
ops.push({ who, run: async (s) => (await s.lexical(grant, q, 100)).map((h) => h.id).sort() });
|
|
574
|
+
}
|
|
575
|
+
else if (kind === 5) {
|
|
576
|
+
const v = emb();
|
|
577
|
+
ops.push({ who, run: async (s) => s.dense(grant, v, 5) });
|
|
578
|
+
}
|
|
579
|
+
else if (kind === 6) {
|
|
580
|
+
ops.push({ who, run: async (s) => (await s.list(grant, { validOnly: false })).map(render) });
|
|
581
|
+
}
|
|
582
|
+
else if (kind === 7) {
|
|
583
|
+
const id = target();
|
|
584
|
+
ops.push({
|
|
585
|
+
who,
|
|
586
|
+
run: async (s) => {
|
|
587
|
+
const m = await s.get(grant, id);
|
|
588
|
+
return m === null ? 'none' : s.coldPut(grant, m);
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
else if (kind === 8) {
|
|
593
|
+
const id = target();
|
|
594
|
+
ops.push({ who, run: async (s) => render(await s.coldGet(grant, id)) });
|
|
595
|
+
}
|
|
596
|
+
else if (kind === 9) {
|
|
597
|
+
const id = target();
|
|
598
|
+
const tier = rng.pick(['hot', 'warm']);
|
|
599
|
+
ops.push({ who, run: (s) => s.setTier(grant, id, tier) });
|
|
600
|
+
}
|
|
601
|
+
else if (kind === 10) {
|
|
602
|
+
const ids = [target(), target()];
|
|
603
|
+
ops.push({ who, run: (s) => s.recordAccess(grant, ids, new Date(NOW)) });
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
const id = target();
|
|
607
|
+
ops.push({ who, run: (s) => s.forget(grant, id) });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return ops;
|
|
611
|
+
}
|
|
456
612
|
/**
|
|
457
613
|
* Non-interference: tenant A's observable results (return values, errors,
|
|
458
614
|
* order and scores) are the same whether or not tenant B ran its operations
|
|
@@ -467,9 +623,14 @@ function opSequence(seed, steps, multiNode) {
|
|
|
467
623
|
* `multiNode`: A's grant spans its Planet, its agent's Star and a
|
|
468
624
|
* Constellation of another owner; B's spans its own Planet and a sibling
|
|
469
625
|
* Constellation. Same property.
|
|
626
|
+
*
|
|
627
|
+
* `peers` (CON-62): A and B are two agents of one person, each with the local
|
|
628
|
+
* grant (the shared Planet plus its own Star). A writes to the Planet and its
|
|
629
|
+
* Star; B writes only to its own Star and aims every method at A's Star ids.
|
|
630
|
+
* Nothing B does through its Star reaches A.
|
|
470
631
|
*/
|
|
471
632
|
export async function nonInterference(make, seed, steps, opts = {}) {
|
|
472
|
-
const ops = opSequence(seed, steps, opts.multiNode === true);
|
|
633
|
+
const ops = opSequence(seed, steps, opts.peers === true ? 'peers' : opts.multiNode === true ? 'multi' : 'single');
|
|
473
634
|
const observe = async (withB) => {
|
|
474
635
|
const s = await make();
|
|
475
636
|
const seen = [];
|
package/dist/levels.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type Level, type NodeRef } from './address.js';
|
|
2
|
+
import { type Grant } from './grant.js';
|
|
3
|
+
import type { Memory } from './model.js';
|
|
4
|
+
/**
|
|
5
|
+
* The levels a recall can name in `include` (spec 7.1): `self` is the
|
|
6
|
+
* caller's home node, `planet` the person's Planet, `star` the calling agent's
|
|
7
|
+
* own Star, `constellation` and `galaxy` the hive nodes. Always intersected
|
|
8
|
+
* with the grant.
|
|
9
|
+
*/
|
|
10
|
+
export type IncludeLevel = 'self' | 'planet' | 'star' | 'constellation' | 'galaxy';
|
|
11
|
+
export declare const INCLUDE_LEVELS: readonly IncludeLevel[];
|
|
12
|
+
export declare const isIncludeLevel: (s: string) => s is IncludeLevel;
|
|
13
|
+
/** At most this many hive nodes are read by default (spec 7.1: one recall, a bounded fan-out). */
|
|
14
|
+
export declare const DEFAULT_HIVE_NODES = 4;
|
|
15
|
+
/**
|
|
16
|
+
* A recall asked for a level its grant does not cover. Typed, so callers tell
|
|
17
|
+
* "you may not read that" apart from "nothing matched" (spec 7.1).
|
|
18
|
+
*/
|
|
19
|
+
export declare class ScopeDeniedError extends Error {
|
|
20
|
+
readonly code: 'scope_denied';
|
|
21
|
+
readonly levels: IncludeLevel[];
|
|
22
|
+
constructor(levels: IncludeLevel[]);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The nodes one recall reads: with `include`, every readable node a named
|
|
26
|
+
* level covers (a level that covers none rejects with `ScopeDeniedError`);
|
|
27
|
+
* without it, every readable node, hive nodes capped at `DEFAULT_HIVE_NODES`
|
|
28
|
+
* in grant order. Never a node outside `g`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function nodesToRead(g: Grant, include?: readonly IncludeLevel[]): NodeRef[];
|
|
31
|
+
/**
|
|
32
|
+
* How specific a node is, for the specificity prior and the conflict rule:
|
|
33
|
+
* an agent's own memory (Star, Orbit, Flyby) 3, the person's Planet 2, a
|
|
34
|
+
* Constellation 1, a Galaxy 0. Narrower is higher.
|
|
35
|
+
*/
|
|
36
|
+
export declare function specificity(node: Pick<NodeRef, 'level'> & {
|
|
37
|
+
hiveId?: string;
|
|
38
|
+
}): number;
|
|
39
|
+
/**
|
|
40
|
+
* The specificity prior's factor for a node: `1 + ε × specificity / 3`, so
|
|
41
|
+
* with `ε` the narrowest level gains `ε` and a Galaxy nothing. `ε = 0` (the
|
|
42
|
+
* default) leaves every score as it was.
|
|
43
|
+
*/
|
|
44
|
+
export declare function specificityFactor(node: NodeRef, epsilon: number): number;
|
|
45
|
+
/** Brackets in memory text, escaped so the text cannot pass for a code-built label. */
|
|
46
|
+
export declare function escapeLabelText(text: string): string;
|
|
47
|
+
/** Where a recalled memory came from, built by code from its address and author. */
|
|
48
|
+
export interface MemoryOrigin {
|
|
49
|
+
level: Level;
|
|
50
|
+
/** The space name: Planet, Star, Orbit, Flyby, Constellation or Galaxy. */
|
|
51
|
+
space: string;
|
|
52
|
+
/** The node, as a label shows it: `your Planet`, `Star cursor`, `Constellation checkout`. */
|
|
53
|
+
node: string;
|
|
54
|
+
/** For people: `from your Planet`, `from this agent's Star`, `from Star cursor`. */
|
|
55
|
+
from: string;
|
|
56
|
+
/** The memory lives in the caller's home node. */
|
|
57
|
+
home: boolean;
|
|
58
|
+
/** The agent that wrote it. */
|
|
59
|
+
author: string;
|
|
60
|
+
/**
|
|
61
|
+
* A narrower memory in this result contradicts this one (spec 7.2): it ranks
|
|
62
|
+
* first, and this one stays, labelled.
|
|
63
|
+
*/
|
|
64
|
+
overriddenBy?: {
|
|
65
|
+
id: string;
|
|
66
|
+
node: string;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** The node part of a label for `node`, as the caller of grant `g` sees it. */
|
|
70
|
+
export declare function nodeLabel(node: NodeRef, g: Grant): string;
|
|
71
|
+
/** The origin of `m` for the caller of grant `g`. */
|
|
72
|
+
export declare function originOf(m: Pick<Memory, 'scope' | 'address' | 'author'>, g: Grant): MemoryOrigin;
|
|
73
|
+
/** The decision `observe` records on a memory written instead of editing one in another node. */
|
|
74
|
+
export declare const CONTRADICTS: {
|
|
75
|
+
readonly stage: 'level';
|
|
76
|
+
readonly question: 'contradicts';
|
|
77
|
+
};
|
|
78
|
+
/** The ids of memories in other nodes that `m` contradicts (recorded by `observe`). */
|
|
79
|
+
export declare function contradicted(m: Pick<Memory, 'source'>): string[];
|
package/dist/levels.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { addressOf, authorOf, GALAXY_ROOT, nodeKey, nodeOf, spaceName, } from './address.js';
|
|
2
|
+
import { readableNodes } from './grant.js';
|
|
3
|
+
export const INCLUDE_LEVELS = Object.freeze([
|
|
4
|
+
'self',
|
|
5
|
+
'planet',
|
|
6
|
+
'star',
|
|
7
|
+
'constellation',
|
|
8
|
+
'galaxy',
|
|
9
|
+
]);
|
|
10
|
+
export const isIncludeLevel = (s) => INCLUDE_LEVELS.includes(s);
|
|
11
|
+
/** At most this many hive nodes are read by default (spec 7.1: one recall, a bounded fan-out). */
|
|
12
|
+
export const DEFAULT_HIVE_NODES = 4;
|
|
13
|
+
/**
|
|
14
|
+
* A recall asked for a level its grant does not cover. Typed, so callers tell
|
|
15
|
+
* "you may not read that" apart from "nothing matched" (spec 7.1).
|
|
16
|
+
*/
|
|
17
|
+
export class ScopeDeniedError extends Error {
|
|
18
|
+
code = 'scope_denied';
|
|
19
|
+
levels;
|
|
20
|
+
constructor(levels) {
|
|
21
|
+
super(`scope_denied: this grant cannot read ${levels.join(', ')}`);
|
|
22
|
+
this.name = 'ScopeDeniedError';
|
|
23
|
+
this.levels = levels;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** True when `node` is what `level` names for a caller whose grant is `g`. */
|
|
27
|
+
function names(level, node, g) {
|
|
28
|
+
switch (level) {
|
|
29
|
+
case 'self':
|
|
30
|
+
return nodeKey(node) === nodeKey(g.home);
|
|
31
|
+
case 'planet':
|
|
32
|
+
return node.level === 'user';
|
|
33
|
+
case 'star':
|
|
34
|
+
return (node.level === 'agent' &&
|
|
35
|
+
node.orgId === g.principal.orgId &&
|
|
36
|
+
node.agentId === g.principal.agentId);
|
|
37
|
+
case 'constellation':
|
|
38
|
+
return node.level === 'hive' && node.hiveId !== GALAXY_ROOT;
|
|
39
|
+
case 'galaxy':
|
|
40
|
+
return node.level === 'hive' && node.hiveId === GALAXY_ROOT;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The nodes one recall reads: with `include`, every readable node a named
|
|
45
|
+
* level covers (a level that covers none rejects with `ScopeDeniedError`);
|
|
46
|
+
* without it, every readable node, hive nodes capped at `DEFAULT_HIVE_NODES`
|
|
47
|
+
* in grant order. Never a node outside `g`.
|
|
48
|
+
*/
|
|
49
|
+
export function nodesToRead(g, include) {
|
|
50
|
+
const readable = readableNodes(g);
|
|
51
|
+
if (include === undefined || include.length === 0) {
|
|
52
|
+
let hives = 0;
|
|
53
|
+
return readable.filter((n) => n.level !== 'hive' || ++hives <= DEFAULT_HIVE_NODES);
|
|
54
|
+
}
|
|
55
|
+
for (const l of include) {
|
|
56
|
+
if (!isIncludeLevel(l))
|
|
57
|
+
throw new RangeError(`unknown level ${JSON.stringify(l)}`);
|
|
58
|
+
}
|
|
59
|
+
const denied = [...new Set(include)].filter((l) => !readable.some((n) => names(l, n, g)));
|
|
60
|
+
if (denied.length > 0)
|
|
61
|
+
throw new ScopeDeniedError(denied);
|
|
62
|
+
return readable.filter((n) => include.some((l) => names(l, n, g)));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* How specific a node is, for the specificity prior and the conflict rule:
|
|
66
|
+
* an agent's own memory (Star, Orbit, Flyby) 3, the person's Planet 2, a
|
|
67
|
+
* Constellation 1, a Galaxy 0. Narrower is higher.
|
|
68
|
+
*/
|
|
69
|
+
export function specificity(node) {
|
|
70
|
+
switch (node.level) {
|
|
71
|
+
case 'agent':
|
|
72
|
+
case 'agent-user':
|
|
73
|
+
case 'run':
|
|
74
|
+
return 3;
|
|
75
|
+
case 'user':
|
|
76
|
+
return 2;
|
|
77
|
+
case 'hive':
|
|
78
|
+
return node.hiveId === GALAXY_ROOT ? 0 : 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The specificity prior's factor for a node: `1 + ε × specificity / 3`, so
|
|
83
|
+
* with `ε` the narrowest level gains `ε` and a Galaxy nothing. `ε = 0` (the
|
|
84
|
+
* default) leaves every score as it was.
|
|
85
|
+
*/
|
|
86
|
+
export function specificityFactor(node, epsilon) {
|
|
87
|
+
return 1 + (epsilon * specificity(node)) / 3;
|
|
88
|
+
}
|
|
89
|
+
/** Brackets in memory text, escaped so the text cannot pass for a code-built label. */
|
|
90
|
+
export function escapeLabelText(text) {
|
|
91
|
+
return text.replace(/[[\]]/g, (c) => `\\${c}`);
|
|
92
|
+
}
|
|
93
|
+
/** An id shown inside a label: control characters dropped, brackets escaped, clipped. */
|
|
94
|
+
function labelId(id) {
|
|
95
|
+
// oxlint-disable-next-line no-control-regex
|
|
96
|
+
const clean = escapeLabelText(id.replace(/[\u0000-\u001f\u007f]/g, ''));
|
|
97
|
+
return clean.length > 40 ? `${clean.slice(0, 39)}…` : clean;
|
|
98
|
+
}
|
|
99
|
+
/** The node part of a label for `node`, as the caller of grant `g` sees it. */
|
|
100
|
+
export function nodeLabel(node, g) {
|
|
101
|
+
switch (node.level) {
|
|
102
|
+
case 'user':
|
|
103
|
+
return node.orgId === g.principal.orgId && node.customerId === g.principal.customerId
|
|
104
|
+
? 'your Planet'
|
|
105
|
+
: 'a Planet';
|
|
106
|
+
case 'agent':
|
|
107
|
+
return `Star ${labelId(node.agentId)}`;
|
|
108
|
+
case 'agent-user':
|
|
109
|
+
return `Orbit ${labelId(node.agentId)}`;
|
|
110
|
+
case 'run':
|
|
111
|
+
return `Flyby ${labelId(node.runId)}`;
|
|
112
|
+
case 'hive':
|
|
113
|
+
return node.hiveId === GALAXY_ROOT ? 'Galaxy' : `Constellation ${labelId(node.hiveId)}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** The origin of `m` for the caller of grant `g`. */
|
|
117
|
+
export function originOf(m, g) {
|
|
118
|
+
const node = nodeOf(addressOf(m));
|
|
119
|
+
const label = nodeLabel(node, g);
|
|
120
|
+
const ownStar = node.level === 'agent' &&
|
|
121
|
+
node.orgId === g.principal.orgId &&
|
|
122
|
+
node.agentId === g.principal.agentId;
|
|
123
|
+
return {
|
|
124
|
+
level: node.level,
|
|
125
|
+
space: spaceName(node),
|
|
126
|
+
node: label,
|
|
127
|
+
from: ownStar ? "from this agent's Star" : `from ${label}`,
|
|
128
|
+
home: nodeKey(node) === nodeKey(g.home),
|
|
129
|
+
author: authorOf(m).agentId,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** The decision `observe` records on a memory written instead of editing one in another node. */
|
|
133
|
+
export const CONTRADICTS = { stage: 'level', question: 'contradicts' };
|
|
134
|
+
/** The ids of memories in other nodes that `m` contradicts (recorded by `observe`). */
|
|
135
|
+
export function contradicted(m) {
|
|
136
|
+
return m.source.decisions
|
|
137
|
+
.filter((d) => d.stage === CONTRADICTS.stage && d.question === CONTRADICTS.question)
|
|
138
|
+
.map((d) => d.answer);
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=levels.js.map
|
package/dist/observe.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DecisionController, Embedder, MemoryStore, MemoryWriter, Observation } from './adapters.js';
|
|
2
2
|
import type { ResolvedConfig } from './config.js';
|
|
3
|
+
import { type Home } from './grant.js';
|
|
3
4
|
import type { Decision, Scope } from './model.js';
|
|
4
5
|
export type MemoryOutcomeKind = 'skipped' | 'reinforced' | 'added' | 'updated' | 'superseded' | 'corrected';
|
|
5
6
|
/** What happened to one memory the turn produced. */
|
|
@@ -29,6 +30,14 @@ export interface ObserveDeps {
|
|
|
29
30
|
now?: Date;
|
|
30
31
|
/** Recorded on `source.harness`; defaults to "unknown". */
|
|
31
32
|
harness?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Where new memories land (spec 5.1): the person's Planet (default) or, for an agent configured
|
|
35
|
+
* as a Star, `scope.agentId`'s own Star. Candidates come from both. A memory is only ever
|
|
36
|
+
* superseded, corrected or refined in the home node, and an agent whose home is its Star never
|
|
37
|
+
* writes the Planet at all: a Planet memory it re-observes gets an access record (activation),
|
|
38
|
+
* never a confirmation or an edit.
|
|
39
|
+
*/
|
|
40
|
+
home?: Home;
|
|
32
41
|
}
|
|
33
42
|
/**
|
|
34
43
|
* Gate-stage reinforcement match floor. When the gate says "do not store", a
|
package/dist/observe.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { addressed, addressOf, nodeKey, nodeOf } from './address.js';
|
|
3
|
+
import { grantWrites, homeAddress, localGrant } from './grant.js';
|
|
4
|
+
import { CONTRADICTS } from './levels.js';
|
|
3
5
|
import { capObservation } from './cap.js';
|
|
4
6
|
import { scrubObservation } from './scrub.js';
|
|
5
7
|
import { checkWriterDate, factInterval, utcOffset } from './temporal.js';
|
|
@@ -125,11 +127,30 @@ async function apply(a) {
|
|
|
125
127
|
if (target === undefined)
|
|
126
128
|
return { outcome: 'skipped' };
|
|
127
129
|
await store.recordAccess(grant, [target.id], now);
|
|
128
|
-
|
|
130
|
+
// Confirming writes the memory: only where this agent may write (a Star agent reinforces a
|
|
131
|
+
// Planet memory's activation but never edits it).
|
|
132
|
+
if (grantWrites(grant, target))
|
|
133
|
+
await confirm(store, grant, target.id, now.toISOString());
|
|
129
134
|
return { outcome: 'reinforced', memoryId: target.id };
|
|
130
135
|
}
|
|
131
136
|
let action = best.action === 'UPDATE' ? 'REFINE' : best.action;
|
|
132
137
|
let prior = action === 'ADD' ? undefined : candidates.find((c) => c.id === best.candidateId);
|
|
138
|
+
if (prior !== undefined && nodeKey(nodeOf(addressOf(prior))) !== nodeKey(grant.home)) {
|
|
139
|
+
// The prior lives in another node (the Planet, for an agent writing to its Star, or the
|
|
140
|
+
// reverse). A write never edits a memory outside its home node: the observation becomes a
|
|
141
|
+
// memory of its own there. A contradiction is recorded, so recall can rank the narrower of the
|
|
142
|
+
// two first and label the other (spec 7.2). Reinforcing it (above) stays allowed.
|
|
143
|
+
decisions.push({
|
|
144
|
+
stage: 'level',
|
|
145
|
+
question: 'other-node',
|
|
146
|
+
answer: 'add-instead',
|
|
147
|
+
probability: 1,
|
|
148
|
+
});
|
|
149
|
+
if (action === 'SUPERSEDE' || action === 'CORRECT') {
|
|
150
|
+
decisions.push({ ...CONTRADICTS, answer: prior.id, probability: best.probability });
|
|
151
|
+
}
|
|
152
|
+
prior = undefined;
|
|
153
|
+
}
|
|
133
154
|
if (prior !== undefined && (prior.scope.projectId ?? null) !== (scope.projectId ?? null)) {
|
|
134
155
|
// Candidates include global memories. A project's observation must not end or
|
|
135
156
|
// rewrite a fact another scope (every project, for a global one) relies on, so it
|
|
@@ -235,6 +256,9 @@ async function apply(a) {
|
|
|
235
256
|
const base = {
|
|
236
257
|
id,
|
|
237
258
|
scope,
|
|
259
|
+
// A memory at another level than the Planet carries its address and author.
|
|
260
|
+
...(grant.home.level !== 'user' &&
|
|
261
|
+
addressed(homeAddress(grant, scope.projectId), { agentId: scope.agentId })),
|
|
238
262
|
type: w.type,
|
|
239
263
|
text: w.text,
|
|
240
264
|
stub: w.stub,
|
|
@@ -418,7 +442,7 @@ export async function observe(raw, scope, deps) {
|
|
|
418
442
|
const obs = capObservation(scrubbed.obs);
|
|
419
443
|
const { store, embed, decide, write } = deps;
|
|
420
444
|
const now = deps.now ?? new Date();
|
|
421
|
-
const grant = localGrant(filterFor(scope));
|
|
445
|
+
const grant = localGrant(filterFor(scope), deps.home === undefined ? {} : { home: deps.home });
|
|
422
446
|
const g = await decide.gate(obs);
|
|
423
447
|
const text = `${obs.userText}\n${obs.assistantText}`;
|
|
424
448
|
if (g.shouldStore < GATE_THRESHOLD) {
|