@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.
Files changed (70) hide show
  1. package/dist/{activation-D2PVljaw.js → activation-CWPmt0Do.js} +4 -4
  2. package/dist/{activation-D2PVljaw.js.map → activation-CWPmt0Do.js.map} +1 -1
  3. package/dist/{activation-DGDl4wwI.d.ts → activation-CXqtijue.d.ts} +2 -2
  4. package/dist/agent.d.ts +1 -1
  5. package/dist/agent.js +2 -2
  6. package/dist/{bundle-DNUq-OPW.js → bundle-Cwpit6Tg.js} +2 -2
  7. package/dist/{bundle-DNUq-OPW.js.map → bundle-Cwpit6Tg.js.map} +1 -1
  8. package/dist/candidate-execution/index.js +4 -4
  9. package/dist/{driver-control-acknowledgers-MTrs8HAF.js → coordination-manager-B5JsolDR.js} +4852 -4539
  10. package/dist/coordination-manager-B5JsolDR.js.map +1 -0
  11. package/dist/{delegate-CtJIS-tp.js → delegate-Rl-5paLJ.js} +2 -2
  12. package/dist/{delegate-CtJIS-tp.js.map → delegate-Rl-5paLJ.js.map} +1 -1
  13. package/dist/durable.d.ts +1 -1
  14. package/dist/durable.js +451 -358
  15. package/dist/durable.js.map +1 -1
  16. package/dist/{graph-DfWqp_1e.js → graph-Ce-59ZXn.js} +5 -5
  17. package/dist/{graph-DfWqp_1e.js.map → graph-Ce-59ZXn.js.map} +1 -1
  18. package/dist/{improve-CKRVQqzt.js → improve-BZXt2qM0.js} +837 -704
  19. package/dist/improve-BZXt2qM0.js.map +1 -0
  20. package/dist/index.d.ts +3 -3
  21. package/dist/index.js +9 -9
  22. package/dist/intelligence.d.ts +2 -2
  23. package/dist/intelligence.js +205 -191
  24. package/dist/intelligence.js.map +1 -1
  25. package/dist/kernel.d.ts +1 -1
  26. package/dist/kernel.js +9 -9
  27. package/dist/{loop-runner-DNWbTcVm.js → loop-runner-B0MV1-CD.js} +2 -2
  28. package/dist/{loop-runner-DNWbTcVm.js.map → loop-runner-B0MV1-CD.js.map} +1 -1
  29. package/dist/{loop-runner-C4EWEcXf.d.ts → loop-runner-CuhbgVGo.d.ts} +2 -2
  30. package/dist/loop-runner-bin.d.ts +1 -1
  31. package/dist/loop-runner-bin.js +1 -1
  32. package/dist/mcp/bin.js +3 -3
  33. package/dist/mcp/index.d.ts +1 -1
  34. package/dist/mcp/index.js +5 -5
  35. package/dist/{prepare-FzJoS68l.js → prepare-C-cWCyt3.js} +988 -735
  36. package/dist/prepare-C-cWCyt3.js.map +1 -0
  37. package/dist/{profile-DToH0XEs.js → profile-CbMw5hAn.js} +2 -2
  38. package/dist/{profile-DToH0XEs.js.map → profile-CbMw5hAn.js.map} +1 -1
  39. package/dist/profiles.d.ts +1 -1
  40. package/dist/{provision-supervisor-DqvWXmAE.js → provision-supervisor-DL6gEMQp.js} +444 -419
  41. package/dist/provision-supervisor-DL6gEMQp.js.map +1 -0
  42. package/dist/{runtime-C55Zgo1R.js → runtime-eghmj6W4.js} +1245 -1028
  43. package/dist/runtime-eghmj6W4.js.map +1 -0
  44. package/dist/{sandbox-profile-compat-DRr3JwDx.js → sandbox-profile-compat-FN2kab2-.js} +72 -72
  45. package/dist/{sandbox-profile-compat-DRr3JwDx.js.map → sandbox-profile-compat-FN2kab2-.js.map} +1 -1
  46. package/dist/{search-executor-ChOlRBgg.js → search-executor-rbjdZ3OD.js} +3 -3
  47. package/dist/{search-executor-ChOlRBgg.js.map → search-executor-rbjdZ3OD.js.map} +1 -1
  48. package/dist/{server-DkVBSN_S.js → server-BiK1Kcno.js} +4 -4
  49. package/dist/{server-DkVBSN_S.js.map → server-BiK1Kcno.js.map} +1 -1
  50. package/dist/{structural-rollout-C1LEk_zq.js → structural-rollout-BD9IN7K_.js} +3 -3
  51. package/dist/{structural-rollout-C1LEk_zq.js.map → structural-rollout-BD9IN7K_.js.map} +1 -1
  52. package/dist/{supervise-CaUAxZZP.js → supervise-DmOi7gll.js} +6346 -5447
  53. package/dist/supervise-DmOi7gll.js.map +1 -0
  54. package/dist/{supervisor-VXuN2OHT.js → supervisor-DUWcecAd.js} +13822 -12826
  55. package/dist/supervisor-DUWcecAd.js.map +1 -0
  56. package/dist/testing.d.ts +1 -1
  57. package/dist/testing.js +13 -13
  58. package/dist/tui/index.d.ts +1 -1
  59. package/dist/tui/index.js +1 -1
  60. package/dist/{types-DZ6L0Bv3.d.ts → types-Cgs89PQd.d.ts} +266 -356
  61. package/dist/workspace-store/index.js +273 -265
  62. package/dist/workspace-store/index.js.map +1 -1
  63. package/package.json +8 -8
  64. package/dist/driver-control-acknowledgers-MTrs8HAF.js.map +0 -1
  65. package/dist/improve-CKRVQqzt.js.map +0 -1
  66. package/dist/prepare-FzJoS68l.js.map +0 -1
  67. package/dist/provision-supervisor-DqvWXmAE.js.map +0 -1
  68. package/dist/runtime-C55Zgo1R.js.map +0 -1
  69. package/dist/supervise-CaUAxZZP.js.map +0 -1
  70. 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
- ["compare-and-set: create, stale update, and re-create conflict with a typed error", async (store) => {
668
- const base = {
669
- workspaceId: W,
670
- path: "/files/a.md",
671
- kind: "file",
672
- actor: "alice"
673
- };
674
- const first = await store.put({
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]) => ({