@oortmem/core 0.2.1 → 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.
@@ -11,7 +11,8 @@
11
11
  * more); a store with a fixed dimension must be built by `make()` with it.
12
12
  */
13
13
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
14
- import { localGrant, principalGrant } from './grant.js';
14
+ import { addressed, addressOf, authorOf, canonicalMemory, GALAXY_ROOT, nodeOf, } from './address.js';
15
+ import { GrantError, localGrant, nodeGrant, principalGrant } from './grant.js';
15
16
  import { localScope } from './scope.js';
16
17
  import { isolationViolations, nonInterference } from './isolation.js';
17
18
  export const CONFORMANCE_DIMENSION = 4;
@@ -387,12 +388,14 @@ export function storeConformance(name, make) {
387
388
  const others = {
388
389
  'other-org': { ...localScope(), orgId: 'another-org' },
389
390
  'other-customer': { ...localScope(), customerId: 'someone-else' },
390
- 'other-agent': { ...localScope(), agentId: 'another-agent' },
391
391
  };
392
392
  await store.upsert(TG, makeMemory({ id: 'mine', text, embedding }));
393
393
  for (const [id, scope] of Object.entries(others)) {
394
394
  await store.upsert(grantFor(scope), makeMemory({ id, text, embedding, scope }));
395
395
  }
396
+ // Another agent's Star is private to it (CON-62).
397
+ const star = { level: 'agent', orgId: 'local', agentId: 'another-agent' };
398
+ await store.upsert(nodesGrant(star), memoryAt('other-agent', star, { agentId: 'another-agent' }, { text, embedding }));
396
399
  expect(ids(await store.lexical(localGrant(LOCAL), 'auth-service-8080', 10))).toEqual([
397
400
  'mine',
398
401
  ]);
@@ -578,6 +581,367 @@ export function storeConformance(name, make) {
578
581
  expect(await nonInterference(make, seed, 60)).toBeNull();
579
582
  }
580
583
  }, 120_000);
584
+ it(`isolation.non-interference-multi-node-${seeds}-seeds`, async () => {
585
+ for (let seed = 1; seed <= seeds; seed++) {
586
+ expect(await nonInterference(make, seed, 60, { multiNode: true })).toBeNull();
587
+ }
588
+ }, 120_000);
589
+ it(`isolation.non-interference-peer-agents-${seeds}-seeds`, async () => {
590
+ // CON-62: another agent of the same person, working in its own Star, never reaches mine.
591
+ for (let seed = 1; seed <= seeds; seed++) {
592
+ expect(await nonInterference(make, seed, 60, { peers: true })).toBeNull();
593
+ }
594
+ }, 120_000);
595
+ });
596
+ levelConformance(name, make);
597
+ }
598
+ /** The owner every level case belongs to or reads as: the local principal. */
599
+ const OWNER = { orgId: 'local', customerId: 'self', agentId: 'claude-code' };
600
+ /**
601
+ * The grant every local caller had before CON-62: the owner's Planet, restricted to memories its
602
+ * own agent wrote. Kept to prove data written before levels reads back unchanged.
603
+ */
604
+ const PRE_LEVEL = nodeGrant({
605
+ principal: OWNER,
606
+ read: [
607
+ {
608
+ node: { level: 'user', orgId: 'local', customerId: 'self' },
609
+ projectIds: 'all',
610
+ authorAgentId: 'claude-code',
611
+ },
612
+ ],
613
+ });
614
+ const PROVENANCE = {
615
+ derivedFrom: [{ vault: 'acct:1', memoryId: 'src-1' }],
616
+ contributors: [
617
+ {
618
+ memberRef: 'mem-1',
619
+ agentId: 'claude-code',
620
+ at: '2026-11-01T00:00:00.000Z',
621
+ action: 'asserted',
622
+ },
623
+ ],
624
+ support: { members: 1, agents: 1 },
625
+ gate: { policyVersion: 'p1', mode: 'confirmed' },
626
+ };
627
+ /** One memory per level (and a Planet memory by another agent), each with a distinct embedding. */
628
+ export const LEVEL_CASES = [
629
+ {
630
+ name: 'planet-other-author',
631
+ address: { level: 'user', orgId: 'local', customerId: 'self', projectId: 'p1' },
632
+ author: { agentId: 'cursor' },
633
+ },
634
+ {
635
+ name: 'star',
636
+ address: { level: 'agent', orgId: 'local', agentId: 'cursor', projectId: 'p1' },
637
+ author: { agentId: 'cursor' },
638
+ },
639
+ {
640
+ name: 'orbit',
641
+ address: {
642
+ level: 'agent-user',
643
+ orgId: 'org:acme',
644
+ agentId: 'support-bot',
645
+ customerId: 'end-user-1',
646
+ },
647
+ author: { agentId: 'support-bot' },
648
+ },
649
+ {
650
+ name: 'flyby',
651
+ address: {
652
+ level: 'run',
653
+ orgId: 'local',
654
+ agentId: 'claude-code',
655
+ runId: 'run-1',
656
+ customerId: 'self',
657
+ },
658
+ author: { agentId: 'claude-code' },
659
+ },
660
+ {
661
+ name: 'flyby-no-customer',
662
+ address: { level: 'run', orgId: 'local', agentId: 'claude-code', runId: 'run-2' },
663
+ author: { agentId: 'claude-code' },
664
+ },
665
+ {
666
+ name: 'constellation',
667
+ address: { level: 'hive', orgId: 'org:acme', hiveId: 'checkout', projectId: 'p1' },
668
+ author: { agentId: 'claude-code', accountRef: 'acct:1' },
669
+ provenance: PROVENANCE,
670
+ },
671
+ {
672
+ name: 'galaxy',
673
+ address: { level: 'hive', orgId: 'org:acme', hiveId: GALAXY_ROOT },
674
+ author: { agentId: 'claude-code' },
675
+ },
676
+ ];
677
+ /** A memory at `address` by `author` (scope filled to match). */
678
+ export function memoryAt(id, address, author, overrides = {}) {
679
+ return makeMemory({ id, ...addressed(address, author), ...overrides });
680
+ }
681
+ /** Read and write every project of `nodes` (no author restriction). */
682
+ export function nodesGrant(...nodes) {
683
+ return nodeGrant({
684
+ principal: OWNER,
685
+ read: nodes.map((node) => ({ node, projectIds: 'all' })),
686
+ });
687
+ }
688
+ function render(m) {
689
+ return m === null
690
+ ? 'null'
691
+ : JSON.stringify({ ...m, embedding: m.embedding === undefined ? null : [...m.embedding] });
692
+ }
693
+ /**
694
+ * Levels (CON-61): every address round-trips, illegal ones are refused, grants
695
+ * over node sets see exactly their nodes, and memories written before levels
696
+ * (and every pre-level grant) behave exactly as they did.
697
+ */
698
+ function levelConformance(name, make) {
699
+ describe(`store levels: ${name}`, () => {
700
+ let store;
701
+ beforeEach(async () => {
702
+ store = await make();
703
+ });
704
+ afterEach(async () => {
705
+ await store.close();
706
+ });
707
+ const cases = () => LEVEL_CASES.map((c, i) => {
708
+ const m = memoryAt(`lv-${c.name}`, c.address, c.author, {
709
+ text: `levelprobe ${c.name} text`,
710
+ stub: `levelprobe ${c.name}`,
711
+ embedding: vec(i + 1, 1, -1, 2),
712
+ ...(c.provenance !== undefined && { provenance: c.provenance }),
713
+ });
714
+ return { ...c, m, g: nodesGrant(nodeOf(c.address)) };
715
+ });
716
+ it('levels.planet-memories-keep-their-shape', async () => {
717
+ const m = makeMemory({ id: 'legacy', scope: localScope('p1'), embedding: vec(1, 2, 3, 4) });
718
+ await store.upsert(TG, m);
719
+ const got = await store.get(TG, 'legacy');
720
+ expect(got).toEqual(m);
721
+ expect(got !== null && ('address' in got || 'author' in got || 'provenance' in got)).toBe(false);
722
+ expect(addressOf(got)).toEqual({
723
+ level: 'user',
724
+ orgId: 'local',
725
+ customerId: 'self',
726
+ projectId: 'p1',
727
+ });
728
+ expect(authorOf(got)).toEqual({ agentId: 'claude-code' });
729
+ });
730
+ it('levels.every-level-roundtrips', async () => {
731
+ for (const c of cases()) {
732
+ await store.upsert(c.g, c.m);
733
+ const got = await store.get(c.g, c.m.id);
734
+ expect(got, c.name).toEqual(canonicalMemory(c.m));
735
+ expect(addressOf(got), c.name).toEqual(c.address);
736
+ expect(authorOf(got), c.name).toEqual(c.author);
737
+ expect(got?.provenance ?? null, c.name).toEqual(c.provenance ?? null);
738
+ expect(ids(await store.list(c.g)), c.name).toEqual([c.m.id]);
739
+ expect(ids(await store.lexical(c.g, 'levelprobe', 10)), c.name).toEqual([c.m.id]);
740
+ expect(ids(await store.dense(c.g, c.m.embedding, 10)), c.name).toEqual([
741
+ c.m.id,
742
+ ]);
743
+ const snapshot = (await store.get(c.g, c.m.id));
744
+ expect((await store.coldPut(c.g, snapshot)).skipped, c.name).toEqual([]);
745
+ expect(await store.coldGet(c.g, c.m.id), c.name).toEqual(canonicalMemory(snapshot));
746
+ expect((await store.get(c.g, c.m.id))?.tier, c.name).toBe('cold');
747
+ }
748
+ });
749
+ it('levels.pre-level-grants-see-no-other-level', async () => {
750
+ for (const c of cases())
751
+ await store.upsert(c.g, c.m);
752
+ for (const c of cases())
753
+ expect(await store.get(PRE_LEVEL, c.m.id), c.name).toBeNull();
754
+ expect(await store.list(PRE_LEVEL, { validOnly: false })).toEqual([]);
755
+ expect(await store.lexical(PRE_LEVEL, 'levelprobe', 50)).toEqual([]);
756
+ expect(await store.dense(PRE_LEVEL, vec(1, 1, -1, 2), 50)).toEqual([]);
757
+ });
758
+ it('levels.local-grants-see-the-planet-and-their-own-star', async () => {
759
+ // CON-62: a person's agents share the Planet; each agent's Star is its own.
760
+ for (const c of cases())
761
+ await store.upsert(c.g, c.m);
762
+ const ccStar = { level: 'agent', orgId: 'local', agentId: 'claude-code' };
763
+ await store.upsert(nodesGrant(ccStar), memoryAt('cc-star', ccStar, { agentId: 'claude-code' }, { text: 'levelprobe cc star' }));
764
+ const asClaude = [TG, localGrant({ ...OWNER, projectIds: ['p1', null] })];
765
+ for (const g of asClaude) {
766
+ expect(ids(await store.list(g, { validOnly: false })).sort()).toEqual([
767
+ 'cc-star',
768
+ 'lv-planet-other-author',
769
+ ]);
770
+ expect(ids(await store.lexical(g, 'levelprobe', 50)).sort()).toEqual([
771
+ 'cc-star',
772
+ 'lv-planet-other-author',
773
+ ]);
774
+ expect(await store.get(g, 'lv-star')).toBeNull();
775
+ expect(await store.setTier(g, 'lv-star', 'hot')).toBe(false);
776
+ }
777
+ const asCursor = localGrant({ ...OWNER, agentId: 'cursor', projectIds: ['p1', null] });
778
+ expect(ids(await store.list(asCursor, { validOnly: false })).sort()).toEqual([
779
+ 'lv-planet-other-author',
780
+ 'lv-star',
781
+ ]);
782
+ expect(await store.get(asCursor, 'cc-star')).toBeNull();
783
+ expect(ids(await store.dense(asCursor, vec(2, 1, -1, 2), 50))).not.toContain('cc-star');
784
+ });
785
+ it('levels.existing-data-unchanged', async () => {
786
+ const legacy = [
787
+ makeMemory({ id: 'old-1', text: 'levelprobe orbit one', embedding: vec(1, 1, -1, 2) }),
788
+ makeMemory({
789
+ id: 'old-2',
790
+ text: 'levelprobe orbit two',
791
+ scope: localScope('p1'),
792
+ embedding: vec(2, 1, -1, 2),
793
+ pinned: true,
794
+ }),
795
+ makeMemory({ id: 'old-3', text: 'levelprobe orbit three', scope: localScope('p2') }),
796
+ ];
797
+ await store.upsertMany(TG, legacy);
798
+ await seedCold(store, makeMemory({ id: 'old-4', text: 'levelprobe cold', embedding: vec(3, 1, -1, 2) }));
799
+ const g = PRE_LEVEL;
800
+ const view = async () => {
801
+ const out = [];
802
+ for (const id of ['old-1', 'old-2', 'old-3', 'old-4']) {
803
+ out.push(render(await store.get(g, id)), render(await store.coldGet(g, id)));
804
+ }
805
+ out.push((await store.list(g, { validOnly: false })).map(render).join('|'));
806
+ out.push(JSON.stringify(await store.dense(g, vec(1, 1, -1, 2), 10)));
807
+ // bm25 scores use table-wide statistics: compare which memories match.
808
+ out.push(ids(await store.lexical(g, 'levelprobe orbit', 50))
809
+ .sort()
810
+ .join(','));
811
+ return out;
812
+ };
813
+ const before = await view();
814
+ for (const c of cases())
815
+ await store.upsert(c.g, c.m);
816
+ expect(await view()).toEqual(before);
817
+ });
818
+ it('levels.illegal-addresses-are-refused', async () => {
819
+ const bad = [
820
+ [
821
+ 'planet without customer',
822
+ { address: { level: 'user', orgId: 'local' } },
823
+ ],
824
+ [
825
+ 'constellation with a customer',
826
+ {
827
+ address: {
828
+ level: 'hive',
829
+ orgId: 'o',
830
+ hiveId: 'h',
831
+ customerId: 'c',
832
+ },
833
+ },
834
+ ],
835
+ [
836
+ 'star with a customer',
837
+ {
838
+ address: {
839
+ level: 'agent',
840
+ orgId: 'o',
841
+ agentId: 'a',
842
+ customerId: 'c',
843
+ },
844
+ },
845
+ ],
846
+ [
847
+ 'flyby without run',
848
+ { address: { level: 'run', orgId: 'o', agentId: 'a' } },
849
+ ],
850
+ ['unknown level', { address: { level: 'team', orgId: 'o' } }],
851
+ ['empty owner', { address: { level: 'hive', orgId: '', hiveId: 'h' } }],
852
+ [
853
+ 'unknown field',
854
+ { address: { level: 'hive', orgId: 'o', hiveId: 'h', extra: 1 } },
855
+ ],
856
+ ['empty author', { author: { agentId: '' } }],
857
+ [
858
+ 'scope that is not the address',
859
+ { address: { level: 'agent', orgId: 'local', agentId: 'cursor' }, scope: localScope() },
860
+ ],
861
+ ];
862
+ const all = nodesGrant({ level: 'user', orgId: 'local', customerId: 'self' }, { level: 'hive', orgId: 'o', hiveId: 'h' }, { level: 'agent', orgId: 'local', agentId: 'cursor' });
863
+ for (const [what, overrides] of bad) {
864
+ const m = makeMemory({ id: 'bad', ...overrides });
865
+ await expect(store.upsert(all, m), what).rejects.toThrow(RangeError);
866
+ const ok = makeMemory({ id: 'ok' });
867
+ await expect(store.upsertMany(all, [ok, m]), what).rejects.toThrow(RangeError);
868
+ expect(await store.get(all, 'ok'), what).toBeNull();
869
+ expect(await store.get(all, 'bad'), what).toBeNull();
870
+ }
871
+ });
872
+ it('levels.address-and-author-are-fixed-per-id', async () => {
873
+ const [star, hive] = [LEVEL_CASES[1], LEVEL_CASES[5]];
874
+ const g = nodesGrant(nodeOf(star.address), nodeOf(hive.address));
875
+ const m = memoryAt('fixed', star.address, star.author);
876
+ await store.upsert(g, m);
877
+ const moved = memoryAt('fixed', hive.address, hive.author);
878
+ await expect(store.upsert(g, moved)).rejects.toThrow(GrantError);
879
+ const reauthored = memoryAt('fixed', star.address, {
880
+ agentId: 'cursor',
881
+ accountRef: 'acct:2',
882
+ });
883
+ await expect(store.upsert(g, reauthored)).rejects.toThrow(GrantError);
884
+ expect(await store.get(g, 'fixed')).toEqual(canonicalMemory(m));
885
+ });
886
+ it('levels.author-restriction-is-exact', async () => {
887
+ const planet = { level: 'user', orgId: 'local', customerId: 'self' };
888
+ const mine = makeMemory({ id: 'by-cc', text: 'authorprobe one' });
889
+ const theirs = memoryAt('by-cursor', { level: 'user', orgId: 'local', customerId: 'self' }, { agentId: 'cursor' }, { text: 'authorprobe two' });
890
+ const everyone = nodesGrant(planet);
891
+ await store.upsertMany(everyone, [mine, theirs]);
892
+ expect(ids(await store.lexical(everyone, 'authorprobe', 10)).sort()).toEqual([
893
+ 'by-cc',
894
+ 'by-cursor',
895
+ ]);
896
+ expect(ids(await store.lexical(PRE_LEVEL, 'authorprobe', 10))).toEqual(['by-cc']);
897
+ // The local grant shares the Planet among the person's agents (CON-62).
898
+ expect(ids(await store.lexical(TG, 'authorprobe', 10)).sort()).toEqual([
899
+ 'by-cc',
900
+ 'by-cursor',
901
+ ]);
902
+ const cursor = nodeGrant({
903
+ principal: { ...OWNER, agentId: 'cursor' },
904
+ read: [{ node: planet, projectIds: 'all', authorAgentId: 'cursor' }],
905
+ });
906
+ expect(ids(await store.lexical(cursor, 'authorprobe', 10))).toEqual(['by-cursor']);
907
+ expect(await store.get(cursor, 'by-cc')).toBeNull();
908
+ });
909
+ it('levels.multi-node-grant-reads-across-owners', async () => {
910
+ const all = cases();
911
+ for (const c of all)
912
+ await store.upsert(c.g, c.m);
913
+ const own = makeMemory({ id: 'own', text: 'levelprobe own', embedding: vec(9, 1, -1, 2) });
914
+ await store.upsert(TG, own);
915
+ const hive = all.find((c) => c.name === 'constellation');
916
+ const galaxy = all.find((c) => c.name === 'galaxy');
917
+ const star = all.find((c) => c.name === 'star');
918
+ // My Planet (as before levels) plus a Constellation of another owner, read-only.
919
+ const g = nodeGrant({
920
+ principal: OWNER,
921
+ read: [...TG.readNodes, { node: nodeOf(hive.address), projectIds: ['p1'] }],
922
+ write: TG.writeNodes,
923
+ });
924
+ const other = all.find((c) => c.name === 'planet-other-author');
925
+ const visible = ['own', other.m.id, hive.m.id].sort();
926
+ expect(ids(await store.list(g, { validOnly: false })).sort()).toEqual(visible);
927
+ expect(ids(await store.lexical(g, 'levelprobe', 50)).sort()).toEqual(visible);
928
+ const dense = await store.dense(g, vec(9, 1, -1, 2), 50);
929
+ expect(ids(dense).sort()).toEqual(visible);
930
+ expect(dense.map((h) => h.score)).toEqual(dense.map((h) => h.score).sort((a, b) => b - a));
931
+ expect(ids(await store.dense(g, vec(9, 1, -1, 2), 1))).toEqual(['own']);
932
+ expect(await store.get(g, hive.m.id)).toEqual(canonicalMemory(hive.m));
933
+ for (const c of [galaxy, star])
934
+ expect(await store.get(g, c.m.id)).toBeNull();
935
+ // The hive side is read-only.
936
+ expect(await store.setTier(g, hive.m.id, 'hot')).toBe(false);
937
+ await store.forget(g, hive.m.id);
938
+ expect(await store.get(hive.g, hive.m.id)).not.toBeNull();
939
+ await expect(store.upsert(g, memoryAt('new-hive', hive.address, hive.author))).rejects.toThrow(GrantError);
940
+ // A project outside the pattern stays out.
941
+ const hiveP2 = memoryAt('hive-p2', { ...hive.address, projectId: 'p2' }, hive.author);
942
+ await store.upsert(hive.g, hiveP2);
943
+ expect(await store.get(g, 'hive-p2')).toBeNull();
944
+ });
581
945
  });
582
946
  }
583
947
  //# sourceMappingURL=store-conformance.js.map
package/dist/temporal.js CHANGED
@@ -1,4 +1,4 @@
1
- import * as chrono from 'chrono-node';
1
+ import { loadChrono, mayHaveDateCue } from './date-cue.js';
2
2
  /**
3
3
  * Time handling in code, never in a model (CON-34): relative expressions are resolved with
4
4
  * chrono-node against the observation's own timestamp, at day precision, in the turn's UTC
@@ -8,6 +8,13 @@ const DAY_MS = 86_400_000;
8
8
  const MIN_MS = 60_000;
9
9
  /** chrono's reference: the instant, and the offset in minutes east of UTC. */
10
10
  const ref = (at, offset = 0) => ({ instant: at, timezone: offset });
11
+ /**
12
+ * chrono's parse, loading chrono only for a text with a date cue (CON-82): a text without one
13
+ * is a text chrono reads nothing the functions below act on.
14
+ */
15
+ function parse(text, reference, option) {
16
+ return mayHaveDateCue(text) ? loadChrono().parse(text, reference, option) : [];
17
+ }
11
18
  /** Midnight of `d`'s day in the given offset, as an instant. */
12
19
  const startOfDay = (d, offset = 0) => {
13
20
  const local = new Date(d.getTime() + offset * MIN_MS);
@@ -52,8 +59,8 @@ function leadIn(text, r) {
52
59
  */
53
60
  export function factInterval(text, at, offset = 0) {
54
61
  const out = {};
55
- const forward = chrono.parse(text, ref(at, offset), { forwardDate: true });
56
- const backward = chrono.parse(text, ref(at, offset), { forwardDate: false });
62
+ const forward = parse(text, ref(at, offset), { forwardDate: true });
63
+ const backward = parse(text, ref(at, offset), { forwardDate: false });
57
64
  const day = (d) => startOfDay(d, offset);
58
65
  for (const r of forward) {
59
66
  const lead = leadIn(text, r);
@@ -113,9 +120,7 @@ export function checkWriterDate(turnText, factText, at, offset = 0) {
113
120
  if (relatives.length !== 1 || days.length !== 1)
114
121
  return { text: factText };
115
122
  const isRelative = new RegExp(RELATIVE.source, 'i');
116
- const parsed = chrono
117
- .parse(turnText, ref(at, offset), { forwardDate: false })
118
- .filter((r) => isRelative.test(r.text));
123
+ const parsed = parse(turnText, ref(at, offset), { forwardDate: false }).filter((r) => isRelative.test(r.text));
119
124
  if (parsed.length !== 1)
120
125
  return { text: factText };
121
126
  const resolved = ymd(parsed[0].start.date(), offset);
@@ -161,7 +166,7 @@ function without(query, part, dangling = false) {
161
166
  * in November is this year's March).
162
167
  */
163
168
  export function temporalIntent(query, now) {
164
- const r = chrono.parse(query, ref(now), { forwardDate: false })[0];
169
+ const r = parse(query, ref(now), { forwardDate: false })[0];
165
170
  let day;
166
171
  if (r !== undefined) {
167
172
  let d = r.start.date();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oortmem/core",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Oort core: the tiered hot, warm and cold memory engine for AI agents (types, scoring, recall, consolidation).",
5
5
  "keywords": [
6
6
  "oort",