@tangle-network/agent-runtime 0.340.0 → 0.342.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/{activation-D2PVljaw.js → activation-CWPmt0Do.js} +4 -4
- package/dist/{activation-D2PVljaw.js.map → activation-CWPmt0Do.js.map} +1 -1
- package/dist/{activation-DGDl4wwI.d.ts → activation-CXqtijue.d.ts} +2 -2
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +2 -2
- package/dist/{bundle-DNUq-OPW.js → bundle-Cwpit6Tg.js} +2 -2
- package/dist/{bundle-DNUq-OPW.js.map → bundle-Cwpit6Tg.js.map} +1 -1
- package/dist/candidate-execution/index.js +4 -4
- package/dist/{driver-control-acknowledgers-MTrs8HAF.js → coordination-manager-B5JsolDR.js} +4852 -4539
- package/dist/coordination-manager-B5JsolDR.js.map +1 -0
- package/dist/{delegate-CtJIS-tp.js → delegate-Rl-5paLJ.js} +2 -2
- package/dist/{delegate-CtJIS-tp.js.map → delegate-Rl-5paLJ.js.map} +1 -1
- package/dist/durable.d.ts +1 -1
- package/dist/durable.js +451 -358
- package/dist/durable.js.map +1 -1
- package/dist/{graph-DfWqp_1e.js → graph-Ce-59ZXn.js} +5 -5
- package/dist/{graph-DfWqp_1e.js.map → graph-Ce-59ZXn.js.map} +1 -1
- package/dist/{improve-CKRVQqzt.js → improve-BZXt2qM0.js} +837 -704
- package/dist/improve-BZXt2qM0.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +9 -9
- package/dist/intelligence.d.ts +2 -2
- package/dist/intelligence.js +205 -191
- package/dist/intelligence.js.map +1 -1
- package/dist/kernel.d.ts +1 -1
- package/dist/kernel.js +9 -9
- package/dist/{loop-runner-DNWbTcVm.js → loop-runner-B0MV1-CD.js} +2 -2
- package/dist/{loop-runner-DNWbTcVm.js.map → loop-runner-B0MV1-CD.js.map} +1 -1
- package/dist/{loop-runner-C4EWEcXf.d.ts → loop-runner-CuhbgVGo.d.ts} +2 -2
- package/dist/loop-runner-bin.d.ts +1 -1
- package/dist/loop-runner-bin.js +1 -1
- package/dist/mcp/bin.js +3 -3
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +5 -5
- package/dist/{prepare-FzJoS68l.js → prepare-C-cWCyt3.js} +988 -735
- package/dist/prepare-C-cWCyt3.js.map +1 -0
- package/dist/{profile-DToH0XEs.js → profile-CbMw5hAn.js} +2 -2
- package/dist/{profile-DToH0XEs.js.map → profile-CbMw5hAn.js.map} +1 -1
- package/dist/profiles.d.ts +1 -1
- package/dist/{provision-supervisor-DqvWXmAE.js → provision-supervisor-DL6gEMQp.js} +444 -419
- package/dist/provision-supervisor-DL6gEMQp.js.map +1 -0
- package/dist/{runtime-C55Zgo1R.js → runtime-eghmj6W4.js} +1245 -1028
- package/dist/runtime-eghmj6W4.js.map +1 -0
- package/dist/{sandbox-profile-compat-DRr3JwDx.js → sandbox-profile-compat-FN2kab2-.js} +72 -72
- package/dist/{sandbox-profile-compat-DRr3JwDx.js.map → sandbox-profile-compat-FN2kab2-.js.map} +1 -1
- package/dist/{search-executor-ChOlRBgg.js → search-executor-rbjdZ3OD.js} +3 -3
- package/dist/{search-executor-ChOlRBgg.js.map → search-executor-rbjdZ3OD.js.map} +1 -1
- package/dist/{server-DkVBSN_S.js → server-BiK1Kcno.js} +4 -4
- package/dist/{server-DkVBSN_S.js.map → server-BiK1Kcno.js.map} +1 -1
- package/dist/{structural-rollout-C1LEk_zq.js → structural-rollout-BD9IN7K_.js} +3 -3
- package/dist/{structural-rollout-C1LEk_zq.js.map → structural-rollout-BD9IN7K_.js.map} +1 -1
- package/dist/{supervise-CaUAxZZP.js → supervise-DmOi7gll.js} +6346 -5447
- package/dist/supervise-DmOi7gll.js.map +1 -0
- package/dist/{supervisor-VXuN2OHT.js → supervisor-DUWcecAd.js} +13822 -12826
- package/dist/supervisor-DUWcecAd.js.map +1 -0
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +13 -13
- package/dist/tui/index.d.ts +1 -1
- package/dist/tui/index.js +1 -1
- package/dist/{types-DZ6L0Bv3.d.ts → types-Cgs89PQd.d.ts} +266 -356
- package/dist/workspace-store/index.js +273 -265
- package/dist/workspace-store/index.js.map +1 -1
- package/package.json +8 -8
- package/dist/driver-control-acknowledgers-MTrs8HAF.js.map +0 -1
- package/dist/improve-CKRVQqzt.js.map +0 -1
- package/dist/prepare-FzJoS68l.js.map +0 -1
- package/dist/provision-supervisor-DqvWXmAE.js.map +0 -1
- package/dist/runtime-C55Zgo1R.js.map +0 -1
- package/dist/supervise-CaUAxZZP.js.map +0 -1
- package/dist/supervisor-VXuN2OHT.js.map +0 -1
|
@@ -661,274 +661,282 @@ const idempotentOperationCase = ["operationId: a repeated put or delete returns
|
|
|
661
661
|
operationId: ""
|
|
662
662
|
}), TypeError, "an empty operationId");
|
|
663
663
|
}];
|
|
664
|
+
const compareAndSetCase = ["compare-and-set: create, stale update, and re-create conflict with a typed error", async (store) => {
|
|
665
|
+
const base = {
|
|
666
|
+
workspaceId: W,
|
|
667
|
+
path: "/files/a.md",
|
|
668
|
+
kind: "file",
|
|
669
|
+
actor: "alice"
|
|
670
|
+
};
|
|
671
|
+
const first = await store.put({
|
|
672
|
+
...base,
|
|
673
|
+
content: "one",
|
|
674
|
+
ifRevision: null
|
|
675
|
+
});
|
|
676
|
+
check(first.revision >= 1 && first.content === "one", "create returns revision and content");
|
|
677
|
+
check(first.createdBy === "alice" && first.createdAt && first.updatedAt, "create stamps author and times");
|
|
678
|
+
check((await rejects(() => store.put({
|
|
679
|
+
...base,
|
|
680
|
+
content: "dup",
|
|
681
|
+
ifRevision: null
|
|
682
|
+
}), WorkspaceConflictError, "create over a live path")).actual === first.revision, "conflict reports the live revision");
|
|
683
|
+
const second = await store.put({
|
|
684
|
+
...base,
|
|
685
|
+
content: "two",
|
|
686
|
+
ifRevision: first.revision,
|
|
687
|
+
actor: "bob"
|
|
688
|
+
});
|
|
689
|
+
check(second.revision > first.revision && second.id === first.id, "update bumps revision, keeps id");
|
|
690
|
+
check(second.createdBy === "alice" && second.updatedBy === "bob", "update keeps creator, records updater");
|
|
691
|
+
await rejects(() => store.put({
|
|
692
|
+
...base,
|
|
693
|
+
content: "stale",
|
|
694
|
+
ifRevision: first.revision
|
|
695
|
+
}), WorkspaceConflictError, "update at a stale revision");
|
|
696
|
+
check((await store.get(W, base.path))?.content === "two", "a refused write changes nothing");
|
|
697
|
+
check((await Promise.allSettled(["x", "y"].map((content) => store.put({
|
|
698
|
+
...base,
|
|
699
|
+
content,
|
|
700
|
+
ifRevision: second.revision
|
|
701
|
+
})))).filter((r) => r.status === "fulfilled").length === 1, "exactly one racing write wins");
|
|
702
|
+
}];
|
|
703
|
+
const entriesCase = ["entries: exactly one of content or bytes, normalized paths, tenant isolation, sorted list", async (store) => {
|
|
704
|
+
const bytes = {
|
|
705
|
+
key: "r2/obj-1",
|
|
706
|
+
size: 42,
|
|
707
|
+
contentType: "application/pdf"
|
|
708
|
+
};
|
|
709
|
+
const file = await store.put({
|
|
710
|
+
workspaceId: W,
|
|
711
|
+
path: "/files/b.pdf",
|
|
712
|
+
kind: "file",
|
|
713
|
+
actor: "alice",
|
|
714
|
+
bytes,
|
|
715
|
+
ifRevision: null
|
|
716
|
+
});
|
|
717
|
+
check(file.bytes?.key === bytes.key && file.content === void 0, "bytes reference round-trips");
|
|
718
|
+
await rejects(() => store.put({
|
|
719
|
+
workspaceId: W,
|
|
720
|
+
path: "/files/c",
|
|
721
|
+
kind: "file",
|
|
722
|
+
actor: "a",
|
|
723
|
+
ifRevision: null
|
|
724
|
+
}), TypeError, "neither content nor bytes");
|
|
725
|
+
for (const path of [
|
|
726
|
+
"files/x",
|
|
727
|
+
"/files/../x",
|
|
728
|
+
"/files//x",
|
|
729
|
+
"/files/x/"
|
|
730
|
+
]) await rejects(() => store.put({
|
|
731
|
+
workspaceId: W,
|
|
732
|
+
path,
|
|
733
|
+
kind: "file",
|
|
734
|
+
actor: "a",
|
|
735
|
+
content: "x",
|
|
736
|
+
ifRevision: null
|
|
737
|
+
}), TypeError, `path ${path}`);
|
|
738
|
+
await store.put({
|
|
739
|
+
workspaceId: W,
|
|
740
|
+
path: "/files/a.md",
|
|
741
|
+
kind: "file",
|
|
742
|
+
actor: "a",
|
|
743
|
+
content: "a",
|
|
744
|
+
ifRevision: null
|
|
745
|
+
});
|
|
746
|
+
await store.put({
|
|
747
|
+
workspaceId: "other",
|
|
748
|
+
path: "/files/z.md",
|
|
749
|
+
kind: "file",
|
|
750
|
+
actor: "a",
|
|
751
|
+
content: "z",
|
|
752
|
+
ifRevision: null
|
|
753
|
+
});
|
|
754
|
+
check((await store.list(W, "/files/")).map((e) => e.path).join() === "/files/a.md,/files/b.pdf", "list is sorted and tenant-scoped");
|
|
755
|
+
check(await store.get("other", "/files/a.md") === null, "another tenant cannot read the path");
|
|
756
|
+
check((await store.put({
|
|
757
|
+
workspaceId: W,
|
|
758
|
+
path: "/memories/members/alice/m1",
|
|
759
|
+
kind: "fact",
|
|
760
|
+
actor: "alice",
|
|
761
|
+
content: "x",
|
|
762
|
+
ifRevision: null
|
|
763
|
+
})).scope === "member" && file.scope === "workspace", "scope follows the path");
|
|
764
|
+
}];
|
|
765
|
+
const deleteTombstoneCase = ["delete erases content and keeps a tombstone; revisions stay monotonic", async (store) => {
|
|
766
|
+
const base = {
|
|
767
|
+
workspaceId: W,
|
|
768
|
+
path: "/files/secret.md",
|
|
769
|
+
kind: "file",
|
|
770
|
+
actor: "alice"
|
|
771
|
+
};
|
|
772
|
+
const entry = await store.put({
|
|
773
|
+
...base,
|
|
774
|
+
content: "card 4111",
|
|
775
|
+
ifRevision: null
|
|
776
|
+
});
|
|
777
|
+
await rejects(() => store.delete({
|
|
778
|
+
workspaceId: W,
|
|
779
|
+
path: base.path,
|
|
780
|
+
ifRevision: entry.revision + 5,
|
|
781
|
+
actor: "alice"
|
|
782
|
+
}), WorkspaceConflictError, "delete at a stale revision");
|
|
783
|
+
const tombstone = await store.delete({
|
|
784
|
+
workspaceId: W,
|
|
785
|
+
path: base.path,
|
|
786
|
+
ifRevision: entry.revision,
|
|
787
|
+
actor: "bob"
|
|
788
|
+
});
|
|
789
|
+
check(tombstone !== null, "delete returns a tombstone");
|
|
790
|
+
check(tombstone.id === entry.id && tombstone.revision > entry.revision, "tombstone keeps id, bumps revision");
|
|
791
|
+
check(tombstone.deletedBy === "bob" && Boolean(tombstone.deletedAt), "tombstone records who and when");
|
|
792
|
+
check(!JSON.stringify(tombstone).includes("4111"), "tombstone holds no content");
|
|
793
|
+
check(await store.get(W, base.path) === null, "deleted entry is gone");
|
|
794
|
+
check((await store.list(W, "/files/")).length === 0, "deleted entry is not listed");
|
|
795
|
+
const audit = await store.tombstones(W, "/files/");
|
|
796
|
+
check(audit.length === 1 && !JSON.stringify(audit).includes("4111"), "audit rows hold no content");
|
|
797
|
+
check(await store.delete({
|
|
798
|
+
workspaceId: W,
|
|
799
|
+
path: "/files/none",
|
|
800
|
+
actor: "a"
|
|
801
|
+
}) === null, "unconditional delete of a missing path is a no-op");
|
|
802
|
+
check((await store.put({
|
|
803
|
+
...base,
|
|
804
|
+
content: "new",
|
|
805
|
+
ifRevision: null
|
|
806
|
+
})).revision > tombstone.revision, "re-created path continues the revision sequence");
|
|
807
|
+
}];
|
|
808
|
+
const rememberRecallCase = ["tools: remember then recall round-trips, and forget erases", async (store) => {
|
|
809
|
+
const call = toolsFor(store, alice);
|
|
810
|
+
const saved = await call("memory.remember", {
|
|
811
|
+
text: "The walk-in freezer code is on the clipboard",
|
|
812
|
+
kind: "fact",
|
|
813
|
+
scope: "workspace"
|
|
814
|
+
});
|
|
815
|
+
check(saved.saved && saved.path === `/memories/workspace/${saved.id}`, "remember saves under workspace");
|
|
816
|
+
const found = await toolsFor(store, bob)("memory.recall", { query: "freezer code" });
|
|
817
|
+
check(found.memories[0]?.id === saved.id, "another member recalls a workspace fact");
|
|
818
|
+
check(found.memories[0]?.text === "The walk-in freezer code is on the clipboard", "whitespace is collapsed");
|
|
819
|
+
const updated = await call("memory.remember", {
|
|
820
|
+
text: "The freezer code is in the office",
|
|
821
|
+
kind: "fact",
|
|
822
|
+
scope: "workspace",
|
|
823
|
+
replaces: saved.id
|
|
824
|
+
});
|
|
825
|
+
check(updated.id === saved.id && updated.revision > saved.revision, "replaces updates in place");
|
|
826
|
+
await call("memory.forget", { id: saved.id });
|
|
827
|
+
check((await call("memory.recall", { query: "freezer" })).memories.length === 0, "forgotten memory is not recalled");
|
|
828
|
+
check((await store.tombstones(W)).some((row) => row.id === saved.id), "forget leaves a tombstone");
|
|
829
|
+
}];
|
|
830
|
+
const memberScopeCase = ["tools: a member cannot see or touch another member’s scope", async (store) => {
|
|
831
|
+
const mine = await toolsFor(store, alice)("memory.remember", {
|
|
832
|
+
text: "Alice prefers replies in Spanish",
|
|
833
|
+
kind: "preference",
|
|
834
|
+
scope: "me"
|
|
835
|
+
});
|
|
836
|
+
check(mine.path === `/memories/members/alice/${mine.id}`, "scope me writes under the member");
|
|
837
|
+
const asBob = toolsFor(store, bob);
|
|
838
|
+
check((await asBob("memory.recall", { query: "Spanish replies" })).memories.length === 0, "recall is member-isolated");
|
|
839
|
+
await rejects(() => asBob("memory.forget", { id: mine.id }), Error, "forget of another member’s memory");
|
|
840
|
+
check(await store.get(W, mine.path) !== null, "the other member’s memory survives");
|
|
841
|
+
await rejects(() => asBob("files.read", { path: mine.path }), Error, "files.read outside /files and /knowledge");
|
|
842
|
+
check(!renderPinnedMemory(await readTurnMemories(store, bob)).includes("Spanish"), "pinned memory is member-isolated");
|
|
843
|
+
check(renderPinnedMemory(await readTurnMemories(store, alice)).includes("Spanish"), "the owner sees it pinned");
|
|
844
|
+
}];
|
|
845
|
+
const policyCase = ["tools: default policy and admit hook refuse by name", async (store) => {
|
|
846
|
+
await rejects(() => toolsFor(store, alice)("memory.remember", {
|
|
847
|
+
text: "Never discount",
|
|
848
|
+
kind: "instruction",
|
|
849
|
+
scope: "workspace"
|
|
850
|
+
}), WorkspacePolicyError, "member instruction");
|
|
851
|
+
await rejects(() => toolsFor(store, alice)("memory.remember", {
|
|
852
|
+
text: "Use metric",
|
|
853
|
+
kind: "preference",
|
|
854
|
+
scope: "workspace"
|
|
855
|
+
}), WorkspacePolicyError, "member workspace preference");
|
|
856
|
+
await rejects(() => toolsFor(store, guest)("memory.remember", {
|
|
857
|
+
text: "I am Gus",
|
|
858
|
+
kind: "fact",
|
|
859
|
+
scope: "me"
|
|
860
|
+
}), WorkspacePolicyError, "guest write");
|
|
861
|
+
await rejects(() => toolsFor(store, alice)("files.write", {
|
|
862
|
+
path: "/knowledge/menu.md",
|
|
863
|
+
content: "x",
|
|
864
|
+
ifRevision: null
|
|
865
|
+
}), WorkspacePolicyError, "member knowledge write");
|
|
866
|
+
check((await toolsFor(store, owner)("memory.remember", {
|
|
867
|
+
text: "Never offer discounts",
|
|
868
|
+
kind: "instruction",
|
|
869
|
+
scope: "workspace"
|
|
870
|
+
})).saved, "owner saves an instruction");
|
|
871
|
+
const noPhones = defaultMemoryPolicy({ admit: (_turn, { text }) => /\d{3}-\d{4}/.test(text) ? "Phone numbers are not saved" : null });
|
|
872
|
+
check((await rejects(() => toolsFor(store, alice, noPhones)("memory.remember", {
|
|
873
|
+
text: "Call 555-0100",
|
|
874
|
+
kind: "fact",
|
|
875
|
+
scope: "me"
|
|
876
|
+
}), WorkspacePolicyError, "admit hook")).message === "Phone numbers are not saved", "refusal carries the product reason");
|
|
877
|
+
check((await store.list(W, "/memories/members/alice/")).length === 0, "refused writes store nothing");
|
|
878
|
+
}];
|
|
879
|
+
const fileToolsCase = ["tools: files write, read, CAS conflict, search, delete", async (store) => {
|
|
880
|
+
const call = toolsFor(store, alice);
|
|
881
|
+
const created = await call("files.write", {
|
|
882
|
+
path: "/files/notes/menu.md",
|
|
883
|
+
content: "Tuesday special: lamb",
|
|
884
|
+
ifRevision: null
|
|
885
|
+
});
|
|
886
|
+
await rejects(() => call("files.write", {
|
|
887
|
+
path: "/files/notes/menu.md",
|
|
888
|
+
content: "x",
|
|
889
|
+
ifRevision: null
|
|
890
|
+
}), WorkspaceConflictError, "files.write create over a live file");
|
|
891
|
+
const read = await toolsFor(store, bob)("files.read", { path: "/files/notes/menu.md" });
|
|
892
|
+
check(read.content === "Tuesday special: lamb" && read.revision === created.revision, "files.read returns content and revision");
|
|
893
|
+
const hits = await call("files.search", { query: "lamb special" });
|
|
894
|
+
check(hits.files[0]?.path === "/files/notes/menu.md" && hits.files[0].snippet.includes("lamb"), "files.search finds text");
|
|
895
|
+
check((await call("files.list", {})).files.length === 1, "files.list lists it");
|
|
896
|
+
await call("files.delete", {
|
|
897
|
+
path: "/files/notes/menu.md",
|
|
898
|
+
ifRevision: created.revision
|
|
899
|
+
});
|
|
900
|
+
await rejects(() => call("files.read", { path: "/files/notes/menu.md" }), Error, "read after delete");
|
|
901
|
+
}];
|
|
902
|
+
const pinnedMemoryCase = ["renderPinnedMemory: stable, ordered, budgeted, ends with the recall index", async (store) => {
|
|
903
|
+
await toolsFor(store, alice)("memory.remember", {
|
|
904
|
+
text: "Alice works weekends",
|
|
905
|
+
kind: "fact",
|
|
906
|
+
scope: "me"
|
|
907
|
+
});
|
|
908
|
+
await toolsFor(store, owner)("memory.remember", {
|
|
909
|
+
text: "Always greet by name",
|
|
910
|
+
kind: "instruction",
|
|
911
|
+
scope: "workspace"
|
|
912
|
+
});
|
|
913
|
+
for (let i = 0; i < 40; i += 1) await toolsFor(store, alice)("memory.remember", {
|
|
914
|
+
text: `Supplier ${i} delivers on day ${i % 7}`,
|
|
915
|
+
kind: "fact",
|
|
916
|
+
scope: "workspace"
|
|
917
|
+
});
|
|
918
|
+
const entries = await readTurnMemories(store, alice);
|
|
919
|
+
const block = renderPinnedMemory(entries);
|
|
920
|
+
check(block === renderPinnedMemory([...entries].reverse()), "input order does not change the output");
|
|
921
|
+
check(block === renderPinnedMemory(await readTurnMemories(store, alice)), "identical input renders identically");
|
|
922
|
+
const lines = block.split("\n");
|
|
923
|
+
check(lines.findIndex((l) => l.includes("Always greet")) < lines.findIndex((l) => l.includes("Supplier")), "instructions first");
|
|
924
|
+
check(lines.at(-1) === "</saved-memory>" && lines.at(-2)?.startsWith("Recall more:"), "ends with the recall index");
|
|
925
|
+
check(entries.every((e) => block.includes(`[${e.id}]`)), "every memory is shown with its id under the default budget");
|
|
926
|
+
const small = renderPinnedMemory(entries, { maxTokens: 120 });
|
|
927
|
+
check(small.length <= 480 && /\d+ more not shown\./.test(small), "a small budget truncates and counts");
|
|
928
|
+
}];
|
|
664
929
|
/** The contract as named cases; each case builds its own store. */
|
|
665
930
|
function workspaceStoreContractCases(makeStore, options = {}) {
|
|
666
931
|
const cases = [
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
...base,
|
|
676
|
-
content: "one",
|
|
677
|
-
ifRevision: null
|
|
678
|
-
});
|
|
679
|
-
check(first.revision >= 1 && first.content === "one", "create returns revision and content");
|
|
680
|
-
check(first.createdBy === "alice" && first.createdAt && first.updatedAt, "create stamps author and times");
|
|
681
|
-
check((await rejects(() => store.put({
|
|
682
|
-
...base,
|
|
683
|
-
content: "dup",
|
|
684
|
-
ifRevision: null
|
|
685
|
-
}), WorkspaceConflictError, "create over a live path")).actual === first.revision, "conflict reports the live revision");
|
|
686
|
-
const second = await store.put({
|
|
687
|
-
...base,
|
|
688
|
-
content: "two",
|
|
689
|
-
ifRevision: first.revision,
|
|
690
|
-
actor: "bob"
|
|
691
|
-
});
|
|
692
|
-
check(second.revision > first.revision && second.id === first.id, "update bumps revision, keeps id");
|
|
693
|
-
check(second.createdBy === "alice" && second.updatedBy === "bob", "update keeps creator, records updater");
|
|
694
|
-
await rejects(() => store.put({
|
|
695
|
-
...base,
|
|
696
|
-
content: "stale",
|
|
697
|
-
ifRevision: first.revision
|
|
698
|
-
}), WorkspaceConflictError, "update at a stale revision");
|
|
699
|
-
check((await store.get(W, base.path))?.content === "two", "a refused write changes nothing");
|
|
700
|
-
check((await Promise.allSettled(["x", "y"].map((content) => store.put({
|
|
701
|
-
...base,
|
|
702
|
-
content,
|
|
703
|
-
ifRevision: second.revision
|
|
704
|
-
})))).filter((r) => r.status === "fulfilled").length === 1, "exactly one racing write wins");
|
|
705
|
-
}],
|
|
706
|
-
["entries: exactly one of content or bytes, normalized paths, tenant isolation, sorted list", async (store) => {
|
|
707
|
-
const bytes = {
|
|
708
|
-
key: "r2/obj-1",
|
|
709
|
-
size: 42,
|
|
710
|
-
contentType: "application/pdf"
|
|
711
|
-
};
|
|
712
|
-
const file = await store.put({
|
|
713
|
-
workspaceId: W,
|
|
714
|
-
path: "/files/b.pdf",
|
|
715
|
-
kind: "file",
|
|
716
|
-
actor: "alice",
|
|
717
|
-
bytes,
|
|
718
|
-
ifRevision: null
|
|
719
|
-
});
|
|
720
|
-
check(file.bytes?.key === bytes.key && file.content === void 0, "bytes reference round-trips");
|
|
721
|
-
await rejects(() => store.put({
|
|
722
|
-
workspaceId: W,
|
|
723
|
-
path: "/files/c",
|
|
724
|
-
kind: "file",
|
|
725
|
-
actor: "a",
|
|
726
|
-
ifRevision: null
|
|
727
|
-
}), TypeError, "neither content nor bytes");
|
|
728
|
-
for (const path of [
|
|
729
|
-
"files/x",
|
|
730
|
-
"/files/../x",
|
|
731
|
-
"/files//x",
|
|
732
|
-
"/files/x/"
|
|
733
|
-
]) await rejects(() => store.put({
|
|
734
|
-
workspaceId: W,
|
|
735
|
-
path,
|
|
736
|
-
kind: "file",
|
|
737
|
-
actor: "a",
|
|
738
|
-
content: "x",
|
|
739
|
-
ifRevision: null
|
|
740
|
-
}), TypeError, `path ${path}`);
|
|
741
|
-
await store.put({
|
|
742
|
-
workspaceId: W,
|
|
743
|
-
path: "/files/a.md",
|
|
744
|
-
kind: "file",
|
|
745
|
-
actor: "a",
|
|
746
|
-
content: "a",
|
|
747
|
-
ifRevision: null
|
|
748
|
-
});
|
|
749
|
-
await store.put({
|
|
750
|
-
workspaceId: "other",
|
|
751
|
-
path: "/files/z.md",
|
|
752
|
-
kind: "file",
|
|
753
|
-
actor: "a",
|
|
754
|
-
content: "z",
|
|
755
|
-
ifRevision: null
|
|
756
|
-
});
|
|
757
|
-
check((await store.list(W, "/files/")).map((e) => e.path).join() === "/files/a.md,/files/b.pdf", "list is sorted and tenant-scoped");
|
|
758
|
-
check(await store.get("other", "/files/a.md") === null, "another tenant cannot read the path");
|
|
759
|
-
check((await store.put({
|
|
760
|
-
workspaceId: W,
|
|
761
|
-
path: "/memories/members/alice/m1",
|
|
762
|
-
kind: "fact",
|
|
763
|
-
actor: "alice",
|
|
764
|
-
content: "x",
|
|
765
|
-
ifRevision: null
|
|
766
|
-
})).scope === "member" && file.scope === "workspace", "scope follows the path");
|
|
767
|
-
}],
|
|
768
|
-
["delete erases content and keeps a tombstone; revisions stay monotonic", async (store) => {
|
|
769
|
-
const base = {
|
|
770
|
-
workspaceId: W,
|
|
771
|
-
path: "/files/secret.md",
|
|
772
|
-
kind: "file",
|
|
773
|
-
actor: "alice"
|
|
774
|
-
};
|
|
775
|
-
const entry = await store.put({
|
|
776
|
-
...base,
|
|
777
|
-
content: "card 4111",
|
|
778
|
-
ifRevision: null
|
|
779
|
-
});
|
|
780
|
-
await rejects(() => store.delete({
|
|
781
|
-
workspaceId: W,
|
|
782
|
-
path: base.path,
|
|
783
|
-
ifRevision: entry.revision + 5,
|
|
784
|
-
actor: "alice"
|
|
785
|
-
}), WorkspaceConflictError, "delete at a stale revision");
|
|
786
|
-
const tombstone = await store.delete({
|
|
787
|
-
workspaceId: W,
|
|
788
|
-
path: base.path,
|
|
789
|
-
ifRevision: entry.revision,
|
|
790
|
-
actor: "bob"
|
|
791
|
-
});
|
|
792
|
-
check(tombstone !== null, "delete returns a tombstone");
|
|
793
|
-
check(tombstone.id === entry.id && tombstone.revision > entry.revision, "tombstone keeps id, bumps revision");
|
|
794
|
-
check(tombstone.deletedBy === "bob" && Boolean(tombstone.deletedAt), "tombstone records who and when");
|
|
795
|
-
check(!JSON.stringify(tombstone).includes("4111"), "tombstone holds no content");
|
|
796
|
-
check(await store.get(W, base.path) === null, "deleted entry is gone");
|
|
797
|
-
check((await store.list(W, "/files/")).length === 0, "deleted entry is not listed");
|
|
798
|
-
const audit = await store.tombstones(W, "/files/");
|
|
799
|
-
check(audit.length === 1 && !JSON.stringify(audit).includes("4111"), "audit rows hold no content");
|
|
800
|
-
check(await store.delete({
|
|
801
|
-
workspaceId: W,
|
|
802
|
-
path: "/files/none",
|
|
803
|
-
actor: "a"
|
|
804
|
-
}) === null, "unconditional delete of a missing path is a no-op");
|
|
805
|
-
check((await store.put({
|
|
806
|
-
...base,
|
|
807
|
-
content: "new",
|
|
808
|
-
ifRevision: null
|
|
809
|
-
})).revision > tombstone.revision, "re-created path continues the revision sequence");
|
|
810
|
-
}],
|
|
811
|
-
["tools: remember then recall round-trips, and forget erases", async (store) => {
|
|
812
|
-
const call = toolsFor(store, alice);
|
|
813
|
-
const saved = await call("memory.remember", {
|
|
814
|
-
text: "The walk-in freezer code is on the clipboard",
|
|
815
|
-
kind: "fact",
|
|
816
|
-
scope: "workspace"
|
|
817
|
-
});
|
|
818
|
-
check(saved.saved && saved.path === `/memories/workspace/${saved.id}`, "remember saves under workspace");
|
|
819
|
-
const found = await toolsFor(store, bob)("memory.recall", { query: "freezer code" });
|
|
820
|
-
check(found.memories[0]?.id === saved.id, "another member recalls a workspace fact");
|
|
821
|
-
check(found.memories[0]?.text === "The walk-in freezer code is on the clipboard", "whitespace is collapsed");
|
|
822
|
-
const updated = await call("memory.remember", {
|
|
823
|
-
text: "The freezer code is in the office",
|
|
824
|
-
kind: "fact",
|
|
825
|
-
scope: "workspace",
|
|
826
|
-
replaces: saved.id
|
|
827
|
-
});
|
|
828
|
-
check(updated.id === saved.id && updated.revision > saved.revision, "replaces updates in place");
|
|
829
|
-
await call("memory.forget", { id: saved.id });
|
|
830
|
-
check((await call("memory.recall", { query: "freezer" })).memories.length === 0, "forgotten memory is not recalled");
|
|
831
|
-
check((await store.tombstones(W)).some((row) => row.id === saved.id), "forget leaves a tombstone");
|
|
832
|
-
}],
|
|
833
|
-
["tools: a member cannot see or touch another member’s scope", async (store) => {
|
|
834
|
-
const mine = await toolsFor(store, alice)("memory.remember", {
|
|
835
|
-
text: "Alice prefers replies in Spanish",
|
|
836
|
-
kind: "preference",
|
|
837
|
-
scope: "me"
|
|
838
|
-
});
|
|
839
|
-
check(mine.path === `/memories/members/alice/${mine.id}`, "scope me writes under the member");
|
|
840
|
-
const asBob = toolsFor(store, bob);
|
|
841
|
-
check((await asBob("memory.recall", { query: "Spanish replies" })).memories.length === 0, "recall is member-isolated");
|
|
842
|
-
await rejects(() => asBob("memory.forget", { id: mine.id }), Error, "forget of another member’s memory");
|
|
843
|
-
check(await store.get(W, mine.path) !== null, "the other member’s memory survives");
|
|
844
|
-
await rejects(() => asBob("files.read", { path: mine.path }), Error, "files.read outside /files and /knowledge");
|
|
845
|
-
check(!renderPinnedMemory(await readTurnMemories(store, bob)).includes("Spanish"), "pinned memory is member-isolated");
|
|
846
|
-
check(renderPinnedMemory(await readTurnMemories(store, alice)).includes("Spanish"), "the owner sees it pinned");
|
|
847
|
-
}],
|
|
848
|
-
["tools: default policy and admit hook refuse by name", async (store) => {
|
|
849
|
-
await rejects(() => toolsFor(store, alice)("memory.remember", {
|
|
850
|
-
text: "Never discount",
|
|
851
|
-
kind: "instruction",
|
|
852
|
-
scope: "workspace"
|
|
853
|
-
}), WorkspacePolicyError, "member instruction");
|
|
854
|
-
await rejects(() => toolsFor(store, alice)("memory.remember", {
|
|
855
|
-
text: "Use metric",
|
|
856
|
-
kind: "preference",
|
|
857
|
-
scope: "workspace"
|
|
858
|
-
}), WorkspacePolicyError, "member workspace preference");
|
|
859
|
-
await rejects(() => toolsFor(store, guest)("memory.remember", {
|
|
860
|
-
text: "I am Gus",
|
|
861
|
-
kind: "fact",
|
|
862
|
-
scope: "me"
|
|
863
|
-
}), WorkspacePolicyError, "guest write");
|
|
864
|
-
await rejects(() => toolsFor(store, alice)("files.write", {
|
|
865
|
-
path: "/knowledge/menu.md",
|
|
866
|
-
content: "x",
|
|
867
|
-
ifRevision: null
|
|
868
|
-
}), WorkspacePolicyError, "member knowledge write");
|
|
869
|
-
check((await toolsFor(store, owner)("memory.remember", {
|
|
870
|
-
text: "Never offer discounts",
|
|
871
|
-
kind: "instruction",
|
|
872
|
-
scope: "workspace"
|
|
873
|
-
})).saved, "owner saves an instruction");
|
|
874
|
-
const noPhones = defaultMemoryPolicy({ admit: (_turn, { text }) => /\d{3}-\d{4}/.test(text) ? "Phone numbers are not saved" : null });
|
|
875
|
-
check((await rejects(() => toolsFor(store, alice, noPhones)("memory.remember", {
|
|
876
|
-
text: "Call 555-0100",
|
|
877
|
-
kind: "fact",
|
|
878
|
-
scope: "me"
|
|
879
|
-
}), WorkspacePolicyError, "admit hook")).message === "Phone numbers are not saved", "refusal carries the product reason");
|
|
880
|
-
check((await store.list(W, "/memories/members/alice/")).length === 0, "refused writes store nothing");
|
|
881
|
-
}],
|
|
882
|
-
["tools: files write, read, CAS conflict, search, delete", async (store) => {
|
|
883
|
-
const call = toolsFor(store, alice);
|
|
884
|
-
const created = await call("files.write", {
|
|
885
|
-
path: "/files/notes/menu.md",
|
|
886
|
-
content: "Tuesday special: lamb",
|
|
887
|
-
ifRevision: null
|
|
888
|
-
});
|
|
889
|
-
await rejects(() => call("files.write", {
|
|
890
|
-
path: "/files/notes/menu.md",
|
|
891
|
-
content: "x",
|
|
892
|
-
ifRevision: null
|
|
893
|
-
}), WorkspaceConflictError, "files.write create over a live file");
|
|
894
|
-
const read = await toolsFor(store, bob)("files.read", { path: "/files/notes/menu.md" });
|
|
895
|
-
check(read.content === "Tuesday special: lamb" && read.revision === created.revision, "files.read returns content and revision");
|
|
896
|
-
const hits = await call("files.search", { query: "lamb special" });
|
|
897
|
-
check(hits.files[0]?.path === "/files/notes/menu.md" && hits.files[0].snippet.includes("lamb"), "files.search finds text");
|
|
898
|
-
check((await call("files.list", {})).files.length === 1, "files.list lists it");
|
|
899
|
-
await call("files.delete", {
|
|
900
|
-
path: "/files/notes/menu.md",
|
|
901
|
-
ifRevision: created.revision
|
|
902
|
-
});
|
|
903
|
-
await rejects(() => call("files.read", { path: "/files/notes/menu.md" }), Error, "read after delete");
|
|
904
|
-
}],
|
|
905
|
-
["renderPinnedMemory: stable, ordered, budgeted, ends with the recall index", async (store) => {
|
|
906
|
-
await toolsFor(store, alice)("memory.remember", {
|
|
907
|
-
text: "Alice works weekends",
|
|
908
|
-
kind: "fact",
|
|
909
|
-
scope: "me"
|
|
910
|
-
});
|
|
911
|
-
await toolsFor(store, owner)("memory.remember", {
|
|
912
|
-
text: "Always greet by name",
|
|
913
|
-
kind: "instruction",
|
|
914
|
-
scope: "workspace"
|
|
915
|
-
});
|
|
916
|
-
for (let i = 0; i < 40; i += 1) await toolsFor(store, alice)("memory.remember", {
|
|
917
|
-
text: `Supplier ${i} delivers on day ${i % 7}`,
|
|
918
|
-
kind: "fact",
|
|
919
|
-
scope: "workspace"
|
|
920
|
-
});
|
|
921
|
-
const entries = await readTurnMemories(store, alice);
|
|
922
|
-
const block = renderPinnedMemory(entries);
|
|
923
|
-
check(block === renderPinnedMemory([...entries].reverse()), "input order does not change the output");
|
|
924
|
-
check(block === renderPinnedMemory(await readTurnMemories(store, alice)), "identical input renders identically");
|
|
925
|
-
const lines = block.split("\n");
|
|
926
|
-
check(lines.findIndex((l) => l.includes("Always greet")) < lines.findIndex((l) => l.includes("Supplier")), "instructions first");
|
|
927
|
-
check(lines.at(-1) === "</saved-memory>" && lines.at(-2)?.startsWith("Recall more:"), "ends with the recall index");
|
|
928
|
-
check(entries.every((e) => block.includes(`[${e.id}]`)), "every memory is shown with its id under the default budget");
|
|
929
|
-
const small = renderPinnedMemory(entries, { maxTokens: 120 });
|
|
930
|
-
check(small.length <= 480 && /\d+ more not shown\./.test(small), "a small budget truncates and counts");
|
|
931
|
-
}]
|
|
932
|
+
compareAndSetCase,
|
|
933
|
+
entriesCase,
|
|
934
|
+
deleteTombstoneCase,
|
|
935
|
+
rememberRecallCase,
|
|
936
|
+
memberScopeCase,
|
|
937
|
+
policyCase,
|
|
938
|
+
fileToolsCase,
|
|
939
|
+
pinnedMemoryCase
|
|
932
940
|
];
|
|
933
941
|
if (options.idempotentOperations) cases.push(idempotentOperationCase);
|
|
934
942
|
return cases.map(([name, body]) => ({
|