@mastra/convex 1.7.2-alpha.0 → 1.7.3-alpha.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.
@@ -550,6 +550,153 @@ function mergeReflectionWithUnreflected(activeObservations, bufferedReflection,
550
550
  const unreflectedContent = (activeObservations || "").split("\n").slice(reflectedLineCount).join("\n").trim();
551
551
  return unreflectedContent ? `${bufferedReflection}\n\n${unreflectedContent}` : bufferedReflection;
552
552
  }
553
+ /**
554
+ * A buffered chunk stores `lastObservedAt = max message time + 1ms`; it is wholly covered by
555
+ * the cursor iff `cursor >= chunk.lastObservedAt - 1ms`.
556
+ */
557
+ function isChunkCoveredByCursor(chunkLastObservedAt, cursor) {
558
+ if (!cursor) return false;
559
+ return Date.parse(cursor) >= Date.parse(chunkLastObservedAt) - 1;
560
+ }
561
+ /** The later of two ISO cursors (`null` only when both are absent). */
562
+ function maxCursor(a, b) {
563
+ if (!a) return b ?? null;
564
+ if (!b) return a;
565
+ return Date.parse(b) > Date.parse(a) ? b : a;
566
+ }
567
+ /**
568
+ * New-generation text for a reflection built from `snapshot`: the reflection when the stored
569
+ * text equals the snapshot; the reflection plus the appended tail when the stored text only
570
+ * extends it; `null` (do not apply) after a non-append rewrite.
571
+ */
572
+ function planReflectionText(input) {
573
+ const stored = input.storedObservations ?? "";
574
+ const snapshot = input.snapshotObservations ?? "";
575
+ if (stored === snapshot) return {
576
+ observations: input.reflection,
577
+ tokenCount: input.tokenCount
578
+ };
579
+ if (!stored.startsWith(snapshot)) return null;
580
+ const tail = stored.slice(snapshot.length).trimStart();
581
+ if (!tail.trim()) return {
582
+ observations: input.reflection,
583
+ tokenCount: input.tokenCount
584
+ };
585
+ return {
586
+ observations: input.reflection ? `${input.reflection}\n\n${tail}` : tail,
587
+ tokenCount: input.tokenCount + Math.max(0, (input.storedObservationTokenCount ?? 0) - (input.snapshotObservationTokenCount ?? 0))
588
+ };
589
+ }
590
+ /** Whether the stored text equals or only extends the snapshot. */
591
+ function isAppendOnly(storedObservations, snapshotObservations) {
592
+ const stored = storedObservations ?? "";
593
+ const snapshot = snapshotObservations ?? "";
594
+ return stored === snapshot || stored.startsWith(snapshot);
595
+ }
596
+ /** Canonical head order: generationCount DESC, createdAt ASC, id ASC. */
597
+ function sortsBeforeAsHead(a, b) {
598
+ if (a.generationCount !== b.generationCount) return a.generationCount > b.generationCount;
599
+ if (a.createdAt !== b.createdAt) return Date.parse(a.createdAt) < Date.parse(b.createdAt);
600
+ return a.id < b.id;
601
+ }
602
+ /**
603
+ * The canonical head of a lookup key. `by_lookup_key` orders ties by `_creationTime`, so when
604
+ * the top two share a generation all tied rows are read and the canonical order picks one.
605
+ */
606
+ async function getCanonicalHead(ctx, convexTable, lookupKey) {
607
+ const top = await ctx.db.query(convexTable).withIndex("by_lookup_key", (q) => q.eq("lookupKey", lookupKey)).order("desc").take(2);
608
+ if (top.length === 0) return null;
609
+ if (top.length === 1 || top[1].generationCount !== top[0].generationCount) return top[0];
610
+ return (await ctx.db.query(convexTable).withIndex("by_lookup_key", (q) => q.eq("lookupKey", lookupKey).eq("generationCount", top[0].generationCount)).collect()).reduce((best, doc) => sortsBeforeAsHead(doc, best) ? doc : best);
611
+ }
612
+ /**
613
+ * Liveness of the record a lifecycle write names. A record with `supersededBy` set is retired.
614
+ * A record that is unmarked but is not its key's canonical head (left by an older adapter
615
+ * version, which never set the marker) is marked superseded by the head here and treated as
616
+ * retired, so a write never lands on it.
617
+ */
618
+ async function resolveTarget(ctx, convexTable, id, now) {
619
+ const doc = await findRecordById(ctx, convexTable, id);
620
+ if (!doc) return { kind: "missing" };
621
+ const head = await getCanonicalHead(ctx, convexTable, doc.lookupKey);
622
+ if (doc.supersededBy) return {
623
+ kind: "retired",
624
+ doc,
625
+ head
626
+ };
627
+ if (head && head.id !== doc.id) {
628
+ await ctx.db.patch(doc._id, {
629
+ supersededBy: head.id,
630
+ updatedAt: now
631
+ });
632
+ return {
633
+ kind: "retired",
634
+ doc,
635
+ head
636
+ };
637
+ }
638
+ return {
639
+ kind: "live",
640
+ doc
641
+ };
642
+ }
643
+ /** The live record a write aimed at `id` lands on (the head when `id` is retired). Throws when missing. */
644
+ async function resolveWriteTarget(ctx, convexTable, id, now) {
645
+ const target = await resolveTarget(ctx, convexTable, id, now);
646
+ if (target.kind === "missing") return requireRecord(null, id);
647
+ if (target.kind === "live") return target.doc;
648
+ if (!target.head) throw new Error(`Observational memory record ${id} is superseded but no live head was found`);
649
+ return target.head;
650
+ }
651
+ /**
652
+ * Create the next generation from the stored (live) record and retire it in the same mutation.
653
+ * Buffered chunks move to the new generation; the cursor, buffering markers, flags, and
654
+ * counters carry over; buffered reflection state does not.
655
+ */
656
+ async function rollOver(ctx, convexTable, stored, args) {
657
+ const newRecord = {
658
+ id: args.newId,
659
+ lookupKey: stored.lookupKey,
660
+ scope: stored.scope,
661
+ resourceId: stored.resourceId ?? null,
662
+ threadId: stored.threadId ?? null,
663
+ activeObservations: args.observations,
664
+ activeObservationsPendingUpdate: null,
665
+ originType: "reflection",
666
+ config: stored.config,
667
+ generationCount: Number(stored.generationCount || 0) + 1,
668
+ lastObservedAt: stored.lastObservedAt ?? null,
669
+ lastReflectionAt: args.now,
670
+ pendingMessageTokens: Number(stored.pendingMessageTokens || 0),
671
+ totalTokensObserved: Number(stored.totalTokensObserved || 0),
672
+ observationTokenCount: args.tokenCount,
673
+ isObserving: false,
674
+ isReflecting: false,
675
+ bufferedObservationChunks: parseStoredChunks(stored.bufferedObservationChunks).length ? stored.bufferedObservationChunks : null,
676
+ isBufferingObservation: Boolean(stored.isBufferingObservation),
677
+ isBufferingReflection: false,
678
+ lastBufferedAtTokens: Number(stored.lastBufferedAtTokens || 0),
679
+ lastBufferedAtTime: stored.lastBufferedAtTime ?? null,
680
+ observedTimezone: stored.observedTimezone ?? null,
681
+ metadata: stored.metadata ?? null,
682
+ supersededBy: null,
683
+ createdAt: args.now,
684
+ updatedAt: args.now
685
+ };
686
+ await ctx.db.insert(convexTable, newRecord);
687
+ await ctx.db.patch(stored._id, {
688
+ supersededBy: args.newId,
689
+ bufferedObservationChunks: null,
690
+ ...args.clearBufferedReflection ? {
691
+ bufferedReflection: null,
692
+ bufferedReflectionTokens: null,
693
+ bufferedReflectionInputTokens: null,
694
+ reflectedObservationLineCount: null
695
+ } : {},
696
+ updatedAt: args.now
697
+ });
698
+ return newRecord;
699
+ }
553
700
  function isPlainObj(value) {
554
701
  return typeof value === "object" && value !== null && !Array.isArray(value);
555
702
  }
@@ -595,8 +742,78 @@ async function handleObservationalMemoryOperation(ctx, convexTable, request) {
595
742
  switch (request.op) {
596
743
  case "omGetLatest": return {
597
744
  ok: true,
598
- result: await ctx.db.query(convexTable).withIndex("by_lookup_key", (q) => q.eq("lookupKey", request.lookupKey)).order("desc").first() ?? null
745
+ result: await getCanonicalHead(ctx, convexTable, request.lookupKey)
599
746
  };
747
+ case "omInitialize": {
748
+ const existing = await getCanonicalHead(ctx, convexTable, request.record.lookupKey);
749
+ if (existing) return {
750
+ ok: true,
751
+ result: existing
752
+ };
753
+ await ctx.db.insert(convexTable, {
754
+ ...request.record,
755
+ supersededBy: null
756
+ });
757
+ return {
758
+ ok: true,
759
+ result: {
760
+ ...request.record,
761
+ supersededBy: null
762
+ }
763
+ };
764
+ }
765
+ case "omCreateReflectionGeneration": {
766
+ const { currentRecord, newId, reflection, tokenCount, now } = request;
767
+ const target = await resolveTarget(ctx, convexTable, currentRecord.id, now);
768
+ if (target.kind === "missing") return {
769
+ ok: true,
770
+ result: null
771
+ };
772
+ if (target.kind === "retired") return {
773
+ ok: true,
774
+ result: target.head
775
+ };
776
+ const stored = target.doc;
777
+ const plan = planReflectionText({
778
+ storedObservations: stored.activeObservations || "",
779
+ storedObservationTokenCount: Number(stored.observationTokenCount || 0),
780
+ snapshotObservations: currentRecord.activeObservations ?? "",
781
+ snapshotObservationTokenCount: currentRecord.observationTokenCount ?? 0,
782
+ reflection,
783
+ tokenCount
784
+ });
785
+ if (!plan) return {
786
+ ok: true,
787
+ result: stored
788
+ };
789
+ return {
790
+ ok: true,
791
+ result: await rollOver(ctx, convexTable, stored, {
792
+ newId,
793
+ observations: plan.observations,
794
+ tokenCount: plan.tokenCount,
795
+ now,
796
+ clearBufferedReflection: false
797
+ })
798
+ };
799
+ }
800
+ case "omSetPendingMessageTokens": {
801
+ const doc = await resolveWriteTarget(ctx, convexTable, request.id, request.updatedAt);
802
+ await ctx.db.patch(doc._id, {
803
+ pendingMessageTokens: request.tokenCount,
804
+ updatedAt: request.updatedAt
805
+ });
806
+ return { ok: true };
807
+ }
808
+ case "omSetBufferingObservationFlag": {
809
+ const doc = await resolveWriteTarget(ctx, convexTable, request.id, request.updatedAt);
810
+ await ctx.db.patch(doc._id, {
811
+ isBufferingObservation: request.isBuffering,
812
+ ...request.lastBufferedAtTokens !== void 0 ? { lastBufferedAtTokens: request.lastBufferedAtTokens } : {},
813
+ updatedAt: request.updatedAt
814
+ });
815
+ return { ok: true };
816
+ }
600
817
  case "omGetHistory": {
601
818
  let docs = request.recordId !== void 0 ? [await findRecordById(ctx, convexTable, request.recordId)].filter((doc) => doc?.lookupKey === request.lookupKey) : await ctx.db.query(convexTable).withIndex("by_lookup_key", (q) => {
602
819
  let range = q.eq("lookupKey", request.lookupKey);
@@ -621,39 +838,82 @@ async function handleObservationalMemoryOperation(ctx, convexTable, request) {
621
838
  };
622
839
  }
623
840
  case "omUpdateActive": {
624
- const doc = requireRecord(await findRecordById(ctx, convexTable, request.id), request.id);
841
+ const target = await resolveTarget(ctx, convexTable, request.id, request.updatedAt);
842
+ if (target.kind === "missing") requireRecord(null, request.id);
843
+ if (target.kind !== "live") return {
844
+ ok: true,
845
+ result: {
846
+ applied: false,
847
+ reason: "retired"
848
+ }
849
+ };
850
+ const doc = target.doc;
851
+ if (request.expectedActiveObservations !== void 0 && request.expectedActiveObservations !== (doc.activeObservations || "")) return {
852
+ ok: true,
853
+ result: {
854
+ applied: false,
855
+ reason: "conflict"
856
+ }
857
+ };
625
858
  const safeTokenCount = Number.isFinite(request.tokenCount) && request.tokenCount >= 0 ? request.tokenCount : 0;
626
859
  await ctx.db.patch(doc._id, {
627
860
  activeObservations: request.observations,
628
- lastObservedAt: request.lastObservedAt,
861
+ lastObservedAt: maxCursor(doc.lastObservedAt, request.lastObservedAt),
629
862
  pendingMessageTokens: 0,
630
863
  observationTokenCount: safeTokenCount,
631
864
  totalTokensObserved: Number(doc.totalTokensObserved || 0) + safeTokenCount,
632
865
  observedMessageIds: request.observedMessageIds,
633
866
  updatedAt: request.updatedAt
634
867
  });
635
- return { ok: true };
868
+ return {
869
+ ok: true,
870
+ result: { applied: true }
871
+ };
636
872
  }
637
873
  case "omAppendBufferedChunk": {
638
- const doc = requireRecord(await findRecordById(ctx, convexTable, request.id), request.id);
874
+ const doc = await resolveWriteTarget(ctx, convexTable, request.id, request.updatedAt);
639
875
  const chunks = parseStoredChunks(doc.bufferedObservationChunks);
876
+ if (chunks.some((chunk) => chunk.cycleId === request.chunk.cycleId) || isChunkCoveredByCursor(request.chunk.lastObservedAt, doc.lastObservedAt)) return {
877
+ ok: true,
878
+ result: {
879
+ persisted: false,
880
+ recordId: doc.id
881
+ }
882
+ };
640
883
  chunks.push(request.chunk);
641
- const patch = {
884
+ await ctx.db.patch(doc._id, {
642
885
  bufferedObservationChunks: JSON.stringify(chunks),
886
+ lastBufferedAtTime: maxCursor(doc.lastBufferedAtTime, request.lastBufferedAtTime),
643
887
  updatedAt: request.updatedAt
888
+ });
889
+ return {
890
+ ok: true,
891
+ result: {
892
+ persisted: true,
893
+ recordId: doc.id
894
+ }
644
895
  };
645
- if (request.lastBufferedAtTime) patch.lastBufferedAtTime = request.lastBufferedAtTime;
646
- await ctx.db.patch(doc._id, patch);
647
- return { ok: true };
648
896
  }
649
897
  case "omSwapBuffered": {
650
- const doc = requireRecord(await findRecordById(ctx, convexTable, request.id), request.id);
651
- const persistedChunks = parseStoredChunks(doc.bufferedObservationChunks);
652
- if (persistedChunks.length === 0) return {
898
+ const target = await resolveTarget(ctx, convexTable, request.id, request.now);
899
+ if (target.kind === "missing") requireRecord(null, request.id);
900
+ if (target.kind !== "live") return {
653
901
  ok: true,
654
- result: EMPTY_SWAP_RESULT
902
+ result: {
903
+ ...EMPTY_SWAP_RESULT,
904
+ retired: true
905
+ }
655
906
  };
656
- const chunks = Array.isArray(request.bufferedChunks) ? request.bufferedChunks : persistedChunks;
907
+ const doc = target.doc;
908
+ const refreshedWeights = new Map((request.bufferedChunks ?? []).map((chunk) => [chunk.id, chunk.messageTokens]));
909
+ const storedChunks = parseStoredChunks(doc.bufferedObservationChunks);
910
+ const chunks = storedChunks.map((chunk) => {
911
+ const weight = refreshedWeights.get(chunk.id);
912
+ return weight === void 0 ? chunk : {
913
+ ...chunk,
914
+ messageTokens: weight
915
+ };
916
+ });
657
917
  if (chunks.length === 0) return {
658
918
  ok: true,
659
919
  result: EMPTY_SWAP_RESULT
@@ -665,7 +925,7 @@ async function handleObservationalMemoryOperation(ctx, convexTable, request) {
665
925
  forceMaxActivation: request.forceMaxActivation
666
926
  });
667
927
  const activatedChunks = chunks.slice(0, chunksToActivate);
668
- const remainingChunks = chunks.slice(chunksToActivate);
928
+ const remainingChunks = storedChunks.slice(chunksToActivate);
669
929
  const activatedContent = activatedChunks.map((c) => c.observations).join("\n\n");
670
930
  const activatedTokens = activatedChunks.reduce((sum, c) => sum + c.tokenCount, 0);
671
931
  const activatedMessageTokens = activatedChunks.reduce((sum, c) => sum + (c.messageTokens ?? 0), 0);
@@ -682,7 +942,7 @@ async function handleObservationalMemoryOperation(ctx, convexTable, request) {
682
942
  observationTokenCount: Number(doc.observationTokenCount || 0) + activatedTokens,
683
943
  pendingMessageTokens: Math.max(0, Number(doc.pendingMessageTokens || 0) - activatedMessageTokens),
684
944
  bufferedObservationChunks: remainingChunks.length > 0 ? JSON.stringify(remainingChunks) : null,
685
- lastObservedAt,
945
+ lastObservedAt: maxCursor(doc.lastObservedAt, lastObservedAt),
686
946
  updatedAt: request.now
687
947
  });
688
948
  const latestChunkHints = activatedChunks[activatedChunks.length - 1];
@@ -722,48 +982,32 @@ async function handleObservationalMemoryOperation(ctx, convexTable, request) {
722
982
  }
723
983
  case "omSwapBufferedReflection": {
724
984
  const { currentRecord, newId, tokenCount, now } = request;
725
- const doc = requireRecord(await findRecordById(ctx, convexTable, currentRecord.id), currentRecord.id);
726
- const bufferedReflection = doc.bufferedReflection || "";
985
+ const target = await resolveTarget(ctx, convexTable, currentRecord.id, now);
986
+ if (target.kind === "missing") return {
987
+ ok: true,
988
+ result: null
989
+ };
990
+ if (target.kind === "retired") return {
991
+ ok: true,
992
+ result: target.head
993
+ };
994
+ const stored = target.doc;
995
+ const bufferedReflection = stored.bufferedReflection || "";
727
996
  if (!bufferedReflection) throw new Error("No buffered reflection to swap");
728
- const newObservations = mergeReflectionWithUnreflected(doc.activeObservations || "", bufferedReflection, Number(doc.reflectedObservationLineCount || 0));
729
- const newRecord = {
730
- id: newId,
731
- lookupKey: currentRecord.lookupKey,
732
- scope: currentRecord.scope,
733
- resourceId: currentRecord.resourceId,
734
- threadId: currentRecord.threadId,
735
- activeObservations: newObservations,
736
- activeObservationsPendingUpdate: null,
737
- originType: "reflection",
738
- config: currentRecord.config,
739
- generationCount: currentRecord.generationCount + 1,
740
- lastObservedAt: currentRecord.lastObservedAt,
741
- lastReflectionAt: now,
742
- pendingMessageTokens: 0,
743
- totalTokensObserved: currentRecord.totalTokensObserved,
744
- observationTokenCount: tokenCount,
745
- isObserving: false,
746
- isReflecting: false,
747
- isBufferingObservation: false,
748
- isBufferingReflection: false,
749
- lastBufferedAtTokens: 0,
750
- lastBufferedAtTime: null,
751
- observedTimezone: currentRecord.observedTimezone,
752
- metadata: currentRecord.metadata,
753
- createdAt: now,
754
- updatedAt: now
755
- };
756
- await ctx.db.insert(convexTable, newRecord);
757
- await ctx.db.patch(doc._id, {
758
- bufferedReflection: null,
759
- bufferedReflectionTokens: null,
760
- bufferedReflectionInputTokens: null,
761
- reflectedObservationLineCount: null,
762
- updatedAt: now
763
- });
997
+ const storedObservations = stored.activeObservations || "";
998
+ if (!isAppendOnly(storedObservations, currentRecord.activeObservations ?? "")) return {
999
+ ok: true,
1000
+ result: stored
1001
+ };
764
1002
  return {
765
1003
  ok: true,
766
- result: newRecord
1004
+ result: await rollOver(ctx, convexTable, stored, {
1005
+ newId,
1006
+ observations: mergeReflectionWithUnreflected(storedObservations, bufferedReflection, Number(stored.reflectedObservationLineCount || 0)),
1007
+ tokenCount: tokenCount + Math.max(0, Number(stored.observationTokenCount || 0) - (currentRecord.observationTokenCount ?? 0)),
1008
+ now,
1009
+ clearBufferedReflection: true
1010
+ })
767
1011
  };
768
1012
  }
769
1013
  case "omUpdateConfig": {
@@ -1108,6 +1352,10 @@ async function handleTypedOperation(ctx, convexTable, request) {
1108
1352
  switch (request.op) {
1109
1353
  case "omGetLatest":
1110
1354
  case "omGetHistory":
1355
+ case "omInitialize":
1356
+ case "omCreateReflectionGeneration":
1357
+ case "omSetPendingMessageTokens":
1358
+ case "omSetBufferingObservationFlag":
1111
1359
  case "omUpdateActive":
1112
1360
  case "omAppendBufferedChunk":
1113
1361
  case "omSwapBuffered":
@@ -1895,4 +2143,4 @@ const mastraNativeVectorMutation = mutationGeneric({
1895
2143
  //#endregion
1896
2144
  export { mastraCache as a, mastraStorage as i, mastraNativeVectorMutation as n, mastraNativeVectorQuery as r, mastraNativeVectorAction as t };
1897
2145
 
1898
- //# sourceMappingURL=server-D3SG5LQf.js.map
2146
+ //# sourceMappingURL=server-DozOFGfO.js.map