@oortmem/core 0.2.1 → 0.3.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;
@@ -578,6 +579,316 @@ export function storeConformance(name, make) {
578
579
  expect(await nonInterference(make, seed, 60)).toBeNull();
579
580
  }
580
581
  }, 120_000);
582
+ it(`isolation.non-interference-multi-node-${seeds}-seeds`, async () => {
583
+ for (let seed = 1; seed <= seeds; seed++) {
584
+ expect(await nonInterference(make, seed, 60, { multiNode: true })).toBeNull();
585
+ }
586
+ }, 120_000);
587
+ });
588
+ levelConformance(name, make);
589
+ }
590
+ /** The owner every level case belongs to or reads as: the local principal. */
591
+ const OWNER = { orgId: 'local', customerId: 'self', agentId: 'claude-code' };
592
+ const PROVENANCE = {
593
+ derivedFrom: [{ vault: 'acct:1', memoryId: 'src-1' }],
594
+ contributors: [
595
+ {
596
+ memberRef: 'mem-1',
597
+ agentId: 'claude-code',
598
+ at: '2026-11-01T00:00:00.000Z',
599
+ action: 'asserted',
600
+ },
601
+ ],
602
+ support: { members: 1, agents: 1 },
603
+ gate: { policyVersion: 'p1', mode: 'confirmed' },
604
+ };
605
+ /** One memory per level (and a Planet memory by another agent), each with a distinct embedding. */
606
+ export const LEVEL_CASES = [
607
+ {
608
+ name: 'planet-other-author',
609
+ address: { level: 'user', orgId: 'local', customerId: 'self', projectId: 'p1' },
610
+ author: { agentId: 'cursor' },
611
+ },
612
+ {
613
+ name: 'star',
614
+ address: { level: 'agent', orgId: 'local', agentId: 'cursor', projectId: 'p1' },
615
+ author: { agentId: 'cursor' },
616
+ },
617
+ {
618
+ name: 'orbit',
619
+ address: {
620
+ level: 'agent-user',
621
+ orgId: 'org:acme',
622
+ agentId: 'support-bot',
623
+ customerId: 'end-user-1',
624
+ },
625
+ author: { agentId: 'support-bot' },
626
+ },
627
+ {
628
+ name: 'flyby',
629
+ address: {
630
+ level: 'run',
631
+ orgId: 'local',
632
+ agentId: 'claude-code',
633
+ runId: 'run-1',
634
+ customerId: 'self',
635
+ },
636
+ author: { agentId: 'claude-code' },
637
+ },
638
+ {
639
+ name: 'flyby-no-customer',
640
+ address: { level: 'run', orgId: 'local', agentId: 'claude-code', runId: 'run-2' },
641
+ author: { agentId: 'claude-code' },
642
+ },
643
+ {
644
+ name: 'constellation',
645
+ address: { level: 'hive', orgId: 'org:acme', hiveId: 'checkout', projectId: 'p1' },
646
+ author: { agentId: 'claude-code', accountRef: 'acct:1' },
647
+ provenance: PROVENANCE,
648
+ },
649
+ {
650
+ name: 'galaxy',
651
+ address: { level: 'hive', orgId: 'org:acme', hiveId: GALAXY_ROOT },
652
+ author: { agentId: 'claude-code' },
653
+ },
654
+ ];
655
+ /** A memory at `address` by `author` (scope filled to match). */
656
+ export function memoryAt(id, address, author, overrides = {}) {
657
+ return makeMemory({ id, ...addressed(address, author), ...overrides });
658
+ }
659
+ /** Read and write every project of `nodes` (no author restriction). */
660
+ export function nodesGrant(...nodes) {
661
+ return nodeGrant({
662
+ principal: OWNER,
663
+ read: nodes.map((node) => ({ node, projectIds: 'all' })),
664
+ });
665
+ }
666
+ function render(m) {
667
+ return m === null
668
+ ? 'null'
669
+ : JSON.stringify({ ...m, embedding: m.embedding === undefined ? null : [...m.embedding] });
670
+ }
671
+ /**
672
+ * Levels (CON-61): every address round-trips, illegal ones are refused, grants
673
+ * over node sets see exactly their nodes, and memories written before levels
674
+ * (and every pre-level grant) behave exactly as they did.
675
+ */
676
+ function levelConformance(name, make) {
677
+ describe(`store levels: ${name}`, () => {
678
+ let store;
679
+ beforeEach(async () => {
680
+ store = await make();
681
+ });
682
+ afterEach(async () => {
683
+ await store.close();
684
+ });
685
+ const cases = () => LEVEL_CASES.map((c, i) => {
686
+ const m = memoryAt(`lv-${c.name}`, c.address, c.author, {
687
+ text: `levelprobe ${c.name} text`,
688
+ stub: `levelprobe ${c.name}`,
689
+ embedding: vec(i + 1, 1, -1, 2),
690
+ ...(c.provenance !== undefined && { provenance: c.provenance }),
691
+ });
692
+ return { ...c, m, g: nodesGrant(nodeOf(c.address)) };
693
+ });
694
+ it('levels.planet-memories-keep-their-shape', async () => {
695
+ const m = makeMemory({ id: 'legacy', scope: localScope('p1'), embedding: vec(1, 2, 3, 4) });
696
+ await store.upsert(TG, m);
697
+ const got = await store.get(TG, 'legacy');
698
+ expect(got).toEqual(m);
699
+ expect(got !== null && ('address' in got || 'author' in got || 'provenance' in got)).toBe(false);
700
+ expect(addressOf(got)).toEqual({
701
+ level: 'user',
702
+ orgId: 'local',
703
+ customerId: 'self',
704
+ projectId: 'p1',
705
+ });
706
+ expect(authorOf(got)).toEqual({ agentId: 'claude-code' });
707
+ });
708
+ it('levels.every-level-roundtrips', async () => {
709
+ for (const c of cases()) {
710
+ await store.upsert(c.g, c.m);
711
+ const got = await store.get(c.g, c.m.id);
712
+ expect(got, c.name).toEqual(canonicalMemory(c.m));
713
+ expect(addressOf(got), c.name).toEqual(c.address);
714
+ expect(authorOf(got), c.name).toEqual(c.author);
715
+ expect(got?.provenance ?? null, c.name).toEqual(c.provenance ?? null);
716
+ expect(ids(await store.list(c.g)), c.name).toEqual([c.m.id]);
717
+ expect(ids(await store.lexical(c.g, 'levelprobe', 10)), c.name).toEqual([c.m.id]);
718
+ expect(ids(await store.dense(c.g, c.m.embedding, 10)), c.name).toEqual([
719
+ c.m.id,
720
+ ]);
721
+ const snapshot = (await store.get(c.g, c.m.id));
722
+ expect((await store.coldPut(c.g, snapshot)).skipped, c.name).toEqual([]);
723
+ expect(await store.coldGet(c.g, c.m.id), c.name).toEqual(canonicalMemory(snapshot));
724
+ expect((await store.get(c.g, c.m.id))?.tier, c.name).toBe('cold');
725
+ }
726
+ });
727
+ it('levels.pre-level-grants-see-no-other-level', async () => {
728
+ for (const c of cases())
729
+ await store.upsert(c.g, c.m);
730
+ const legacy = [TG, localGrant({ ...OWNER, projectIds: ['p1', null] })];
731
+ for (const g of legacy) {
732
+ for (const c of cases())
733
+ expect(await store.get(g, c.m.id), c.name).toBeNull();
734
+ expect(await store.list(g, { validOnly: false })).toEqual([]);
735
+ expect(await store.lexical(g, 'levelprobe', 50)).toEqual([]);
736
+ expect(await store.dense(g, vec(1, 1, -1, 2), 50)).toEqual([]);
737
+ }
738
+ });
739
+ it('levels.existing-data-unchanged', async () => {
740
+ const legacy = [
741
+ makeMemory({ id: 'old-1', text: 'levelprobe orbit one', embedding: vec(1, 1, -1, 2) }),
742
+ makeMemory({
743
+ id: 'old-2',
744
+ text: 'levelprobe orbit two',
745
+ scope: localScope('p1'),
746
+ embedding: vec(2, 1, -1, 2),
747
+ pinned: true,
748
+ }),
749
+ makeMemory({ id: 'old-3', text: 'levelprobe orbit three', scope: localScope('p2') }),
750
+ ];
751
+ await store.upsertMany(TG, legacy);
752
+ await seedCold(store, makeMemory({ id: 'old-4', text: 'levelprobe cold', embedding: vec(3, 1, -1, 2) }));
753
+ const view = async () => {
754
+ const out = [];
755
+ for (const id of ['old-1', 'old-2', 'old-3', 'old-4']) {
756
+ out.push(render(await store.get(TG, id)), render(await store.coldGet(TG, id)));
757
+ }
758
+ out.push((await store.list(TG, { validOnly: false })).map(render).join('|'));
759
+ out.push(JSON.stringify(await store.dense(TG, vec(1, 1, -1, 2), 10)));
760
+ // bm25 scores use table-wide statistics: compare which memories match.
761
+ out.push(ids(await store.lexical(TG, 'levelprobe orbit', 50))
762
+ .sort()
763
+ .join(','));
764
+ return out;
765
+ };
766
+ const before = await view();
767
+ for (const c of cases())
768
+ await store.upsert(c.g, c.m);
769
+ expect(await view()).toEqual(before);
770
+ });
771
+ it('levels.illegal-addresses-are-refused', async () => {
772
+ const bad = [
773
+ [
774
+ 'planet without customer',
775
+ { address: { level: 'user', orgId: 'local' } },
776
+ ],
777
+ [
778
+ 'constellation with a customer',
779
+ {
780
+ address: {
781
+ level: 'hive',
782
+ orgId: 'o',
783
+ hiveId: 'h',
784
+ customerId: 'c',
785
+ },
786
+ },
787
+ ],
788
+ [
789
+ 'star with a customer',
790
+ {
791
+ address: {
792
+ level: 'agent',
793
+ orgId: 'o',
794
+ agentId: 'a',
795
+ customerId: 'c',
796
+ },
797
+ },
798
+ ],
799
+ [
800
+ 'flyby without run',
801
+ { address: { level: 'run', orgId: 'o', agentId: 'a' } },
802
+ ],
803
+ ['unknown level', { address: { level: 'team', orgId: 'o' } }],
804
+ ['empty owner', { address: { level: 'hive', orgId: '', hiveId: 'h' } }],
805
+ [
806
+ 'unknown field',
807
+ { address: { level: 'hive', orgId: 'o', hiveId: 'h', extra: 1 } },
808
+ ],
809
+ ['empty author', { author: { agentId: '' } }],
810
+ [
811
+ 'scope that is not the address',
812
+ { address: { level: 'agent', orgId: 'local', agentId: 'cursor' }, scope: localScope() },
813
+ ],
814
+ ];
815
+ const all = nodesGrant({ level: 'user', orgId: 'local', customerId: 'self' }, { level: 'hive', orgId: 'o', hiveId: 'h' }, { level: 'agent', orgId: 'local', agentId: 'cursor' });
816
+ for (const [what, overrides] of bad) {
817
+ const m = makeMemory({ id: 'bad', ...overrides });
818
+ await expect(store.upsert(all, m), what).rejects.toThrow(RangeError);
819
+ const ok = makeMemory({ id: 'ok' });
820
+ await expect(store.upsertMany(all, [ok, m]), what).rejects.toThrow(RangeError);
821
+ expect(await store.get(all, 'ok'), what).toBeNull();
822
+ expect(await store.get(all, 'bad'), what).toBeNull();
823
+ }
824
+ });
825
+ it('levels.address-and-author-are-fixed-per-id', async () => {
826
+ const [star, hive] = [LEVEL_CASES[1], LEVEL_CASES[5]];
827
+ const g = nodesGrant(nodeOf(star.address), nodeOf(hive.address));
828
+ const m = memoryAt('fixed', star.address, star.author);
829
+ await store.upsert(g, m);
830
+ const moved = memoryAt('fixed', hive.address, hive.author);
831
+ await expect(store.upsert(g, moved)).rejects.toThrow(GrantError);
832
+ const reauthored = memoryAt('fixed', star.address, {
833
+ agentId: 'cursor',
834
+ accountRef: 'acct:2',
835
+ });
836
+ await expect(store.upsert(g, reauthored)).rejects.toThrow(GrantError);
837
+ expect(await store.get(g, 'fixed')).toEqual(canonicalMemory(m));
838
+ });
839
+ it('levels.author-restriction-is-exact', async () => {
840
+ const planet = { level: 'user', orgId: 'local', customerId: 'self' };
841
+ const mine = makeMemory({ id: 'by-cc', text: 'authorprobe one' });
842
+ const theirs = memoryAt('by-cursor', { level: 'user', orgId: 'local', customerId: 'self' }, { agentId: 'cursor' }, { text: 'authorprobe two' });
843
+ const everyone = nodesGrant(planet);
844
+ await store.upsertMany(everyone, [mine, theirs]);
845
+ expect(ids(await store.lexical(everyone, 'authorprobe', 10)).sort()).toEqual([
846
+ 'by-cc',
847
+ 'by-cursor',
848
+ ]);
849
+ expect(ids(await store.lexical(TG, 'authorprobe', 10))).toEqual(['by-cc']);
850
+ const cursor = nodeGrant({
851
+ principal: { ...OWNER, agentId: 'cursor' },
852
+ read: [{ node: planet, projectIds: 'all', authorAgentId: 'cursor' }],
853
+ });
854
+ expect(ids(await store.lexical(cursor, 'authorprobe', 10))).toEqual(['by-cursor']);
855
+ expect(await store.get(cursor, 'by-cc')).toBeNull();
856
+ });
857
+ it('levels.multi-node-grant-reads-across-owners', async () => {
858
+ const all = cases();
859
+ for (const c of all)
860
+ await store.upsert(c.g, c.m);
861
+ const own = makeMemory({ id: 'own', text: 'levelprobe own', embedding: vec(9, 1, -1, 2) });
862
+ await store.upsert(TG, own);
863
+ const hive = all.find((c) => c.name === 'constellation');
864
+ const galaxy = all.find((c) => c.name === 'galaxy');
865
+ const star = all.find((c) => c.name === 'star');
866
+ // My Planet (as before levels) plus a Constellation of another owner, read-only.
867
+ const g = nodeGrant({
868
+ principal: OWNER,
869
+ read: [...TG.readNodes, { node: nodeOf(hive.address), projectIds: ['p1'] }],
870
+ write: TG.writeNodes,
871
+ });
872
+ const visible = ['own', hive.m.id].sort();
873
+ expect(ids(await store.list(g, { validOnly: false })).sort()).toEqual(visible);
874
+ expect(ids(await store.lexical(g, 'levelprobe', 50)).sort()).toEqual(visible);
875
+ const dense = await store.dense(g, vec(9, 1, -1, 2), 50);
876
+ expect(ids(dense).sort()).toEqual(visible);
877
+ expect(dense.map((h) => h.score)).toEqual(dense.map((h) => h.score).sort((a, b) => b - a));
878
+ expect(ids(await store.dense(g, vec(9, 1, -1, 2), 1))).toEqual(['own']);
879
+ expect(await store.get(g, hive.m.id)).toEqual(canonicalMemory(hive.m));
880
+ for (const c of [galaxy, star])
881
+ expect(await store.get(g, c.m.id)).toBeNull();
882
+ // The hive side is read-only.
883
+ expect(await store.setTier(g, hive.m.id, 'hot')).toBe(false);
884
+ await store.forget(g, hive.m.id);
885
+ expect(await store.get(hive.g, hive.m.id)).not.toBeNull();
886
+ await expect(store.upsert(g, memoryAt('new-hive', hive.address, hive.author))).rejects.toThrow(GrantError);
887
+ // A project outside the pattern stays out.
888
+ const other = memoryAt('hive-p2', { ...hive.address, projectId: 'p2' }, hive.author);
889
+ await store.upsert(hive.g, other);
890
+ expect(await store.get(g, 'hive-p2')).toBeNull();
891
+ });
581
892
  });
582
893
  }
583
894
  //# 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.3.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",