@agenr/openclaw-plugin 1.2.0 → 1.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.
- package/dist/index.js +3093 -1033
- package/openclaw.plugin.json +1 -1
- package/package.json +7 -7
- package/LICENSE +0 -661
package/dist/index.js
CHANGED
|
@@ -635,6 +635,793 @@ function elapsedMs(startedAt) {
|
|
|
635
635
|
return Math.max(0, Date.now() - startedAt);
|
|
636
636
|
}
|
|
637
637
|
|
|
638
|
+
// ../../src/core/episode/temporal-window.ts
|
|
639
|
+
var DAY_IN_MILLISECONDS3 = 24 * 60 * 60 * 1e3;
|
|
640
|
+
var DEFAULT_ANCHOR_RADIUS_DAYS = 3;
|
|
641
|
+
var MONTH_INDEX2 = /* @__PURE__ */ new Map([
|
|
642
|
+
["january", 0],
|
|
643
|
+
["february", 1],
|
|
644
|
+
["march", 2],
|
|
645
|
+
["april", 3],
|
|
646
|
+
["may", 4],
|
|
647
|
+
["june", 5],
|
|
648
|
+
["july", 6],
|
|
649
|
+
["august", 7],
|
|
650
|
+
["september", 8],
|
|
651
|
+
["october", 9],
|
|
652
|
+
["november", 10],
|
|
653
|
+
["december", 11]
|
|
654
|
+
]);
|
|
655
|
+
var WEEKDAY_INDEX = /* @__PURE__ */ new Map([
|
|
656
|
+
["sunday", 0],
|
|
657
|
+
["monday", 1],
|
|
658
|
+
["tuesday", 2],
|
|
659
|
+
["wednesday", 3],
|
|
660
|
+
["thursday", 4],
|
|
661
|
+
["friday", 5],
|
|
662
|
+
["saturday", 6]
|
|
663
|
+
]);
|
|
664
|
+
function parseTemporalWindow(text, now = /* @__PURE__ */ new Date()) {
|
|
665
|
+
const normalizedText = text.trim();
|
|
666
|
+
const referenceNow = asValidDate3(now);
|
|
667
|
+
if (normalizedText.length === 0 || !referenceNow) {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
const timezone = getSystemTimeZone();
|
|
671
|
+
const lower = normalizedText.toLowerCase();
|
|
672
|
+
if (/\btoday\b/.test(lower)) {
|
|
673
|
+
return buildResolvedWindow({
|
|
674
|
+
window: {
|
|
675
|
+
kind: "interval",
|
|
676
|
+
start: startOfDayLocal(referenceNow),
|
|
677
|
+
end: referenceNow,
|
|
678
|
+
source: "inferred"
|
|
679
|
+
},
|
|
680
|
+
resolvedFrom: "today",
|
|
681
|
+
timezone,
|
|
682
|
+
now: referenceNow
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (/\byesterday\b/.test(lower)) {
|
|
686
|
+
const target = addDaysLocal(referenceNow, -1);
|
|
687
|
+
return buildResolvedWindow({
|
|
688
|
+
window: {
|
|
689
|
+
kind: "interval",
|
|
690
|
+
start: startOfDayLocal(target),
|
|
691
|
+
end: endOfDayLocal(target),
|
|
692
|
+
source: "inferred"
|
|
693
|
+
},
|
|
694
|
+
resolvedFrom: "yesterday",
|
|
695
|
+
timezone,
|
|
696
|
+
now: referenceNow
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
const monthDayMatch = normalizedText.match(
|
|
700
|
+
/\b(?:on\s+)?((january|february|march|april|may|june|july|august|september|october|november|december)\s+(\d{1,2})(?:st|nd|rd|th)?)\b/i
|
|
701
|
+
);
|
|
702
|
+
if (monthDayMatch?.[1] && monthDayMatch[2] && monthDayMatch[3]) {
|
|
703
|
+
const targetDate = resolveMostRecentMonthDay(monthDayMatch[2].toLowerCase(), Number(monthDayMatch[3]), referenceNow);
|
|
704
|
+
if (targetDate) {
|
|
705
|
+
return buildResolvedWindow({
|
|
706
|
+
window: {
|
|
707
|
+
kind: "interval",
|
|
708
|
+
start: startOfDayLocal(targetDate),
|
|
709
|
+
end: endOfDayLocal(targetDate),
|
|
710
|
+
source: "inferred"
|
|
711
|
+
},
|
|
712
|
+
resolvedFrom: monthDayMatch[1],
|
|
713
|
+
timezone,
|
|
714
|
+
now: referenceNow
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const weekdayMatch = normalizedText.match(/\b(last\s+(monday|tuesday|wednesday|thursday|friday|saturday|sunday))\b/i);
|
|
719
|
+
if (weekdayMatch?.[1] && weekdayMatch[2]) {
|
|
720
|
+
const targetDate = resolveLastWeekday(weekdayMatch[2].toLowerCase(), referenceNow);
|
|
721
|
+
if (targetDate) {
|
|
722
|
+
return buildResolvedWindow({
|
|
723
|
+
window: {
|
|
724
|
+
kind: "interval",
|
|
725
|
+
start: startOfDayLocal(targetDate),
|
|
726
|
+
end: endOfDayLocal(targetDate),
|
|
727
|
+
source: "inferred"
|
|
728
|
+
},
|
|
729
|
+
resolvedFrom: weekdayMatch[1],
|
|
730
|
+
timezone,
|
|
731
|
+
now: referenceNow
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (/\bthis week\b/.test(lower)) {
|
|
736
|
+
return buildResolvedWindow({
|
|
737
|
+
window: {
|
|
738
|
+
kind: "interval",
|
|
739
|
+
start: startOfWeekLocal(referenceNow),
|
|
740
|
+
end: referenceNow,
|
|
741
|
+
source: "inferred"
|
|
742
|
+
},
|
|
743
|
+
resolvedFrom: "this week",
|
|
744
|
+
timezone,
|
|
745
|
+
now: referenceNow
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
if (/\blast week\b/.test(lower)) {
|
|
749
|
+
const previousWeekDate = addDaysLocal(startOfWeekLocal(referenceNow), -1);
|
|
750
|
+
const start = startOfWeekLocal(previousWeekDate);
|
|
751
|
+
return buildResolvedWindow({
|
|
752
|
+
window: {
|
|
753
|
+
kind: "interval",
|
|
754
|
+
start,
|
|
755
|
+
end: endOfWeekLocal(previousWeekDate),
|
|
756
|
+
source: "inferred"
|
|
757
|
+
},
|
|
758
|
+
resolvedFrom: "last week",
|
|
759
|
+
timezone,
|
|
760
|
+
now: referenceNow
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
if (/\bthis month\b/.test(lower)) {
|
|
764
|
+
return buildResolvedWindow({
|
|
765
|
+
window: {
|
|
766
|
+
kind: "interval",
|
|
767
|
+
start: startOfMonthLocal(referenceNow),
|
|
768
|
+
end: referenceNow,
|
|
769
|
+
source: "inferred"
|
|
770
|
+
},
|
|
771
|
+
resolvedFrom: "this month",
|
|
772
|
+
timezone,
|
|
773
|
+
now: referenceNow
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
if (/\blast month\b/.test(lower)) {
|
|
777
|
+
const previousMonthDate = new Date(referenceNow.getFullYear(), referenceNow.getMonth() - 1, 1, 12);
|
|
778
|
+
return buildResolvedWindow({
|
|
779
|
+
window: {
|
|
780
|
+
kind: "interval",
|
|
781
|
+
start: startOfMonthLocal(previousMonthDate),
|
|
782
|
+
end: endOfMonthLocal(previousMonthDate),
|
|
783
|
+
source: "inferred"
|
|
784
|
+
},
|
|
785
|
+
resolvedFrom: "last month",
|
|
786
|
+
timezone,
|
|
787
|
+
now: referenceNow
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
const relativeMatch = lower.match(/\b(\d+)\s+(day|days|week|weeks|month|months)\s+ago\b/);
|
|
791
|
+
if (relativeMatch?.[1] && relativeMatch[2]) {
|
|
792
|
+
const amount = Number(relativeMatch[1]);
|
|
793
|
+
if (Number.isFinite(amount) && amount > 0) {
|
|
794
|
+
const unit = relativeMatch[2];
|
|
795
|
+
if (unit.startsWith("day")) {
|
|
796
|
+
const target = addDaysLocal(referenceNow, -amount);
|
|
797
|
+
return buildResolvedWindow({
|
|
798
|
+
window: {
|
|
799
|
+
kind: "interval",
|
|
800
|
+
start: startOfDayLocal(target),
|
|
801
|
+
end: endOfDayLocal(target),
|
|
802
|
+
source: "inferred"
|
|
803
|
+
},
|
|
804
|
+
resolvedFrom: relativeMatch[0],
|
|
805
|
+
timezone,
|
|
806
|
+
now: referenceNow
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
if (unit.startsWith("week")) {
|
|
810
|
+
return buildResolvedWindow({
|
|
811
|
+
window: {
|
|
812
|
+
kind: "anchor",
|
|
813
|
+
anchor: addDaysLocal(referenceNow, -amount * 7),
|
|
814
|
+
radiusDays: DEFAULT_ANCHOR_RADIUS_DAYS,
|
|
815
|
+
source: "inferred"
|
|
816
|
+
},
|
|
817
|
+
resolvedFrom: relativeMatch[0],
|
|
818
|
+
timezone,
|
|
819
|
+
now: referenceNow
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
if (unit.startsWith("month")) {
|
|
823
|
+
return buildResolvedWindow({
|
|
824
|
+
window: {
|
|
825
|
+
kind: "anchor",
|
|
826
|
+
anchor: subtractCalendarMonths(referenceNow, amount),
|
|
827
|
+
radiusDays: DEFAULT_ANCHOR_RADIUS_DAYS,
|
|
828
|
+
source: "inferred"
|
|
829
|
+
},
|
|
830
|
+
resolvedFrom: relativeMatch[0],
|
|
831
|
+
timezone,
|
|
832
|
+
now: referenceNow
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const monthMatch = lower.match(/\bin\s+(january|february|march|april|may|june|july|august|september|october|november|december)\b/);
|
|
838
|
+
if (monthMatch?.[1]) {
|
|
839
|
+
const targetMonth = resolveMostRecentMonth(monthMatch[1], referenceNow);
|
|
840
|
+
if (targetMonth) {
|
|
841
|
+
return buildResolvedWindow({
|
|
842
|
+
window: {
|
|
843
|
+
kind: "interval",
|
|
844
|
+
start: startOfMonthLocal(targetMonth),
|
|
845
|
+
end: endOfMonthLocal(targetMonth),
|
|
846
|
+
source: "inferred"
|
|
847
|
+
},
|
|
848
|
+
resolvedFrom: monthMatch[0],
|
|
849
|
+
timezone,
|
|
850
|
+
now: referenceNow
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
const isoDateMatch = normalizedText.match(/\b(\d{4}-\d{2}-\d{2})(?:[tT][0-9:.+-Zz]+)?\b/);
|
|
855
|
+
if (isoDateMatch?.[1]) {
|
|
856
|
+
const targetDate = parseIsoDateLocal(isoDateMatch[1]);
|
|
857
|
+
if (targetDate) {
|
|
858
|
+
return buildResolvedWindow({
|
|
859
|
+
window: {
|
|
860
|
+
kind: "interval",
|
|
861
|
+
start: startOfDayLocal(targetDate),
|
|
862
|
+
end: endOfDayLocal(targetDate),
|
|
863
|
+
source: "inferred"
|
|
864
|
+
},
|
|
865
|
+
resolvedFrom: isoDateMatch[1],
|
|
866
|
+
timezone,
|
|
867
|
+
now: referenceNow
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
function resolveTemporalWindowBounds(window, now = /* @__PURE__ */ new Date()) {
|
|
874
|
+
switch (window.kind) {
|
|
875
|
+
case "interval":
|
|
876
|
+
return window.start && window.end ? { start: window.start, end: window.end } : null;
|
|
877
|
+
case "anchor":
|
|
878
|
+
if (!window.anchor || window.radiusDays === void 0 || window.radiusDays < 0) {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
return {
|
|
882
|
+
start: new Date(window.anchor.getTime() - Math.trunc(window.radiusDays) * DAY_IN_MILLISECONDS3),
|
|
883
|
+
end: new Date(window.anchor.getTime() + Math.trunc(window.radiusDays) * DAY_IN_MILLISECONDS3)
|
|
884
|
+
};
|
|
885
|
+
case "open_end":
|
|
886
|
+
return window.start ? { start: window.start, end: asValidDate3(now) ?? /* @__PURE__ */ new Date() } : null;
|
|
887
|
+
case "open_start":
|
|
888
|
+
return null;
|
|
889
|
+
default:
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
function getSystemTimeZone() {
|
|
894
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
895
|
+
}
|
|
896
|
+
function buildResolvedWindow(params) {
|
|
897
|
+
const bounds = resolveTemporalWindowBounds(params.window, params.now);
|
|
898
|
+
if (!bounds) {
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
window: params.window,
|
|
903
|
+
bounds,
|
|
904
|
+
timezone: params.timezone,
|
|
905
|
+
resolvedFrom: params.resolvedFrom
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
function asValidDate3(value) {
|
|
909
|
+
const normalized = new Date(value.getTime());
|
|
910
|
+
return Number.isNaN(normalized.getTime()) ? null : normalized;
|
|
911
|
+
}
|
|
912
|
+
function addDaysLocal(date, days) {
|
|
913
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days, date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
|
|
914
|
+
}
|
|
915
|
+
function startOfDayLocal(date) {
|
|
916
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0);
|
|
917
|
+
}
|
|
918
|
+
function endOfDayLocal(date) {
|
|
919
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
|
|
920
|
+
}
|
|
921
|
+
function startOfWeekLocal(date) {
|
|
922
|
+
const weekStart = resolveWeekStartDay();
|
|
923
|
+
const currentDay = date.getDay();
|
|
924
|
+
const offset = (currentDay - weekStart + 7) % 7;
|
|
925
|
+
return startOfDayLocal(addDaysLocal(date, -offset));
|
|
926
|
+
}
|
|
927
|
+
function endOfWeekLocal(date) {
|
|
928
|
+
return endOfDayLocal(addDaysLocal(startOfWeekLocal(date), 6));
|
|
929
|
+
}
|
|
930
|
+
function startOfMonthLocal(date) {
|
|
931
|
+
return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0);
|
|
932
|
+
}
|
|
933
|
+
function endOfMonthLocal(date) {
|
|
934
|
+
return new Date(date.getFullYear(), date.getMonth() + 1, 0, 23, 59, 59, 999);
|
|
935
|
+
}
|
|
936
|
+
function resolveMostRecentMonth(monthName, now) {
|
|
937
|
+
const monthIndex = MONTH_INDEX2.get(monthName);
|
|
938
|
+
if (monthIndex === void 0) {
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
const year = monthIndex <= now.getMonth() ? now.getFullYear() : now.getFullYear() - 1;
|
|
942
|
+
return new Date(year, monthIndex, 15, 12, 0, 0, 0);
|
|
943
|
+
}
|
|
944
|
+
function resolveMostRecentMonthDay(monthName, day, now) {
|
|
945
|
+
const monthIndex = MONTH_INDEX2.get(monthName);
|
|
946
|
+
if (monthIndex === void 0) {
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
const currentYearCandidate = buildLocalDateAtNoon(now.getFullYear(), monthIndex, day);
|
|
950
|
+
if (!currentYearCandidate) {
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
if (startOfDayLocal(currentYearCandidate).getTime() <= startOfDayLocal(now).getTime()) {
|
|
954
|
+
return currentYearCandidate;
|
|
955
|
+
}
|
|
956
|
+
return buildLocalDateAtNoon(now.getFullYear() - 1, monthIndex, day);
|
|
957
|
+
}
|
|
958
|
+
function parseIsoDateLocal(value) {
|
|
959
|
+
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
960
|
+
if (!match?.[1] || !match[2] || !match[3]) {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
const year = Number(match[1]);
|
|
964
|
+
const month = Number(match[2]) - 1;
|
|
965
|
+
const day = Number(match[3]);
|
|
966
|
+
return buildLocalDateAtNoon(year, month, day);
|
|
967
|
+
}
|
|
968
|
+
function subtractCalendarMonths(date, months) {
|
|
969
|
+
const targetMonthIndex = date.getMonth() - months;
|
|
970
|
+
const targetYear = date.getFullYear() + Math.floor(targetMonthIndex / 12);
|
|
971
|
+
const normalizedMonth = (targetMonthIndex % 12 + 12) % 12;
|
|
972
|
+
const targetLastDay = new Date(targetYear, normalizedMonth + 1, 0).getDate();
|
|
973
|
+
const day = Math.min(date.getDate(), targetLastDay);
|
|
974
|
+
return new Date(targetYear, normalizedMonth, day, date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
|
|
975
|
+
}
|
|
976
|
+
function resolveLastWeekday(weekdayName, now) {
|
|
977
|
+
const targetDay = WEEKDAY_INDEX.get(weekdayName);
|
|
978
|
+
if (targetDay === void 0) {
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
const today = startOfDayLocal(now);
|
|
982
|
+
const currentDay = today.getDay();
|
|
983
|
+
const daysBack = (currentDay - targetDay + 7) % 7 || 7;
|
|
984
|
+
return addDaysLocal(today, -daysBack);
|
|
985
|
+
}
|
|
986
|
+
function buildLocalDateAtNoon(year, month, day) {
|
|
987
|
+
const parsed = new Date(year, month, day, 12, 0, 0, 0);
|
|
988
|
+
if (parsed.getFullYear() !== year || parsed.getMonth() !== month || parsed.getDate() !== day) {
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
return parsed;
|
|
992
|
+
}
|
|
993
|
+
function resolveWeekStartDay() {
|
|
994
|
+
try {
|
|
995
|
+
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
996
|
+
const info = new Intl.Locale(locale).weekInfo;
|
|
997
|
+
const firstDay = info?.firstDay;
|
|
998
|
+
if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) {
|
|
999
|
+
return firstDay % 7;
|
|
1000
|
+
}
|
|
1001
|
+
} catch {
|
|
1002
|
+
}
|
|
1003
|
+
return 1;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// ../../src/core/episode/scoring.ts
|
|
1007
|
+
var DAY_IN_MILLISECONDS4 = 24 * 60 * 60 * 1e3;
|
|
1008
|
+
function scoreEpisodeMatch(episode, bounds, now = /* @__PURE__ */ new Date()) {
|
|
1009
|
+
const episodeStart = parseEpisodeDate(episode.startedAt);
|
|
1010
|
+
const episodeEnd = parseEpisodeDate(episode.endedAt ?? episode.startedAt);
|
|
1011
|
+
const overlapQuality = computeOverlapQuality(episodeStart, episodeEnd, bounds.start, bounds.end);
|
|
1012
|
+
const midpointProximity = computeMidpointProximity(episodeStart, episodeEnd, bounds.start, bounds.end);
|
|
1013
|
+
const activity = activityScore(episode.activityLevel);
|
|
1014
|
+
const recency = recencyScore2(episodeEnd, now);
|
|
1015
|
+
const finalScore = overlapQuality * 0.75 + midpointProximity * 0.2 + activity * 0.04 + recency * 0.01;
|
|
1016
|
+
return {
|
|
1017
|
+
result: {
|
|
1018
|
+
episode,
|
|
1019
|
+
score: Number(finalScore.toFixed(6)),
|
|
1020
|
+
scores: {
|
|
1021
|
+
temporal: Number(overlapQuality.toFixed(6)),
|
|
1022
|
+
semantic: 0,
|
|
1023
|
+
activity: Number(activity.toFixed(6)),
|
|
1024
|
+
recency: Number(recency.toFixed(6))
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
explanation: {
|
|
1028
|
+
overlapQuality: Number(overlapQuality.toFixed(6)),
|
|
1029
|
+
midpointProximity: Number(midpointProximity.toFixed(6)),
|
|
1030
|
+
activity: Number(activity.toFixed(6)),
|
|
1031
|
+
recency: Number(recency.toFixed(6))
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
function compareEpisodeMatches(left, right) {
|
|
1036
|
+
return compareDescending(left.explanation.overlapQuality, right.explanation.overlapQuality) || compareDescending(left.explanation.midpointProximity, right.explanation.midpointProximity) || compareDescending(left.explanation.activity, right.explanation.activity) || compareDescending(left.explanation.recency, right.explanation.recency) || compareDescending(left.result.score, right.result.score) || compareAscending(left.result.episode.startedAt, right.result.episode.startedAt) || compareAscending(left.result.episode.id, right.result.episode.id);
|
|
1037
|
+
}
|
|
1038
|
+
function computeOverlapQuality(episodeStart, episodeEnd, queryStart, queryEnd) {
|
|
1039
|
+
const overlapStart = Math.max(episodeStart.getTime(), queryStart.getTime());
|
|
1040
|
+
const overlapEnd = Math.min(episodeEnd.getTime(), queryEnd.getTime());
|
|
1041
|
+
const overlapMs = Math.max(0, overlapEnd - overlapStart);
|
|
1042
|
+
if (overlapMs <= 0) {
|
|
1043
|
+
return 0;
|
|
1044
|
+
}
|
|
1045
|
+
const queryDurationMs = Math.max(1, queryEnd.getTime() - queryStart.getTime());
|
|
1046
|
+
const episodeDurationMs = Math.max(1, episodeEnd.getTime() - episodeStart.getTime());
|
|
1047
|
+
const coverage = overlapMs / queryDurationMs;
|
|
1048
|
+
const precision = overlapMs / episodeDurationMs;
|
|
1049
|
+
if (coverage <= 0 || precision <= 0) {
|
|
1050
|
+
return 0;
|
|
1051
|
+
}
|
|
1052
|
+
const beta = 0.5;
|
|
1053
|
+
const betaSquared = beta * beta;
|
|
1054
|
+
return (1 + betaSquared) * precision * coverage / (betaSquared * precision + coverage);
|
|
1055
|
+
}
|
|
1056
|
+
function computeMidpointProximity(episodeStart, episodeEnd, queryStart, queryEnd) {
|
|
1057
|
+
const episodeMidpoint = (episodeStart.getTime() + episodeEnd.getTime()) / 2;
|
|
1058
|
+
const queryMidpoint = (queryStart.getTime() + queryEnd.getTime()) / 2;
|
|
1059
|
+
const queryDurationMs = Math.max(1, queryEnd.getTime() - queryStart.getTime());
|
|
1060
|
+
const distanceMs = Math.abs(episodeMidpoint - queryMidpoint);
|
|
1061
|
+
return 1 / (1 + distanceMs / queryDurationMs);
|
|
1062
|
+
}
|
|
1063
|
+
function activityScore(value) {
|
|
1064
|
+
switch (value) {
|
|
1065
|
+
case "substantial":
|
|
1066
|
+
return 1;
|
|
1067
|
+
case "minimal":
|
|
1068
|
+
return 0.5;
|
|
1069
|
+
case "none":
|
|
1070
|
+
return 0;
|
|
1071
|
+
default:
|
|
1072
|
+
return 0.25;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
function recencyScore2(episodeEnd, now) {
|
|
1076
|
+
const ageMs = Math.max(0, now.getTime() - episodeEnd.getTime());
|
|
1077
|
+
const ageDays = ageMs / DAY_IN_MILLISECONDS4;
|
|
1078
|
+
return 1 / (1 + ageDays / 90);
|
|
1079
|
+
}
|
|
1080
|
+
function parseEpisodeDate(value) {
|
|
1081
|
+
const parsed = new Date(value);
|
|
1082
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
1083
|
+
throw new Error(`Episode timestamp is invalid: ${value}`);
|
|
1084
|
+
}
|
|
1085
|
+
return parsed;
|
|
1086
|
+
}
|
|
1087
|
+
function compareDescending(left, right) {
|
|
1088
|
+
if (left === right) {
|
|
1089
|
+
return 0;
|
|
1090
|
+
}
|
|
1091
|
+
return right > left ? 1 : -1;
|
|
1092
|
+
}
|
|
1093
|
+
function compareAscending(left, right) {
|
|
1094
|
+
return left.localeCompare(right);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// ../../src/core/episode/search.ts
|
|
1098
|
+
var DEFAULT_LIMIT = 10;
|
|
1099
|
+
var MIN_CANDIDATE_LIMIT = 25;
|
|
1100
|
+
var MAX_CANDIDATE_LIMIT = 100;
|
|
1101
|
+
var CANDIDATE_MULTIPLIER = 5;
|
|
1102
|
+
async function searchEpisodes(query, database, now = /* @__PURE__ */ new Date()) {
|
|
1103
|
+
const limit = normalizeLimit2(query.limit);
|
|
1104
|
+
if (limit === 0) {
|
|
1105
|
+
return [];
|
|
1106
|
+
}
|
|
1107
|
+
const normalizedEmbedding = normalizeEmbedding(query.embedding);
|
|
1108
|
+
const bounds = query.timeWindow ? resolveTemporalWindowBounds(query.timeWindow, now) : null;
|
|
1109
|
+
const hasTemporal = bounds !== null;
|
|
1110
|
+
const hasSemantic = normalizedEmbedding.length > 0;
|
|
1111
|
+
if (!hasTemporal && !hasSemantic) {
|
|
1112
|
+
return [];
|
|
1113
|
+
}
|
|
1114
|
+
if (hasTemporal && !hasSemantic) {
|
|
1115
|
+
const candidates2 = await database.listEpisodesByTimeWindow(query.timeWindow, computeCandidateLimit(limit));
|
|
1116
|
+
return candidates2.map((episode) => scoreEpisodeMatch(episode, bounds, now)).sort(compareEpisodeMatches).slice(0, limit).map((match) => match.result);
|
|
1117
|
+
}
|
|
1118
|
+
if (!hasTemporal) {
|
|
1119
|
+
const matches = await database.episodeVectorSearch({
|
|
1120
|
+
embedding: normalizedEmbedding,
|
|
1121
|
+
limit
|
|
1122
|
+
});
|
|
1123
|
+
return matches.map((match) => buildSemanticResult(match.episode, match.vectorSim, now)).sort(compareSemanticEpisodeResults).slice(0, limit);
|
|
1124
|
+
}
|
|
1125
|
+
const candidates = await database.listEpisodesByTimeWindow(query.timeWindow, computeCandidateLimit(limit));
|
|
1126
|
+
return candidates.map((episode) => buildHybridResult(episode, normalizedEmbedding, bounds, now)).sort(compareSemanticEpisodeResults).slice(0, limit);
|
|
1127
|
+
}
|
|
1128
|
+
function normalizeLimit2(value) {
|
|
1129
|
+
if (value === void 0) {
|
|
1130
|
+
return DEFAULT_LIMIT;
|
|
1131
|
+
}
|
|
1132
|
+
if (!Number.isFinite(value)) {
|
|
1133
|
+
return DEFAULT_LIMIT;
|
|
1134
|
+
}
|
|
1135
|
+
return Math.max(0, Math.trunc(value));
|
|
1136
|
+
}
|
|
1137
|
+
function computeCandidateLimit(limit) {
|
|
1138
|
+
return Math.min(Math.max(limit * CANDIDATE_MULTIPLIER, MIN_CANDIDATE_LIMIT), MAX_CANDIDATE_LIMIT);
|
|
1139
|
+
}
|
|
1140
|
+
function normalizeEmbedding(embedding) {
|
|
1141
|
+
if (!embedding || embedding.length === 0) {
|
|
1142
|
+
return [];
|
|
1143
|
+
}
|
|
1144
|
+
return embedding.map((value) => Number.isFinite(value) ? value : 0);
|
|
1145
|
+
}
|
|
1146
|
+
function buildSemanticResult(episode, semantic, now) {
|
|
1147
|
+
const parsedEpisodeEnd = new Date(episode.endedAt ?? episode.startedAt);
|
|
1148
|
+
const episodeEnd = Number.isNaN(parsedEpisodeEnd.getTime()) ? now : parsedEpisodeEnd;
|
|
1149
|
+
const activity = activityScore(episode.activityLevel);
|
|
1150
|
+
const recency = recencyScore2(episodeEnd, now);
|
|
1151
|
+
const normalizedSemantic = Number(semantic.toFixed(6));
|
|
1152
|
+
return {
|
|
1153
|
+
episode,
|
|
1154
|
+
score: normalizedSemantic,
|
|
1155
|
+
scores: {
|
|
1156
|
+
temporal: 0,
|
|
1157
|
+
semantic: normalizedSemantic,
|
|
1158
|
+
activity: Number(activity.toFixed(6)),
|
|
1159
|
+
recency: Number(recency.toFixed(6))
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
function buildHybridResult(episode, queryEmbedding, bounds, now) {
|
|
1164
|
+
const temporalMatch = scoreEpisodeMatch(episode, bounds, now);
|
|
1165
|
+
const semantic = Number(cosineSimilarity(queryEmbedding, episode.embedding ?? []).toFixed(6));
|
|
1166
|
+
return {
|
|
1167
|
+
episode,
|
|
1168
|
+
score: semantic,
|
|
1169
|
+
scores: {
|
|
1170
|
+
temporal: temporalMatch.result.scores.temporal,
|
|
1171
|
+
semantic,
|
|
1172
|
+
activity: temporalMatch.result.scores.activity,
|
|
1173
|
+
recency: temporalMatch.result.scores.recency
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
function compareSemanticEpisodeResults(left, right) {
|
|
1178
|
+
return compareDescending2(left.scores.semantic, right.scores.semantic) || compareDescending2(left.scores.temporal, right.scores.temporal) || compareDescending2(left.scores.activity, right.scores.activity) || compareDescending2(left.scores.recency, right.scores.recency) || compareDescending2(left.score, right.score) || compareAscending2(left.episode.startedAt, right.episode.startedAt) || compareAscending2(left.episode.id, right.episode.id);
|
|
1179
|
+
}
|
|
1180
|
+
function compareDescending2(left, right) {
|
|
1181
|
+
if (left === right) {
|
|
1182
|
+
return 0;
|
|
1183
|
+
}
|
|
1184
|
+
return right > left ? 1 : -1;
|
|
1185
|
+
}
|
|
1186
|
+
function compareAscending2(left, right) {
|
|
1187
|
+
return left.localeCompare(right);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// ../../src/app/recall/unified.ts
|
|
1191
|
+
var EPISODE_FRESHNESS_NOTICE = "Episodes cover consolidated prior sessions only; the most recent completed session may not appear yet.";
|
|
1192
|
+
var EPISODE_SEMANTIC_FALLBACK_NOTICE = "Semantic episode search unavailable - showing temporal results only.";
|
|
1193
|
+
var EPISODE_SEMANTIC_UNAVAILABLE_NOTICE = "Semantic episode search unavailable - no semantic episode results could be returned.";
|
|
1194
|
+
var ENTRY_FILTER_NOTICE = "Threshold, type filters, and tag filters were applied to entries only.";
|
|
1195
|
+
async function runUnifiedRecall(input, deps) {
|
|
1196
|
+
const now = deps.now ?? /* @__PURE__ */ new Date();
|
|
1197
|
+
const requested = normalizeMode(input.mode);
|
|
1198
|
+
const parsedTimeWindow = parseTemporalWindow(input.text, now);
|
|
1199
|
+
const hasEntryFilters = hasEntryScopedFilters(input);
|
|
1200
|
+
const topicAnchor = hasTopicAnchor(input.text, hasEntryFilters);
|
|
1201
|
+
const routing = routeRecall({
|
|
1202
|
+
requested,
|
|
1203
|
+
text: input.text,
|
|
1204
|
+
parsedTimeWindow: parsedTimeWindow !== null,
|
|
1205
|
+
hasEntryFilters
|
|
1206
|
+
});
|
|
1207
|
+
const notices = [];
|
|
1208
|
+
const episodePlan = routing.queried.includes("episodes") ? await buildEpisodeQueryPlan({
|
|
1209
|
+
text: input.text,
|
|
1210
|
+
limit: input.limit,
|
|
1211
|
+
requested,
|
|
1212
|
+
parsedTimeWindow,
|
|
1213
|
+
topicAnchor,
|
|
1214
|
+
embedQuery: deps.embedQuery
|
|
1215
|
+
}) : {
|
|
1216
|
+
notices: []
|
|
1217
|
+
};
|
|
1218
|
+
const episodes = routing.queried.includes("episodes") && episodePlan.query ? await searchEpisodes(episodePlan.query, deps.database, now) : [];
|
|
1219
|
+
if (routing.queried.includes("episodes")) {
|
|
1220
|
+
notices.push(EPISODE_FRESHNESS_NOTICE);
|
|
1221
|
+
notices.push(...episodePlan.notices);
|
|
1222
|
+
}
|
|
1223
|
+
if (routing.queried.includes("episodes") && hasEntryScopedFilters(input)) {
|
|
1224
|
+
notices.push(ENTRY_FILTER_NOTICE);
|
|
1225
|
+
}
|
|
1226
|
+
const entries = await maybeRunEntryRecall({
|
|
1227
|
+
input,
|
|
1228
|
+
deps,
|
|
1229
|
+
parsedTimeWindow,
|
|
1230
|
+
routing
|
|
1231
|
+
});
|
|
1232
|
+
if (routing.queried.includes("entries") && entries.kind === "skipped") {
|
|
1233
|
+
notices.push(entries.notice);
|
|
1234
|
+
}
|
|
1235
|
+
return {
|
|
1236
|
+
routing,
|
|
1237
|
+
...parsedTimeWindow ? {
|
|
1238
|
+
parsedTimeWindow,
|
|
1239
|
+
timeWindow: {
|
|
1240
|
+
start: parsedTimeWindow.bounds.start.toISOString(),
|
|
1241
|
+
end: parsedTimeWindow.bounds.end.toISOString(),
|
|
1242
|
+
timezone: parsedTimeWindow.timezone,
|
|
1243
|
+
resolvedFrom: parsedTimeWindow.resolvedFrom
|
|
1244
|
+
}
|
|
1245
|
+
} : {},
|
|
1246
|
+
episodes,
|
|
1247
|
+
entries: entries.kind === "results" ? entries.results : [],
|
|
1248
|
+
notices: dedupePreservingOrder(notices),
|
|
1249
|
+
count: episodes.length + (entries.kind === "results" ? entries.results.length : 0)
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
function routeRecall(params) {
|
|
1253
|
+
const lower = params.text.trim().toLowerCase();
|
|
1254
|
+
const factual = /^(when did|when was|what decision|what preference|what(?:'s| is) the default|which version|what threshold)\b/.test(lower);
|
|
1255
|
+
const narrative = /\b(what happened|what were we doing|what was going on|summarize|catch me up)\b/.test(lower);
|
|
1256
|
+
const topicAnchor = hasTopicAnchor(params.text, params.hasEntryFilters);
|
|
1257
|
+
if (params.requested === "entries") {
|
|
1258
|
+
return {
|
|
1259
|
+
requested: params.requested,
|
|
1260
|
+
detectedIntent: factual ? "factual" : params.parsedTimeWindow ? "mixed" : "factual",
|
|
1261
|
+
queried: ["entries"],
|
|
1262
|
+
reason: "Explicit mode=entries override."
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
if (params.requested === "episodes") {
|
|
1266
|
+
return {
|
|
1267
|
+
requested: params.requested,
|
|
1268
|
+
detectedIntent: params.parsedTimeWindow ? "temporal_narrative" : "mixed",
|
|
1269
|
+
queried: ["episodes"],
|
|
1270
|
+
reason: params.parsedTimeWindow ? "Explicit mode=episodes override with a resolved time window." : "Explicit mode=episodes override without a resolved time window."
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
if (factual && params.parsedTimeWindow) {
|
|
1274
|
+
return {
|
|
1275
|
+
requested: params.requested,
|
|
1276
|
+
detectedIntent: "mixed",
|
|
1277
|
+
queried: ["entries", "episodes"],
|
|
1278
|
+
reason: "The query combines a factual phrase with a supported time expression, so both entries and episodes were queried."
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
if (factual) {
|
|
1282
|
+
return {
|
|
1283
|
+
requested: params.requested,
|
|
1284
|
+
detectedIntent: "factual",
|
|
1285
|
+
queried: ["entries"],
|
|
1286
|
+
reason: "The query looks like an exact fact lookup, so entry recall was used."
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
if (params.parsedTimeWindow && narrative && topicAnchor) {
|
|
1290
|
+
return {
|
|
1291
|
+
requested: params.requested,
|
|
1292
|
+
detectedIntent: "mixed",
|
|
1293
|
+
queried: ["episodes", "entries"],
|
|
1294
|
+
reason: "The query combines narrative time-based recall with a topic anchor, so both episodes and entries were queried."
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
if (params.parsedTimeWindow && narrative) {
|
|
1298
|
+
return {
|
|
1299
|
+
requested: params.requested,
|
|
1300
|
+
detectedIntent: "temporal_narrative",
|
|
1301
|
+
queried: ["episodes"],
|
|
1302
|
+
reason: "The query asks for what happened during a time period, so episode recall was used first."
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
if (params.parsedTimeWindow && topicAnchor) {
|
|
1306
|
+
return {
|
|
1307
|
+
requested: params.requested,
|
|
1308
|
+
detectedIntent: "mixed",
|
|
1309
|
+
queried: ["episodes", "entries"],
|
|
1310
|
+
reason: "The query contains both a supported time expression and a topic anchor, so both episodes and entries were queried."
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
return {
|
|
1314
|
+
requested: params.requested,
|
|
1315
|
+
detectedIntent: "factual",
|
|
1316
|
+
queried: ["entries"],
|
|
1317
|
+
reason: params.parsedTimeWindow ? "The query did not clearly ask for narrative recall, so entry recall was used." : "No supported episode time window was detected, so entry recall was used."
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
async function buildEpisodeQueryPlan(params) {
|
|
1321
|
+
const notices = [];
|
|
1322
|
+
const shouldUseSemantic = params.parsedTimeWindow ? params.topicAnchor : params.requested === "episodes";
|
|
1323
|
+
let embedding;
|
|
1324
|
+
if (shouldUseSemantic) {
|
|
1325
|
+
embedding = await maybeEmbedEpisodeQuery(params.text, params.embedQuery);
|
|
1326
|
+
if (!embedding) {
|
|
1327
|
+
notices.push(params.parsedTimeWindow ? EPISODE_SEMANTIC_FALLBACK_NOTICE : EPISODE_SEMANTIC_UNAVAILABLE_NOTICE);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
if (!params.parsedTimeWindow && !embedding) {
|
|
1331
|
+
return {
|
|
1332
|
+
notices
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
return {
|
|
1336
|
+
query: {
|
|
1337
|
+
text: params.text,
|
|
1338
|
+
...params.limit !== void 0 ? { limit: params.limit } : {},
|
|
1339
|
+
...params.parsedTimeWindow ? { timeWindow: params.parsedTimeWindow.window } : {},
|
|
1340
|
+
...embedding ? { embedding } : {}
|
|
1341
|
+
},
|
|
1342
|
+
notices
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
async function maybeRunEntryRecall(params) {
|
|
1346
|
+
if (!params.routing.queried.includes("entries")) {
|
|
1347
|
+
return {
|
|
1348
|
+
kind: "results",
|
|
1349
|
+
results: []
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
if (!params.deps.embeddingAvailable) {
|
|
1353
|
+
const message = params.deps.embeddingError ?? "Embeddings are unavailable, so entry recall could not run.";
|
|
1354
|
+
if (params.routing.requested === "entries") {
|
|
1355
|
+
throw new Error(message);
|
|
1356
|
+
}
|
|
1357
|
+
return {
|
|
1358
|
+
kind: "skipped",
|
|
1359
|
+
notice: `${message} Entry recall was skipped.`
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
return {
|
|
1363
|
+
kind: "results",
|
|
1364
|
+
results: await recall(buildEntryRecallInput(params.input, params.parsedTimeWindow), params.deps.recall)
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
function buildEntryRecallInput(input, parsedTimeWindow) {
|
|
1368
|
+
const request = {
|
|
1369
|
+
text: input.text,
|
|
1370
|
+
...input.limit !== void 0 ? { limit: input.limit } : {},
|
|
1371
|
+
...input.threshold !== void 0 ? { threshold: input.threshold } : {},
|
|
1372
|
+
...input.types && input.types.length > 0 ? { types: input.types } : {},
|
|
1373
|
+
...input.tags && input.tags.length > 0 ? { tags: input.tags } : {},
|
|
1374
|
+
...input.sessionKey ? { sessionKey: input.sessionKey } : {}
|
|
1375
|
+
};
|
|
1376
|
+
if (!parsedTimeWindow) {
|
|
1377
|
+
return request;
|
|
1378
|
+
}
|
|
1379
|
+
const start = parsedTimeWindow.bounds.start;
|
|
1380
|
+
const end = parsedTimeWindow.bounds.end;
|
|
1381
|
+
const midpoint = new Date((start.getTime() + end.getTime()) / 2);
|
|
1382
|
+
const radiusDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 2 / (24 * 60 * 60 * 1e3)));
|
|
1383
|
+
return {
|
|
1384
|
+
...request,
|
|
1385
|
+
since: start.toISOString(),
|
|
1386
|
+
until: end.toISOString(),
|
|
1387
|
+
around: midpoint.toISOString(),
|
|
1388
|
+
aroundRadius: radiusDays
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
function normalizeMode(value) {
|
|
1392
|
+
return value === "entries" || value === "episodes" ? value : "auto";
|
|
1393
|
+
}
|
|
1394
|
+
function hasEntryScopedFilters(input) {
|
|
1395
|
+
return Boolean(input.threshold !== void 0 || (input.types?.length ?? 0) > 0 || (input.tags?.length ?? 0) > 0);
|
|
1396
|
+
}
|
|
1397
|
+
function hasTopicAnchor(text, hasEntryFilters) {
|
|
1398
|
+
const lower = text.trim().toLowerCase();
|
|
1399
|
+
return hasEntryFilters || /\b(about|regarding|with)\b/.test(lower) || /\bon\s+[a-z][a-z0-9_-]{1,}\b/.test(lower);
|
|
1400
|
+
}
|
|
1401
|
+
async function maybeEmbedEpisodeQuery(text, embedQuery) {
|
|
1402
|
+
if (!embedQuery) {
|
|
1403
|
+
return void 0;
|
|
1404
|
+
}
|
|
1405
|
+
try {
|
|
1406
|
+
const embedding = await embedQuery(text);
|
|
1407
|
+
return embedding.length > 0 ? embedding : void 0;
|
|
1408
|
+
} catch {
|
|
1409
|
+
return void 0;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function dedupePreservingOrder(values) {
|
|
1413
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1414
|
+
const deduped = [];
|
|
1415
|
+
for (const value of values) {
|
|
1416
|
+
if (seen.has(value)) {
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
seen.add(value);
|
|
1420
|
+
deduped.push(value);
|
|
1421
|
+
}
|
|
1422
|
+
return deduped;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
638
1425
|
// ../../src/core/store/pipeline.ts
|
|
639
1426
|
import { randomUUID } from "crypto";
|
|
640
1427
|
|
|
@@ -656,8 +1443,9 @@ function computeNormContentHash(content) {
|
|
|
656
1443
|
}
|
|
657
1444
|
|
|
658
1445
|
// ../../src/core/types.ts
|
|
659
|
-
var ENTRY_TYPES = ["fact", "decision", "preference", "lesson", "
|
|
1446
|
+
var ENTRY_TYPES = ["fact", "decision", "preference", "lesson", "relationship", "milestone"];
|
|
660
1447
|
var EXPIRY_LEVELS = ["core", "permanent", "temporary"];
|
|
1448
|
+
var EPISODE_ACTIVITY_LEVELS = ["substantial", "minimal", "none"];
|
|
661
1449
|
|
|
662
1450
|
// ../../src/core/store/validation.ts
|
|
663
1451
|
function validateEntriesWithIndexes(inputs) {
|
|
@@ -708,6 +1496,8 @@ function validateEntriesWithIndexes(inputs) {
|
|
|
708
1496
|
tags: normalizeTags(input.tags),
|
|
709
1497
|
source_file: normalizeOptionalString(input.source_file),
|
|
710
1498
|
source_context: normalizeOptionalString(input.source_context),
|
|
1499
|
+
user_id: normalizeOptionalString(input.user_id),
|
|
1500
|
+
project: normalizeOptionalString(input.project),
|
|
711
1501
|
created_at: normalizeOptionalString(input.created_at)
|
|
712
1502
|
}
|
|
713
1503
|
});
|
|
@@ -837,6 +1627,8 @@ function buildEntry(preparedEntry, embedding) {
|
|
|
837
1627
|
tags: preparedEntry.input.tags ?? [],
|
|
838
1628
|
source_file: preparedEntry.input.source_file,
|
|
839
1629
|
source_context: preparedEntry.input.source_context,
|
|
1630
|
+
user_id: preparedEntry.input.user_id,
|
|
1631
|
+
project: preparedEntry.input.project,
|
|
840
1632
|
embedding,
|
|
841
1633
|
content_hash: preparedEntry.contentHash,
|
|
842
1634
|
norm_content_hash: preparedEntry.normContentHash,
|
|
@@ -914,12 +1706,19 @@ function sortStoreDetails(details) {
|
|
|
914
1706
|
// ../../src/adapters/db/row-mapping.ts
|
|
915
1707
|
var DEFAULT_QUALITY_SCORE = 0.5;
|
|
916
1708
|
var ACTIVE_ENTRY_CLAUSE = "retired = 0 AND superseded_by IS NULL";
|
|
1709
|
+
var ACTIVE_EPISODE_CLAUSE = "retired = 0 AND superseded_by IS NULL";
|
|
917
1710
|
function buildActiveEntryClause(alias) {
|
|
918
1711
|
if (!alias) {
|
|
919
1712
|
return ACTIVE_ENTRY_CLAUSE;
|
|
920
1713
|
}
|
|
921
1714
|
return `${alias}.retired = 0 AND ${alias}.superseded_by IS NULL`;
|
|
922
1715
|
}
|
|
1716
|
+
function buildActiveEpisodeClause(alias) {
|
|
1717
|
+
if (!alias) {
|
|
1718
|
+
return ACTIVE_EPISODE_CLAUSE;
|
|
1719
|
+
}
|
|
1720
|
+
return `${alias}.retired = 0 AND ${alias}.superseded_by IS NULL`;
|
|
1721
|
+
}
|
|
923
1722
|
function serializeEmbeddingForVector(embedding) {
|
|
924
1723
|
if (embedding.length === 0) {
|
|
925
1724
|
return null;
|
|
@@ -1017,6 +1816,8 @@ function mapEntryRow(row) {
|
|
|
1017
1816
|
last_recalled_at: readOptionalString(row, "last_recalled_at"),
|
|
1018
1817
|
superseded_by: readOptionalString(row, "superseded_by"),
|
|
1019
1818
|
cluster_id: readOptionalString(row, "cluster_id"),
|
|
1819
|
+
user_id: readOptionalString(row, "user_id"),
|
|
1820
|
+
project: readOptionalString(row, "project"),
|
|
1020
1821
|
retired: readBoolean(row, "retired"),
|
|
1021
1822
|
retired_at: readOptionalString(row, "retired_at"),
|
|
1022
1823
|
retired_reason: readOptionalString(row, "retired_reason"),
|
|
@@ -1024,6 +1825,43 @@ function mapEntryRow(row) {
|
|
|
1024
1825
|
updated_at: readRequiredString(row, "updated_at")
|
|
1025
1826
|
};
|
|
1026
1827
|
}
|
|
1828
|
+
function mapEpisodeRow(row) {
|
|
1829
|
+
const source = readRequiredString(row, "source");
|
|
1830
|
+
const activityLevel = readOptionalString(row, "activity_level");
|
|
1831
|
+
return {
|
|
1832
|
+
id: readRequiredString(row, "id"),
|
|
1833
|
+
source,
|
|
1834
|
+
sourceId: readOptionalString(row, "source_id"),
|
|
1835
|
+
sourceRef: readOptionalString(row, "source_ref"),
|
|
1836
|
+
transcriptHash: readOptionalString(row, "transcript_hash"),
|
|
1837
|
+
summaryHash: readOptionalString(row, "summary_hash"),
|
|
1838
|
+
agentId: readOptionalString(row, "agent_id"),
|
|
1839
|
+
surface: readOptionalString(row, "surface"),
|
|
1840
|
+
startedAt: readRequiredString(row, "started_at"),
|
|
1841
|
+
endedAt: readOptionalString(row, "ended_at"),
|
|
1842
|
+
summary: readRequiredString(row, "summary"),
|
|
1843
|
+
tags: deserializeTags(row.tags),
|
|
1844
|
+
activityLevel,
|
|
1845
|
+
userId: readOptionalString(row, "user_id"),
|
|
1846
|
+
project: readOptionalString(row, "project"),
|
|
1847
|
+
genModel: readOptionalString(row, "gen_model"),
|
|
1848
|
+
genVersion: readOptionalString(row, "gen_version"),
|
|
1849
|
+
messageCount: readOptionalNumber(row, "message_count"),
|
|
1850
|
+
embedding: readEmbedding(row, "embedding"),
|
|
1851
|
+
retired: readBoolean(row, "retired"),
|
|
1852
|
+
retiredAt: readOptionalString(row, "retired_at"),
|
|
1853
|
+
retiredReason: readOptionalString(row, "retired_reason"),
|
|
1854
|
+
supersededBy: readOptionalString(row, "superseded_by"),
|
|
1855
|
+
createdAt: readRequiredString(row, "created_at"),
|
|
1856
|
+
updatedAt: readRequiredString(row, "updated_at")
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
function readOptionalNumber(row, key) {
|
|
1860
|
+
if (row[key] === null || row[key] === void 0) {
|
|
1861
|
+
return void 0;
|
|
1862
|
+
}
|
|
1863
|
+
return readNumber(row, key, 0);
|
|
1864
|
+
}
|
|
1027
1865
|
|
|
1028
1866
|
// ../../src/adapters/db/openclaw-plugin-queries.ts
|
|
1029
1867
|
var ENTRY_SELECT_COLUMNS = `
|
|
@@ -1044,6 +1882,8 @@ var ENTRY_SELECT_COLUMNS = `
|
|
|
1044
1882
|
last_recalled_at,
|
|
1045
1883
|
superseded_by,
|
|
1046
1884
|
cluster_id,
|
|
1885
|
+
user_id,
|
|
1886
|
+
project,
|
|
1047
1887
|
retired,
|
|
1048
1888
|
retired_at,
|
|
1049
1889
|
retired_reason,
|
|
@@ -1201,11 +2041,12 @@ async function listRecallEvents(executor, entryId) {
|
|
|
1201
2041
|
}
|
|
1202
2042
|
|
|
1203
2043
|
// ../../src/adapters/openclaw/tools.ts
|
|
1204
|
-
var ENTRY_TYPE_DESCRIPTION = "Knowledge type to store. Use decision for rules or
|
|
2044
|
+
var ENTRY_TYPE_DESCRIPTION = "Knowledge type to store. Use fact for durable information about people, places, systems, or how things work. Use decision for standing rules, constraints, or chosen approaches. Use preference for stated wants, values, or opinions. Use lesson for non-obvious insights learned from specific experience. Use milestone for notable one-time events worth remembering (a move, a launch, a life change, a hire, a trip). Use relationship for meaningful connections between people, groups, or systems.";
|
|
1205
2045
|
var EXPIRY_DESCRIPTION = "Lifetime bucket: core (always injected at session start, use sparingly), permanent (durable and recalled on demand), or temporary (short-horizon).";
|
|
1206
2046
|
var UPDATE_EXPIRY_DESCRIPTION = `${EXPIRY_DESCRIPTION} Accepted values: ${EXPIRY_LEVELS.join(", ")}.`;
|
|
1207
2047
|
var DEFAULT_RECALL_LIMIT = 10;
|
|
1208
2048
|
var RESULT_SUBJECT_LOG_LIMIT = 5;
|
|
2049
|
+
var RECALL_MODES = ["auto", "entries", "episodes"];
|
|
1209
2050
|
var STORE_TOOL_PARAMETERS = {
|
|
1210
2051
|
type: "object",
|
|
1211
2052
|
additionalProperties: false,
|
|
@@ -1254,6 +2095,11 @@ var RECALL_TOOL_PARAMETERS = {
|
|
|
1254
2095
|
type: "string",
|
|
1255
2096
|
description: "What you need to remember. Use a focused natural-language query rather than a broad 'everything' search."
|
|
1256
2097
|
},
|
|
2098
|
+
mode: {
|
|
2099
|
+
type: "string",
|
|
2100
|
+
enum: [...RECALL_MODES],
|
|
2101
|
+
description: "Recall mode: auto routes between entries and episodes, entries forces semantic recall, and episodes forces temporal session recall."
|
|
2102
|
+
},
|
|
1257
2103
|
limit: {
|
|
1258
2104
|
type: "integer",
|
|
1259
2105
|
minimum: 1,
|
|
@@ -1272,29 +2118,12 @@ var RECALL_TOOL_PARAMETERS = {
|
|
|
1272
2118
|
type: "string",
|
|
1273
2119
|
enum: [...ENTRY_TYPES]
|
|
1274
2120
|
},
|
|
1275
|
-
description: "Optional knowledge types to filter by, such as decision, preference, lesson, fact, or
|
|
2121
|
+
description: "Optional knowledge types to filter by, such as decision, preference, lesson, fact, milestone, or relationship."
|
|
1276
2122
|
},
|
|
1277
2123
|
tags: {
|
|
1278
2124
|
type: "array",
|
|
1279
2125
|
items: { type: "string" },
|
|
1280
2126
|
description: "Optional tags to filter by once you already know the relevant entity, system, or theme."
|
|
1281
|
-
},
|
|
1282
|
-
since: {
|
|
1283
|
-
type: "string",
|
|
1284
|
-
description: "Only consider entries created on or after this time bound. Accepts ISO dates or relative dates such as 30d. Relative dates count backward from now: 7d means 7 days ago, 30d means 30 days ago."
|
|
1285
|
-
},
|
|
1286
|
-
until: {
|
|
1287
|
-
type: "string",
|
|
1288
|
-
description: "Only consider entries created on or before this time bound. Accepts ISO dates or relative dates such as 7d. Relative dates count backward from now: 7d means 7 days ago, 30d means 30 days ago."
|
|
1289
|
-
},
|
|
1290
|
-
around: {
|
|
1291
|
-
type: "string",
|
|
1292
|
-
description: "Bias ranking toward a specific date or period, such as 7d for one week ago or 2026-03-15 for a specific date. This is a temporal anchor, not a hard filter."
|
|
1293
|
-
},
|
|
1294
|
-
aroundRadius: {
|
|
1295
|
-
type: "integer",
|
|
1296
|
-
minimum: 1,
|
|
1297
|
-
description: "Radius in days for around, e.g. 3 for a +/-3 day window around the anchor. Smaller values focus recall more tightly on that period."
|
|
1298
2127
|
}
|
|
1299
2128
|
},
|
|
1300
2129
|
required: ["query"]
|
|
@@ -1445,32 +2274,25 @@ function createAgenrRecallTool(ctx, servicesPromise, logger) {
|
|
|
1445
2274
|
return {
|
|
1446
2275
|
name: "agenr_recall",
|
|
1447
2276
|
label: "Agenr Recall",
|
|
1448
|
-
description: "Retrieve knowledge from agenr long-term memory.
|
|
2277
|
+
description: "Retrieve knowledge from agenr long-term memory. Use mode=auto for the normal path, mode=entries for exact facts and decisions, and mode=episodes for time-bounded 'what happened' questions. Time periods are parsed from the query text. Session-start recall is already handled automatically.",
|
|
1449
2278
|
parameters: RECALL_TOOL_PARAMETERS,
|
|
1450
2279
|
async execute(_toolCallId, rawParams) {
|
|
1451
2280
|
try {
|
|
1452
2281
|
const params = asRecord(rawParams);
|
|
1453
2282
|
const query = readStringParam(params, "query", { required: true, label: "query" });
|
|
2283
|
+
const mode = parseRecallMode(readStringParam(params, "mode"));
|
|
1454
2284
|
const limit = readNumberParam(params, "limit", { integer: true, strict: true });
|
|
1455
2285
|
const threshold = readNumberParam(params, "threshold", { strict: true });
|
|
1456
|
-
const project = typeof params.project === "string" && params.project.trim().length > 0 ? params.project.trim() : void 0;
|
|
1457
2286
|
const services = await servicesPromise;
|
|
1458
2287
|
const types = parseEntryTypes(readStringArrayParam(params, "types"));
|
|
1459
2288
|
const tags = normalizeStringArray(readStringArrayParam(params, "tags"));
|
|
1460
|
-
const since = readStringParam(params, "since");
|
|
1461
|
-
const until = readStringParam(params, "until");
|
|
1462
|
-
const around = readStringParam(params, "around");
|
|
1463
|
-
const aroundRadius = readNumberParam(params, "aroundRadius", { integer: true, strict: true });
|
|
1464
2289
|
const request = {
|
|
1465
2290
|
text: query,
|
|
2291
|
+
...mode ? { mode } : {},
|
|
1466
2292
|
...limit !== void 0 ? { limit } : {},
|
|
1467
2293
|
...threshold !== void 0 ? { threshold } : {},
|
|
1468
2294
|
...types.length > 0 ? { types } : {},
|
|
1469
2295
|
...tags.length > 0 ? { tags } : {},
|
|
1470
|
-
...since ? { since } : {},
|
|
1471
|
-
...until ? { until } : {},
|
|
1472
|
-
...around ? { around } : {},
|
|
1473
|
-
...aroundRadius !== void 0 ? { aroundRadius } : {},
|
|
1474
2296
|
sessionKey: ctx.sessionKey
|
|
1475
2297
|
};
|
|
1476
2298
|
logToolCall(
|
|
@@ -1479,57 +2301,64 @@ function createAgenrRecallTool(ctx, servicesPromise, logger) {
|
|
|
1479
2301
|
ctx,
|
|
1480
2302
|
formatRecallToolSummary({
|
|
1481
2303
|
query,
|
|
2304
|
+
mode,
|
|
1482
2305
|
limit,
|
|
1483
2306
|
types,
|
|
1484
|
-
tags
|
|
1485
|
-
since,
|
|
1486
|
-
until,
|
|
1487
|
-
around,
|
|
1488
|
-
aroundRadius,
|
|
1489
|
-
project
|
|
2307
|
+
tags
|
|
1490
2308
|
}),
|
|
1491
2309
|
sanitizeRecallToolParams({
|
|
1492
2310
|
query,
|
|
2311
|
+
mode,
|
|
1493
2312
|
limit,
|
|
1494
2313
|
threshold,
|
|
1495
2314
|
types,
|
|
1496
|
-
tags
|
|
1497
|
-
since,
|
|
1498
|
-
until,
|
|
1499
|
-
around,
|
|
1500
|
-
aroundRadius,
|
|
1501
|
-
project
|
|
2315
|
+
tags
|
|
1502
2316
|
})
|
|
1503
2317
|
);
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
count: 0,
|
|
1517
|
-
results: []
|
|
1518
|
-
});
|
|
1519
|
-
}
|
|
1520
|
-
return textResult(formatRecallResults(results), {
|
|
2318
|
+
const result = await runUnifiedRecall(request, {
|
|
2319
|
+
database: services.database,
|
|
2320
|
+
recall: services.recall,
|
|
2321
|
+
embeddingAvailable: services.embeddingStatus.available,
|
|
2322
|
+
embeddingError: services.embeddingStatus.error,
|
|
2323
|
+
embedQuery: services.embeddingStatus.available ? async (text) => {
|
|
2324
|
+
const vectors = await services.embedding.embed([text]);
|
|
2325
|
+
return vectors[0] ?? [];
|
|
2326
|
+
} : void 0
|
|
2327
|
+
});
|
|
2328
|
+
logger.info(`[agenr] tool=agenr_recall ${formatToolSessionContext(ctx)} result: ${formatUnifiedRecallLogSummary(result)}`);
|
|
2329
|
+
return textResult(formatUnifiedRecallResults(result), {
|
|
1521
2330
|
status: "ok",
|
|
1522
|
-
count:
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
2331
|
+
count: result.count,
|
|
2332
|
+
routing: {
|
|
2333
|
+
requested: result.routing.requested,
|
|
2334
|
+
detectedIntent: result.routing.detectedIntent,
|
|
2335
|
+
queried: result.routing.queried,
|
|
2336
|
+
reason: result.routing.reason
|
|
2337
|
+
},
|
|
2338
|
+
...result.timeWindow ? { timeWindow: result.timeWindow } : {},
|
|
2339
|
+
episodes: result.episodes.map((episode) => ({
|
|
2340
|
+
id: episode.episode.id,
|
|
2341
|
+
source: episode.episode.source,
|
|
2342
|
+
sourceId: episode.episode.sourceId,
|
|
2343
|
+
startedAt: episode.episode.startedAt,
|
|
2344
|
+
endedAt: episode.episode.endedAt,
|
|
2345
|
+
tags: episode.episode.tags,
|
|
2346
|
+
score: episode.score,
|
|
2347
|
+
activityLevel: episode.episode.activityLevel,
|
|
2348
|
+
summary: episode.episode.summary,
|
|
2349
|
+
whyMatched: describeEpisodeMatch(episode)
|
|
2350
|
+
})),
|
|
2351
|
+
entries: result.entries.map((entry) => ({
|
|
2352
|
+
id: entry.entry.id,
|
|
2353
|
+
subject: entry.entry.subject,
|
|
2354
|
+
type: entry.entry.type,
|
|
2355
|
+
expiry: entry.entry.expiry,
|
|
2356
|
+
importance: entry.entry.importance,
|
|
2357
|
+
score: entry.score,
|
|
2358
|
+
tags: entry.entry.tags,
|
|
2359
|
+
content: entry.entry.content
|
|
2360
|
+
})),
|
|
2361
|
+
notices: result.notices
|
|
1533
2362
|
});
|
|
1534
2363
|
} catch (error) {
|
|
1535
2364
|
logToolFailure(logger, "agenr_recall", ctx, error);
|
|
@@ -1704,6 +2533,15 @@ function readBooleanParam(params, key) {
|
|
|
1704
2533
|
function parseEntryTypes(values) {
|
|
1705
2534
|
return normalizeStringArray(values).map((value) => parseEntryType(value));
|
|
1706
2535
|
}
|
|
2536
|
+
function parseRecallMode(value) {
|
|
2537
|
+
if (value === void 0) {
|
|
2538
|
+
return void 0;
|
|
2539
|
+
}
|
|
2540
|
+
if (value === "auto" || value === "entries" || value === "episodes") {
|
|
2541
|
+
return value;
|
|
2542
|
+
}
|
|
2543
|
+
throw new Error(`Unsupported recall mode "${value}".`);
|
|
2544
|
+
}
|
|
1707
2545
|
function parseEntryType(value) {
|
|
1708
2546
|
if (ENTRY_TYPES.includes(value)) {
|
|
1709
2547
|
return value;
|
|
@@ -1775,20 +2613,8 @@ function sanitizeStoreToolParams(params) {
|
|
|
1775
2613
|
}
|
|
1776
2614
|
function formatRecallToolSummary(params) {
|
|
1777
2615
|
const parts = [`query=${JSON.stringify(truncate(params.query, 80))}`];
|
|
1778
|
-
if (params.
|
|
1779
|
-
parts.push(`
|
|
1780
|
-
}
|
|
1781
|
-
if (params.until) {
|
|
1782
|
-
parts.push(`until=${params.until}`);
|
|
1783
|
-
}
|
|
1784
|
-
if (params.around) {
|
|
1785
|
-
parts.push(`around=${params.around}`);
|
|
1786
|
-
}
|
|
1787
|
-
if (params.aroundRadius !== void 0) {
|
|
1788
|
-
parts.push(`radius=${params.aroundRadius}`);
|
|
1789
|
-
}
|
|
1790
|
-
if (params.project) {
|
|
1791
|
-
parts.push(`project=${JSON.stringify(params.project)}`);
|
|
2616
|
+
if (params.mode) {
|
|
2617
|
+
parts.push(`mode=${params.mode}`);
|
|
1792
2618
|
}
|
|
1793
2619
|
if (params.limit !== void 0 && params.limit !== DEFAULT_RECALL_LIMIT) {
|
|
1794
2620
|
parts.push(`limit=${params.limit}`);
|
|
@@ -1804,15 +2630,11 @@ function formatRecallToolSummary(params) {
|
|
|
1804
2630
|
function sanitizeRecallToolParams(params) {
|
|
1805
2631
|
return {
|
|
1806
2632
|
query: params.query,
|
|
2633
|
+
...params.mode ? { mode: params.mode } : {},
|
|
1807
2634
|
...params.limit !== void 0 ? { limit: params.limit } : {},
|
|
1808
2635
|
...params.threshold !== void 0 ? { threshold: params.threshold } : {},
|
|
1809
2636
|
...params.types.length > 0 ? { types: params.types } : {},
|
|
1810
|
-
...params.tags.length > 0 ? { tags: params.tags } : {}
|
|
1811
|
-
...params.since ? { since: params.since } : {},
|
|
1812
|
-
...params.until ? { until: params.until } : {},
|
|
1813
|
-
...params.around ? { around: params.around } : {},
|
|
1814
|
-
...params.aroundRadius !== void 0 ? { aroundRadius: params.aroundRadius } : {},
|
|
1815
|
-
...params.project ? { project: params.project } : {}
|
|
2637
|
+
...params.tags.length > 0 ? { tags: params.tags } : {}
|
|
1816
2638
|
};
|
|
1817
2639
|
}
|
|
1818
2640
|
function sanitizeRetireToolParams(params) {
|
|
@@ -1837,27 +2659,69 @@ function sanitizeTraceToolParams(params) {
|
|
|
1837
2659
|
...params.last !== void 0 ? { last: params.last } : {}
|
|
1838
2660
|
};
|
|
1839
2661
|
}
|
|
1840
|
-
function
|
|
1841
|
-
const lines = [
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
2662
|
+
function formatUnifiedRecallResults(result) {
|
|
2663
|
+
const lines = [
|
|
2664
|
+
"Recall Route",
|
|
2665
|
+
`requested=${result.routing.requested} detected=${result.routing.detectedIntent} queried=${result.routing.queried.join(", ") || "none"}`,
|
|
2666
|
+
result.routing.reason,
|
|
2667
|
+
""
|
|
2668
|
+
];
|
|
2669
|
+
if (result.timeWindow) {
|
|
2670
|
+
lines.push("Resolved Time Window");
|
|
2671
|
+
lines.push(`${result.timeWindow.start} -> ${result.timeWindow.end} (${result.timeWindow.timezone}) from ${JSON.stringify(result.timeWindow.resolvedFrom)}`);
|
|
2672
|
+
lines.push("");
|
|
2673
|
+
}
|
|
2674
|
+
lines.push("Episode Matches");
|
|
2675
|
+
if (result.episodes.length === 0) {
|
|
2676
|
+
lines.push("None.");
|
|
2677
|
+
} else {
|
|
2678
|
+
for (const [index, episode] of result.episodes.entries()) {
|
|
2679
|
+
lines.push(
|
|
2680
|
+
`${index + 1}. ${episode.episode.id} | ${episode.episode.source} | ${episode.episode.startedAt} -> ${episode.episode.endedAt ?? episode.episode.startedAt} | score ${episode.score.toFixed(2)}`
|
|
2681
|
+
);
|
|
2682
|
+
lines.push(` ${index < 3 ? episode.episode.summary.trim() : truncate(episode.episode.summary.trim(), 220)}`);
|
|
2683
|
+
lines.push(` why_matched=${describeEpisodeMatch(episode)}`);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
lines.push("");
|
|
2687
|
+
lines.push("Entry Matches");
|
|
2688
|
+
if (result.entries.length === 0) {
|
|
2689
|
+
lines.push("None.");
|
|
2690
|
+
} else {
|
|
2691
|
+
for (const [index, entry] of result.entries.entries()) {
|
|
2692
|
+
lines.push(
|
|
2693
|
+
`${index + 1}. ${entry.entry.id} | ${entry.entry.type} | ${entry.entry.subject} | score ${entry.score.toFixed(2)} | importance ${entry.entry.importance}`
|
|
2694
|
+
);
|
|
2695
|
+
lines.push(` ${truncate(entry.entry.content, 220)}`);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
if (result.notices.length > 0) {
|
|
2699
|
+
lines.push("");
|
|
2700
|
+
lines.push("Notices");
|
|
2701
|
+
for (const notice of result.notices) {
|
|
2702
|
+
lines.push(`- ${notice}`);
|
|
2703
|
+
}
|
|
1847
2704
|
}
|
|
1848
2705
|
return lines.join("\n");
|
|
1849
2706
|
}
|
|
1850
|
-
function
|
|
1851
|
-
const
|
|
1852
|
-
|
|
1853
|
-
|
|
2707
|
+
function formatUnifiedRecallLogSummary(result) {
|
|
2708
|
+
const entrySubjects = result.entries.map((entry) => entry.entry.subject.trim()).filter((subject) => subject.length > 0);
|
|
2709
|
+
const displayed = entrySubjects.slice(0, RESULT_SUBJECT_LOG_LIMIT).map((subject) => JSON.stringify(truncate(subject, 80)));
|
|
2710
|
+
const remaining = entrySubjects.length - RESULT_SUBJECT_LOG_LIMIT;
|
|
2711
|
+
const suffix = displayed.length === 0 ? "" : ` [entry subjects: ${displayed.join(", ")}${remaining > 0 ? `, ... and ${remaining} more` : ""}]`;
|
|
2712
|
+
return `${result.episodes.length} episode${result.episodes.length === 1 ? "" : "s"}, ${result.entries.length} entr${result.entries.length === 1 ? "y" : "ies"}${suffix}`;
|
|
2713
|
+
}
|
|
2714
|
+
function describeEpisodeMatch(result) {
|
|
2715
|
+
if (result.scores.semantic > 0 && result.scores.temporal > 0) {
|
|
2716
|
+
return "Semantic match within the resolved time window.";
|
|
2717
|
+
}
|
|
2718
|
+
if (result.scores.semantic > 0) {
|
|
2719
|
+
return "Semantic match to the episode summary.";
|
|
1854
2720
|
}
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
if (remaining > 0) {
|
|
1858
|
-
displayed.push(`... and ${remaining} more`);
|
|
2721
|
+
if (result.scores.temporal > 0) {
|
|
2722
|
+
return "Session overlaps the resolved time window.";
|
|
1859
2723
|
}
|
|
1860
|
-
return
|
|
2724
|
+
return "Matched episodic recall ranking.";
|
|
1861
2725
|
}
|
|
1862
2726
|
function formatTrace(entry, supersededBy, supersedes, recallEvents) {
|
|
1863
2727
|
const lines = [
|
|
@@ -2014,9 +2878,10 @@ function buildAgenrMemoryPromptSection({
|
|
|
2014
2878
|
const lines = [
|
|
2015
2879
|
"## Memory Recall",
|
|
2016
2880
|
"Before answering anything about prior work, decisions, preferences, people, dates, unfinished work, or past sessions, call agenr_recall first. Session-start recall is automatic; use agenr_recall mid-session when you need context you do not already have.",
|
|
2017
|
-
"agenr_recall supports
|
|
2018
|
-
|
|
2019
|
-
|
|
2881
|
+
"agenr_recall supports two recall kinds behind one tool: use mode=entries for exact facts, decisions, thresholds, and versions; use mode=episodes or auto for what-happened questions tied to a time period.",
|
|
2882
|
+
"For temporal narrative questions, put the time phrase in the query itself: examples include yesterday, last week, this month, 2 weeks ago, or in March.",
|
|
2883
|
+
"Episode results are narrative summaries of completed prior sessions, not authoritative logs. Confirm exact details when precision matters.",
|
|
2884
|
+
"The newest completed session may not be consolidated into episodes yet, so very recent work can be missing from episode recall."
|
|
2020
2885
|
];
|
|
2021
2886
|
if (availableTools.has(MEMORY_TOOL_NAMES.store)) {
|
|
2022
2887
|
lines.push(
|
|
@@ -2041,476 +2906,193 @@ function buildAgenrMemoryPromptSection({
|
|
|
2041
2906
|
return lines;
|
|
2042
2907
|
}
|
|
2043
2908
|
|
|
2044
|
-
// ../../src/adapters/openclaw/
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2909
|
+
// ../../src/adapters/openclaw/episode/episode-writer.ts
|
|
2910
|
+
import { promises as fs3 } from "fs";
|
|
2911
|
+
import os from "os";
|
|
2912
|
+
import path from "path";
|
|
2913
|
+
import { DEFAULT_MODEL, DEFAULT_PROVIDER, parseModelRef, resolveAgentEffectiveModelPrimary, resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
|
|
2914
|
+
|
|
2915
|
+
// ../../src/core/episode/summary-prompt.ts
|
|
2916
|
+
var EPISODE_SUMMARY_SYSTEM_PROMPT = [
|
|
2917
|
+
"You write strict JSON episode summaries for historical recall.",
|
|
2918
|
+
"The transcript can be about any topic - technical work, casual conversation, planning, research, creative projects, life events, or anything else.",
|
|
2919
|
+
"Do not assume any particular domain.",
|
|
2920
|
+
"Describe only what happened in this session.",
|
|
2921
|
+
"Do not carry inherited context or open loops forward unless the session actively worked on them.",
|
|
2922
|
+
"Return exactly one JSON object with this shape:",
|
|
2923
|
+
'{ "summary": string, "tags": string[], "activityLevel": "substantial" | "minimal" | "none", "project": string | null }',
|
|
2924
|
+
"Requirements:",
|
|
2925
|
+
"- summary must be 100 to 300 words in plain prose (roughly 4 to 10 sentences)",
|
|
2926
|
+
"- describe what was discussed, decided, or accomplished - not a turn-by-turn replay",
|
|
2927
|
+
"- preserve concrete details worth remembering: names, places, dates, specific decisions, key topics, and notable specifics that would help someone recall this session months later",
|
|
2928
|
+
"- tags must be 3 to 8 short lowercase anchors drawn from the actual session content",
|
|
2929
|
+
"- project should be null when no clear project scope appears",
|
|
2930
|
+
"- activityLevel: use substantial when meaningful discussion or work occurred, minimal when the session was brief or lightweight, none when essentially nothing happened",
|
|
2931
|
+
"- do not include Markdown fences or extra commentary"
|
|
2932
|
+
].join("\n");
|
|
2933
|
+
function buildEpisodeSummaryPrompt(transcript) {
|
|
2934
|
+
return [
|
|
2935
|
+
"Produce a historical episodic summary for this completed session.",
|
|
2936
|
+
"Describe what was discussed, decided, or accomplished during this transcript window.",
|
|
2937
|
+
"",
|
|
2938
|
+
"Transcript:",
|
|
2939
|
+
transcript
|
|
2940
|
+
].join("\n");
|
|
2941
|
+
}
|
|
2942
|
+
function parseEpisodeSummaryResponse(value) {
|
|
2943
|
+
const parsed = parseJsonObject(value);
|
|
2944
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2945
|
+
return null;
|
|
2050
2946
|
}
|
|
2051
|
-
const
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
lines.push(formatEntryBody(item.entry));
|
|
2057
|
-
}
|
|
2058
|
-
lines.push("");
|
|
2947
|
+
const parsedRecord = parsed;
|
|
2948
|
+
const summary = normalizeSummary(parsedRecord.summary);
|
|
2949
|
+
const activityLevel = normalizeActivityLevel(parsedRecord.activityLevel);
|
|
2950
|
+
if (!summary || !activityLevel) {
|
|
2951
|
+
return null;
|
|
2059
2952
|
}
|
|
2060
|
-
return
|
|
2953
|
+
return {
|
|
2954
|
+
summary,
|
|
2955
|
+
tags: normalizeTags2(parsedRecord.tags),
|
|
2956
|
+
activityLevel,
|
|
2957
|
+
...normalizeProject(parsedRecord.project) ? { project: normalizeProject(parsedRecord.project) } : {}
|
|
2958
|
+
};
|
|
2061
2959
|
}
|
|
2062
|
-
function
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
if (coreEntries.length > 0) {
|
|
2066
|
-
sections.push({ title: "Core Memory", entries: coreEntries });
|
|
2960
|
+
function normalizeSummary(value) {
|
|
2961
|
+
if (typeof value !== "string") {
|
|
2962
|
+
return null;
|
|
2067
2963
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
function formatEntryHeader(item) {
|
|
2071
|
-
const metadata = [
|
|
2072
|
-
item.entry.id,
|
|
2073
|
-
item.entry.type,
|
|
2074
|
-
item.entry.expiry,
|
|
2075
|
-
`importance ${item.entry.importance}`,
|
|
2076
|
-
item.score !== void 0 ? `score ${item.score.toFixed(2)}` : void 0
|
|
2077
|
-
].filter((value) => value !== void 0);
|
|
2078
|
-
return `- [${metadata.join(" | ")}] ${item.entry.subject}`;
|
|
2964
|
+
const normalized = value.replace(/\s+/gu, " ").trim();
|
|
2965
|
+
return normalized ? normalized : null;
|
|
2079
2966
|
}
|
|
2080
|
-
function
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
entry.tags.length > 0 ? `tags: ${entry.tags.join(", ")}` : void 0,
|
|
2084
|
-
entry.created_at ? `created: ${entry.created_at.slice(0, 10)}` : void 0
|
|
2085
|
-
].filter((value) => value !== void 0);
|
|
2086
|
-
if (extra.length === 0) {
|
|
2087
|
-
return ` ${content}`;
|
|
2967
|
+
function normalizeActivityLevel(value) {
|
|
2968
|
+
if (typeof value !== "string") {
|
|
2969
|
+
return null;
|
|
2088
2970
|
}
|
|
2089
|
-
|
|
2971
|
+
const normalized = value.trim().toLowerCase();
|
|
2972
|
+
return EPISODE_ACTIVITY_LEVELS.includes(normalized) ? normalized : null;
|
|
2090
2973
|
}
|
|
2091
|
-
function
|
|
2092
|
-
if (value
|
|
2093
|
-
return
|
|
2974
|
+
function normalizeTags2(value) {
|
|
2975
|
+
if (!Array.isArray(value)) {
|
|
2976
|
+
return [];
|
|
2094
2977
|
}
|
|
2095
|
-
return
|
|
2978
|
+
return Array.from(
|
|
2979
|
+
new Set(
|
|
2980
|
+
value.filter((tag) => typeof tag === "string").map((tag) => tag.trim().toLowerCase()).filter((tag) => tag.length > 0)
|
|
2981
|
+
)
|
|
2982
|
+
).slice(0, 8);
|
|
2096
2983
|
}
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
// ../../src/adapters/openclaw/session/sessions-store-reader.ts
|
|
2102
|
-
import { promises as fs } from "fs";
|
|
2103
|
-
import path from "path";
|
|
2104
|
-
async function readOpenClawSessionsStore(sessionsDir, logger) {
|
|
2105
|
-
const normalizedSessionsDir = sessionsDir.trim();
|
|
2106
|
-
if (normalizedSessionsDir.length === 0) {
|
|
2107
|
-
debugLog(logger, "sessions-store-reader", "skipping sessions.json read because sessionsDir is empty");
|
|
2108
|
-
return [];
|
|
2984
|
+
function normalizeProject(value) {
|
|
2985
|
+
if (typeof value !== "string") {
|
|
2986
|
+
return void 0;
|
|
2109
2987
|
}
|
|
2110
|
-
const
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
return
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
for (const [sessionKey, value] of Object.entries(parsed)) {
|
|
2121
|
-
const normalizedSessionKey = sessionKey.trim();
|
|
2122
|
-
if (normalizedSessionKey.length === 0) {
|
|
2123
|
-
debugLog(logger, "sessions-store-reader", `skipping blank session key in ${sessionsJsonPath}`);
|
|
2124
|
-
continue;
|
|
2125
|
-
}
|
|
2126
|
-
if (!isRecord2(value)) {
|
|
2127
|
-
debugLog(logger, "sessions-store-reader", `skipping non-object entry for key=${normalizedSessionKey}`);
|
|
2128
|
-
continue;
|
|
2129
|
-
}
|
|
2130
|
-
const sessionId = asTrimmedString(value["sessionId"]);
|
|
2131
|
-
const sessionFile = asTrimmedString(value["sessionFile"]);
|
|
2132
|
-
const updatedAt = asFiniteNumber(value["updatedAt"]);
|
|
2133
|
-
entries.push({
|
|
2134
|
-
sessionKey: normalizedSessionKey,
|
|
2135
|
-
...sessionId ? { sessionId } : {},
|
|
2136
|
-
...sessionFile ? { sessionFile: resolveSessionStorePath(sessionFile, resolvedSessionsDir) } : {},
|
|
2137
|
-
...updatedAt !== void 0 ? { updatedAt } : {}
|
|
2138
|
-
});
|
|
2139
|
-
}
|
|
2140
|
-
debugLog(logger, "sessions-store-reader", `loaded sessions.json entries=${entries.length} path=${sessionsJsonPath}`);
|
|
2141
|
-
return entries;
|
|
2142
|
-
} catch (error) {
|
|
2143
|
-
if (isFileNotFound(error)) {
|
|
2144
|
-
debugLog(logger, "sessions-store-reader", `sessions.json missing at ${sessionsJsonPath}`);
|
|
2145
|
-
return [];
|
|
2988
|
+
const normalized = value.replace(/\s+/gu, " ").trim();
|
|
2989
|
+
return normalized ? normalized : void 0;
|
|
2990
|
+
}
|
|
2991
|
+
function parseJsonObject(value) {
|
|
2992
|
+
const candidates = collectJsonCandidates(value);
|
|
2993
|
+
for (const candidate of candidates) {
|
|
2994
|
+
try {
|
|
2995
|
+
return JSON.parse(candidate);
|
|
2996
|
+
} catch {
|
|
2997
|
+
continue;
|
|
2146
2998
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2999
|
+
}
|
|
3000
|
+
return null;
|
|
3001
|
+
}
|
|
3002
|
+
function collectJsonCandidates(value) {
|
|
3003
|
+
const trimmed = value.trim();
|
|
3004
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
3005
|
+
if (trimmed) {
|
|
3006
|
+
candidates.add(trimmed);
|
|
3007
|
+
}
|
|
3008
|
+
const fencedMatches = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/giu) ?? [];
|
|
3009
|
+
for (const match of fencedMatches) {
|
|
3010
|
+
const normalized = match.replace(/```(?:json)?/iu, "").replace(/```/gu, "").trim();
|
|
3011
|
+
if (normalized) {
|
|
3012
|
+
candidates.add(normalized);
|
|
2150
3013
|
}
|
|
2151
|
-
debugLog(logger, "sessions-store-reader", `sessions.json read failed at ${sessionsJsonPath}: ${formatErrorMessage2(error)}`);
|
|
2152
|
-
return [];
|
|
2153
3014
|
}
|
|
3015
|
+
const objectStart = trimmed.indexOf("{");
|
|
3016
|
+
const objectEnd = trimmed.lastIndexOf("}");
|
|
3017
|
+
if (objectStart >= 0 && objectEnd > objectStart) {
|
|
3018
|
+
candidates.add(trimmed.slice(objectStart, objectEnd + 1));
|
|
3019
|
+
}
|
|
3020
|
+
return [...candidates];
|
|
2154
3021
|
}
|
|
2155
|
-
|
|
2156
|
-
|
|
3022
|
+
|
|
3023
|
+
// ../../src/core/episode/summary-generator.ts
|
|
3024
|
+
async function generateEpisodeSummary(transcript, llm) {
|
|
3025
|
+
const response = await llm.complete(EPISODE_SUMMARY_SYSTEM_PROMPT, buildEpisodeSummaryPrompt(transcript));
|
|
3026
|
+
return parseEpisodeSummaryResponse(response);
|
|
2157
3027
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
3028
|
+
|
|
3029
|
+
// ../../src/adapters/openclaw/logging.ts
|
|
3030
|
+
function formatSessionContext(sessionId, sessionKey) {
|
|
3031
|
+
const normalizedSessionId = sessionId?.trim();
|
|
3032
|
+
const normalizedSessionKey = sessionKey?.trim();
|
|
3033
|
+
if (normalizedSessionId && normalizedSessionKey) {
|
|
3034
|
+
return `session=${normalizedSessionId} key=${normalizedSessionKey}`;
|
|
3035
|
+
}
|
|
3036
|
+
if (normalizedSessionId) {
|
|
3037
|
+
return `session=${normalizedSessionId}`;
|
|
3038
|
+
}
|
|
3039
|
+
if (normalizedSessionKey) {
|
|
3040
|
+
return `key=${normalizedSessionKey}`;
|
|
3041
|
+
}
|
|
3042
|
+
return "session=unknown";
|
|
2160
3043
|
}
|
|
2161
|
-
function
|
|
2162
|
-
return
|
|
3044
|
+
function formatErrorMessage2(error) {
|
|
3045
|
+
return error instanceof Error ? error.message : String(error);
|
|
2163
3046
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
3047
|
+
|
|
3048
|
+
// ../../src/adapters/openclaw/transcript/parser.ts
|
|
3049
|
+
import { createHash as createHash2 } from "crypto";
|
|
3050
|
+
import { promises as fs2 } from "fs";
|
|
3051
|
+
|
|
3052
|
+
// ../../src/adapters/openclaw/transcript/jsonl.ts
|
|
3053
|
+
function parseJsonlLines(raw, warnings, onRecord) {
|
|
3054
|
+
const lines = raw.split(/\r?\n/);
|
|
3055
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
3056
|
+
const line = lines[index]?.trim();
|
|
3057
|
+
if (!line) {
|
|
3058
|
+
continue;
|
|
3059
|
+
}
|
|
3060
|
+
let parsed;
|
|
3061
|
+
try {
|
|
3062
|
+
parsed = JSON.parse(line);
|
|
3063
|
+
} catch {
|
|
3064
|
+
warnings.push(`Skipped malformed JSONL line ${index + 1}`);
|
|
3065
|
+
continue;
|
|
3066
|
+
}
|
|
3067
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3068
|
+
continue;
|
|
3069
|
+
}
|
|
3070
|
+
onRecord(parsed, index + 1);
|
|
3071
|
+
}
|
|
2166
3072
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
3073
|
+
|
|
3074
|
+
// ../../src/adapters/openclaw/transcript/tool-summarization.ts
|
|
3075
|
+
var DEFAULT_TOOL_RESULT_DROP_NAMES = ["read", "web_fetch", "browser", "screenshot", "snapshot", "canvas", "tts"];
|
|
3076
|
+
var DEFAULT_TOOL_RESULT_KEEP_NAMES = ["web_search", "memory_search", "memory_get", "image"];
|
|
3077
|
+
var DEFAULT_TOOL_RESULT_DROP_NAME_SET = new Set(DEFAULT_TOOL_RESULT_DROP_NAMES);
|
|
3078
|
+
var DEFAULT_TOOL_RESULT_KEEP_NAME_SET = new Set(DEFAULT_TOOL_RESULT_KEEP_NAMES);
|
|
3079
|
+
function asRecord2(value) {
|
|
3080
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2169
3081
|
}
|
|
2170
|
-
function
|
|
2171
|
-
return typeof
|
|
3082
|
+
function getString(value) {
|
|
3083
|
+
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
2172
3084
|
}
|
|
2173
|
-
function
|
|
2174
|
-
if (
|
|
2175
|
-
return
|
|
3085
|
+
function truncateInline(value, max) {
|
|
3086
|
+
if (value.length <= max) {
|
|
3087
|
+
return value;
|
|
2176
3088
|
}
|
|
2177
|
-
return
|
|
3089
|
+
return value.slice(0, max);
|
|
2178
3090
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
function parseTuiSessionKey(sessionKey) {
|
|
2185
|
-
const normalizedSessionKey = sessionKey.trim();
|
|
2186
|
-
if (normalizedSessionKey.length === 0) {
|
|
2187
|
-
return null;
|
|
2188
|
-
}
|
|
2189
|
-
const match = TUI_SESSION_KEY_PATTERN.exec(normalizedSessionKey);
|
|
2190
|
-
if (!match) {
|
|
2191
|
-
return null;
|
|
2192
|
-
}
|
|
2193
|
-
const [, agentId, instanceLane] = match;
|
|
2194
|
-
const normalizedAgentId = agentId?.trim();
|
|
2195
|
-
const normalizedInstanceLane = instanceLane?.trim();
|
|
2196
|
-
if (!normalizedAgentId || !normalizedInstanceLane || !normalizedInstanceLane.toLowerCase().startsWith("tui")) {
|
|
2197
|
-
return null;
|
|
2198
|
-
}
|
|
2199
|
-
const stableLane = TUI_UUID_LANE_PATTERN.test(normalizedInstanceLane) ? normalizedInstanceLane.replace(TUI_UUID_SUFFIX_PATTERN, "") : normalizedInstanceLane;
|
|
2200
|
-
return {
|
|
2201
|
-
agentId: normalizedAgentId,
|
|
2202
|
-
stableLane,
|
|
2203
|
-
instanceLane: normalizedInstanceLane
|
|
2204
|
-
};
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
// ../../src/adapters/openclaw/session/predecessor.ts
|
|
2208
|
-
async function resolveOpenClawSessionPredecessor(ctx, tracker, params) {
|
|
2209
|
-
const sessionContext = formatSessionContext(ctx.sessionId, ctx.sessionKey);
|
|
2210
|
-
debugLog2(params.logger, "predecessor", `resolving predecessor for ${sessionContext}`);
|
|
2211
|
-
const trackedPredecessor = resolveTrackedPredecessor(ctx, tracker, params.logger);
|
|
2212
|
-
if (trackedPredecessor) {
|
|
2213
|
-
return trackedPredecessor;
|
|
2214
|
-
}
|
|
2215
|
-
const tuiIdentity = parseTuiSessionKey(ctx.sessionKey ?? "");
|
|
2216
|
-
if (!tuiIdentity) {
|
|
2217
|
-
debugLog2(params.logger, "predecessor", `skipping TUI fallback for ${sessionContext}: current session key is not TUI`);
|
|
2218
|
-
return void 0;
|
|
2219
|
-
}
|
|
2220
|
-
const sessionsDir = resolveOpenClawSessionsDirectory(ctx, tuiIdentity.agentId, params.resolveStateDir);
|
|
2221
|
-
if (!sessionsDir) {
|
|
2222
|
-
params.logger?.info?.(`[agenr] predecessor: TUI fallback no predecessor found for ${sessionContext} reason=no_sessions_dir`);
|
|
2223
|
-
return void 0;
|
|
2224
|
-
}
|
|
2225
|
-
params.logger?.info?.(
|
|
2226
|
-
`[agenr] predecessor: TUI fallback activated for ${sessionContext} sessionKey=${ctx.sessionKey?.trim() ?? "unknown"} stableLane=${tuiIdentity.stableLane}`
|
|
2227
|
-
);
|
|
2228
|
-
debugLog2(
|
|
2229
|
-
params.logger,
|
|
2230
|
-
"predecessor",
|
|
2231
|
-
`TUI fallback stable lane for ${sessionContext}: agentId=${tuiIdentity.agentId} instanceLane=${tuiIdentity.instanceLane} stableLane=${tuiIdentity.stableLane} sessionsDir=${sessionsDir}`
|
|
2232
|
-
);
|
|
2233
|
-
const fallbackResolution = await findTuiFallbackPredecessor(ctx.sessionKey ?? "", sessionsDir, params.logger);
|
|
2234
|
-
if (!fallbackResolution.predecessor) {
|
|
2235
|
-
params.logger?.info?.(`[agenr] predecessor: TUI fallback no predecessor found for ${sessionContext} reason=${fallbackResolution.reason}`);
|
|
2236
|
-
return void 0;
|
|
2237
|
-
}
|
|
2238
|
-
params.logger?.info?.(
|
|
2239
|
-
`[agenr] predecessor: TUI fallback predecessor found for ${sessionContext} predecessorKey=${fallbackResolution.predecessor.sessionKey} predecessor=${fallbackResolution.predecessor.sessionFile}`
|
|
2240
|
-
);
|
|
2241
|
-
return {
|
|
2242
|
-
sessionFile: fallbackResolution.predecessor.sessionFile,
|
|
2243
|
-
...fallbackResolution.predecessor.sessionId ? { sessionId: fallbackResolution.predecessor.sessionId } : {}
|
|
2244
|
-
};
|
|
2245
|
-
}
|
|
2246
|
-
function resolveTrackedPredecessor(ctx, tracker, logger) {
|
|
2247
|
-
const sessionContext = formatSessionContext(ctx.sessionId, ctx.sessionKey);
|
|
2248
|
-
const resetRecord = tracker.getLatestReset(ctx.sessionKey);
|
|
2249
|
-
if (!resetRecord) {
|
|
2250
|
-
debugLog2(logger, "predecessor", `no reset record found for ${sessionContext}`);
|
|
2251
|
-
return void 0;
|
|
2252
|
-
}
|
|
2253
|
-
debugLog2(logger, "predecessor", `latest reset record for ${sessionContext}: ${formatResetRecord(resetRecord)}`);
|
|
2254
|
-
const resumedFrom = tracker.getResumedFrom(ctx.sessionId);
|
|
2255
|
-
if (resumedFrom) {
|
|
2256
|
-
debugLog2(logger, "predecessor", `session_start resumedFrom for ${sessionContext}: ${resumedFrom}`);
|
|
2257
|
-
} else {
|
|
2258
|
-
debugLog2(logger, "predecessor", `session_start resumedFrom unavailable for ${sessionContext}`);
|
|
2259
|
-
}
|
|
2260
|
-
if (resumedFrom && resetRecord.sessionId && resetRecord.sessionId !== resumedFrom) {
|
|
2261
|
-
debugLog2(
|
|
2262
|
-
logger,
|
|
2263
|
-
"predecessor",
|
|
2264
|
-
`discarding stale reset record for ${sessionContext}: resumedFrom=${resumedFrom} resetRecordSession=${resetRecord.sessionId}`
|
|
2265
|
-
);
|
|
2266
|
-
return void 0;
|
|
2267
|
-
}
|
|
2268
|
-
return {
|
|
2269
|
-
sessionFile: resetRecord.sessionFile,
|
|
2270
|
-
...resetRecord.sessionId ? { sessionId: resetRecord.sessionId } : {}
|
|
2271
|
-
};
|
|
2272
|
-
}
|
|
2273
|
-
async function findTuiFallbackPredecessor(currentSessionKey, sessionsDir, logger) {
|
|
2274
|
-
const currentIdentity = parseTuiSessionKey(currentSessionKey);
|
|
2275
|
-
if (!currentIdentity) {
|
|
2276
|
-
return { reason: "not_tui_session_key" };
|
|
2277
|
-
}
|
|
2278
|
-
const entries = await readOpenClawSessionsStore(sessionsDir, logger);
|
|
2279
|
-
debugLog2(logger, "predecessor", `TUI fallback sessions.json read result for sessionKey=${currentSessionKey}: entries=${entries.length}`);
|
|
2280
|
-
const sameAgentEntries = entries.filter((entry) => {
|
|
2281
|
-
const parsedCandidate = parseSingleLaneSessionKey(entry.sessionKey);
|
|
2282
|
-
if (!parsedCandidate) {
|
|
2283
|
-
debugLog2(logger, "predecessor", `TUI fallback excluded candidate=${entry.sessionKey} reason=unsupported_session_key_shape`);
|
|
2284
|
-
return false;
|
|
2285
|
-
}
|
|
2286
|
-
if (parsedCandidate.agentId !== currentIdentity.agentId) {
|
|
2287
|
-
debugLog2(
|
|
2288
|
-
logger,
|
|
2289
|
-
"predecessor",
|
|
2290
|
-
`TUI fallback excluded candidate=${entry.sessionKey} reason=agent_mismatch expected=${currentIdentity.agentId} actual=${parsedCandidate.agentId}`
|
|
2291
|
-
);
|
|
2292
|
-
return false;
|
|
2293
|
-
}
|
|
2294
|
-
return true;
|
|
2295
|
-
});
|
|
2296
|
-
debugLog2(logger, "predecessor", `TUI fallback candidate filtering for sessionKey=${currentSessionKey}: sameAgentCount=${sameAgentEntries.length}`);
|
|
2297
|
-
const laneMatches = sameAgentEntries.filter((entry) => {
|
|
2298
|
-
const normalizedCandidateKey = entry.sessionKey.trim();
|
|
2299
|
-
if (normalizedCandidateKey === currentSessionKey.trim()) {
|
|
2300
|
-
debugLog2(logger, "predecessor", `TUI fallback excluded candidate=${entry.sessionKey} reason=current_session`);
|
|
2301
|
-
return false;
|
|
2302
|
-
}
|
|
2303
|
-
const candidateKey = parseSingleLaneSessionKey(entry.sessionKey);
|
|
2304
|
-
if (!candidateKey) {
|
|
2305
|
-
return false;
|
|
2306
|
-
}
|
|
2307
|
-
if (currentIdentity.stableLane === "tui" && candidateKey.lane === "main") {
|
|
2308
|
-
return true;
|
|
2309
|
-
}
|
|
2310
|
-
const candidateIdentity = parseTuiSessionKey(entry.sessionKey);
|
|
2311
|
-
if (!candidateIdentity) {
|
|
2312
|
-
debugLog2(logger, "predecessor", `TUI fallback excluded candidate=${entry.sessionKey} reason=not_tui_candidate`);
|
|
2313
|
-
return false;
|
|
2314
|
-
}
|
|
2315
|
-
if (!isSameTuiFallbackLane(currentIdentity.stableLane, candidateIdentity.stableLane)) {
|
|
2316
|
-
debugLog2(
|
|
2317
|
-
logger,
|
|
2318
|
-
"predecessor",
|
|
2319
|
-
`TUI fallback excluded candidate=${entry.sessionKey} reason=lane_mismatch currentStableLane=${currentIdentity.stableLane} candidateStableLane=${candidateIdentity.stableLane}`
|
|
2320
|
-
);
|
|
2321
|
-
return false;
|
|
2322
|
-
}
|
|
2323
|
-
return true;
|
|
2324
|
-
});
|
|
2325
|
-
debugLog2(logger, "predecessor", `TUI fallback candidate filtering for sessionKey=${currentSessionKey}: laneMatchCount=${laneMatches.length}`);
|
|
2326
|
-
const sortedCandidates = laneMatches.filter((entry) => {
|
|
2327
|
-
if (entry.updatedAt !== void 0) {
|
|
2328
|
-
return true;
|
|
2329
|
-
}
|
|
2330
|
-
debugLog2(logger, "predecessor", `TUI fallback excluded candidate=${entry.sessionKey} reason=missing_updated_at`);
|
|
2331
|
-
return false;
|
|
2332
|
-
}).sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0));
|
|
2333
|
-
if (sortedCandidates.length === 0) {
|
|
2334
|
-
return { reason: "no_matching_sessions" };
|
|
2335
|
-
}
|
|
2336
|
-
const predecessor = sortedCandidates[0];
|
|
2337
|
-
if (!predecessor.sessionFile?.trim()) {
|
|
2338
|
-
debugLog2(
|
|
2339
|
-
logger,
|
|
2340
|
-
"predecessor",
|
|
2341
|
-
`TUI fallback top candidate missing session file for sessionKey=${currentSessionKey}: predecessorKey=${predecessor.sessionKey}`
|
|
2342
|
-
);
|
|
2343
|
-
return { reason: "missing_session_file" };
|
|
2344
|
-
}
|
|
2345
|
-
return {
|
|
2346
|
-
reason: "resolved",
|
|
2347
|
-
predecessor: {
|
|
2348
|
-
sessionFile: predecessor.sessionFile,
|
|
2349
|
-
...predecessor.sessionId ? { sessionId: predecessor.sessionId } : {},
|
|
2350
|
-
sessionKey: predecessor.sessionKey
|
|
2351
|
-
}
|
|
2352
|
-
};
|
|
2353
|
-
}
|
|
2354
|
-
function resolveOpenClawSessionsDirectory(ctx, fallbackAgentId, resolveStateDir) {
|
|
2355
|
-
const agentId = ctx.agentId?.trim() || fallbackAgentId.trim();
|
|
2356
|
-
if (!agentId) {
|
|
2357
|
-
return void 0;
|
|
2358
|
-
}
|
|
2359
|
-
return path2.join(resolveStateDir(process.env), "agents", agentId, "sessions");
|
|
2360
|
-
}
|
|
2361
|
-
function parseSingleLaneSessionKey(sessionKey) {
|
|
2362
|
-
const match = /^agent:([^:]+):([^:]+)$/i.exec(sessionKey.trim());
|
|
2363
|
-
if (!match) {
|
|
2364
|
-
return null;
|
|
2365
|
-
}
|
|
2366
|
-
const [, agentId, lane] = match;
|
|
2367
|
-
const normalizedAgentId = agentId?.trim();
|
|
2368
|
-
const normalizedLane = lane?.trim();
|
|
2369
|
-
if (!normalizedAgentId || !normalizedLane) {
|
|
2370
|
-
return null;
|
|
2371
|
-
}
|
|
2372
|
-
return {
|
|
2373
|
-
agentId: normalizedAgentId,
|
|
2374
|
-
lane: normalizedLane
|
|
2375
|
-
};
|
|
2376
|
-
}
|
|
2377
|
-
function isSameTuiFallbackLane(currentStableLane, candidateStableLane) {
|
|
2378
|
-
if (currentStableLane === "tui") {
|
|
2379
|
-
return candidateStableLane.toLowerCase().startsWith("tui");
|
|
2380
|
-
}
|
|
2381
|
-
return currentStableLane === candidateStableLane;
|
|
2382
|
-
}
|
|
2383
|
-
function debugLog2(logger, subsystem, message) {
|
|
2384
|
-
logger?.debug?.(`[agenr] ${subsystem}: ${message}`);
|
|
2385
|
-
}
|
|
2386
|
-
function formatSessionContext(sessionId, sessionKey) {
|
|
2387
|
-
const normalizedSessionId = sessionId?.trim();
|
|
2388
|
-
const normalizedSessionKey = sessionKey?.trim();
|
|
2389
|
-
if (normalizedSessionId && normalizedSessionKey) {
|
|
2390
|
-
return `session=${normalizedSessionId} key=${normalizedSessionKey}`;
|
|
2391
|
-
}
|
|
2392
|
-
if (normalizedSessionId) {
|
|
2393
|
-
return `session=${normalizedSessionId}`;
|
|
2394
|
-
}
|
|
2395
|
-
if (normalizedSessionKey) {
|
|
2396
|
-
return `key=${normalizedSessionKey}`;
|
|
2397
|
-
}
|
|
2398
|
-
return "session=unknown";
|
|
2399
|
-
}
|
|
2400
|
-
function formatResetRecord(record) {
|
|
2401
|
-
return `sessionFile=${record.sessionFile}${record.sessionId ? ` sessionId=${record.sessionId}` : ""} recordedAt=${record.recordedAt}`;
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
|
-
// ../../src/adapters/openclaw/session/summary-reader.ts
|
|
2405
|
-
import { promises as fs2 } from "fs";
|
|
2406
|
-
import path3 from "path";
|
|
2407
|
-
function deriveOpenClawSessionIdFromFilePath(sessionFile, logger) {
|
|
2408
|
-
const normalizedSessionFile = sessionFile.trim();
|
|
2409
|
-
if (normalizedSessionFile.length === 0) {
|
|
2410
|
-
debugLog3(logger, "summary-reader", "cannot derive session id from empty session file path");
|
|
2411
|
-
return void 0;
|
|
2412
|
-
}
|
|
2413
|
-
const fileName = path3.basename(normalizedSessionFile);
|
|
2414
|
-
const sessionId = fileName.replace(/\.jsonl(?:\..*)?$/i, "").trim();
|
|
2415
|
-
debugLog3(logger, "summary-reader", `derived session id "${sessionId || "<empty>"}" from file=${normalizedSessionFile}`);
|
|
2416
|
-
return sessionId.length > 0 ? sessionId : void 0;
|
|
2417
|
-
}
|
|
2418
|
-
function resolveOpenClawSessionSummaryPath(sessionFile, logger) {
|
|
2419
|
-
const normalizedSessionFile = sessionFile.trim();
|
|
2420
|
-
const sessionId = deriveOpenClawSessionIdFromFilePath(normalizedSessionFile, logger);
|
|
2421
|
-
if (!sessionId) {
|
|
2422
|
-
return void 0;
|
|
2423
|
-
}
|
|
2424
|
-
const summaryPath = path3.join(path3.dirname(normalizedSessionFile), `${sessionId}.summary.md`);
|
|
2425
|
-
debugLog3(logger, "summary-reader", `resolved summary path for session=${sessionId}: ${summaryPath}`);
|
|
2426
|
-
return summaryPath;
|
|
2427
|
-
}
|
|
2428
|
-
async function readOpenClawSessionSummaryFile(sessionFile, logger) {
|
|
2429
|
-
const summaryPath = resolveOpenClawSessionSummaryPath(sessionFile, logger);
|
|
2430
|
-
const sessionId = deriveOpenClawSessionIdFromFilePath(sessionFile, logger);
|
|
2431
|
-
if (!summaryPath || !sessionId) {
|
|
2432
|
-
return null;
|
|
2433
|
-
}
|
|
2434
|
-
try {
|
|
2435
|
-
const content = (await fs2.readFile(summaryPath, "utf8")).trim();
|
|
2436
|
-
if (content.length === 0) {
|
|
2437
|
-
debugLog3(logger, "summary-reader", `summary file is empty for session=${sessionId} path=${summaryPath}`);
|
|
2438
|
-
return null;
|
|
2439
|
-
}
|
|
2440
|
-
debugLog3(logger, "summary-reader", `loaded summary file for session=${sessionId} path=${summaryPath} chars=${content.length}`);
|
|
2441
|
-
return {
|
|
2442
|
-
sessionId,
|
|
2443
|
-
summaryPath,
|
|
2444
|
-
content
|
|
2445
|
-
};
|
|
2446
|
-
} catch (error) {
|
|
2447
|
-
if (isFileNotFound2(error)) {
|
|
2448
|
-
debugLog3(logger, "summary-reader", `summary file missing for session=${sessionId} path=${summaryPath}`);
|
|
2449
|
-
return null;
|
|
2450
|
-
}
|
|
2451
|
-
throw error;
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
function debugLog3(logger, subsystem, message) {
|
|
2455
|
-
logger?.debug?.(`[agenr] ${subsystem}: ${message}`);
|
|
2456
|
-
}
|
|
2457
|
-
function isFileNotFound2(error) {
|
|
2458
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2459
|
-
}
|
|
2460
|
-
|
|
2461
|
-
// ../../src/adapters/openclaw/session/summary.ts
|
|
2462
|
-
import { promises as fs5 } from "fs";
|
|
2463
|
-
import os from "os";
|
|
2464
|
-
import path4 from "path";
|
|
2465
|
-
import { DEFAULT_MODEL, DEFAULT_PROVIDER, parseModelRef, resolveAgentEffectiveModelPrimary, resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
|
|
2466
|
-
|
|
2467
|
-
// ../../src/adapters/openclaw/transcript/parser.ts
|
|
2468
|
-
import { promises as fs4 } from "fs";
|
|
2469
|
-
|
|
2470
|
-
// ../../src/adapters/openclaw/transcript/jsonl.ts
|
|
2471
|
-
function parseJsonlLines(raw, warnings, onRecord) {
|
|
2472
|
-
const lines = raw.split(/\r?\n/);
|
|
2473
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
2474
|
-
const line = lines[index]?.trim();
|
|
2475
|
-
if (!line) {
|
|
2476
|
-
continue;
|
|
2477
|
-
}
|
|
2478
|
-
let parsed;
|
|
2479
|
-
try {
|
|
2480
|
-
parsed = JSON.parse(line);
|
|
2481
|
-
} catch {
|
|
2482
|
-
warnings.push(`Skipped malformed JSONL line ${index + 1}`);
|
|
2483
|
-
continue;
|
|
2484
|
-
}
|
|
2485
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2486
|
-
continue;
|
|
2487
|
-
}
|
|
2488
|
-
onRecord(parsed, index + 1);
|
|
2489
|
-
}
|
|
2490
|
-
}
|
|
2491
|
-
|
|
2492
|
-
// ../../src/adapters/openclaw/transcript/tool-summarization.ts
|
|
2493
|
-
var DEFAULT_TOOL_RESULT_DROP_NAMES = ["read", "web_fetch", "browser", "screenshot", "snapshot", "canvas", "tts"];
|
|
2494
|
-
var DEFAULT_TOOL_RESULT_KEEP_NAMES = ["web_search", "memory_search", "memory_get", "image"];
|
|
2495
|
-
var DEFAULT_TOOL_RESULT_DROP_NAME_SET = new Set(DEFAULT_TOOL_RESULT_DROP_NAMES);
|
|
2496
|
-
var DEFAULT_TOOL_RESULT_KEEP_NAME_SET = new Set(DEFAULT_TOOL_RESULT_KEEP_NAMES);
|
|
2497
|
-
function asRecord2(value) {
|
|
2498
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2499
|
-
}
|
|
2500
|
-
function getString(value) {
|
|
2501
|
-
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
2502
|
-
}
|
|
2503
|
-
function truncateInline(value, max) {
|
|
2504
|
-
if (value.length <= max) {
|
|
2505
|
-
return value;
|
|
2506
|
-
}
|
|
2507
|
-
return value.slice(0, max);
|
|
2508
|
-
}
|
|
2509
|
-
function firstStringArgValue(args, max) {
|
|
2510
|
-
for (const value of Object.values(args)) {
|
|
2511
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
2512
|
-
return truncateInline(value.trim(), max);
|
|
2513
|
-
}
|
|
3091
|
+
function firstStringArgValue(args, max) {
|
|
3092
|
+
for (const value of Object.values(args)) {
|
|
3093
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
3094
|
+
return truncateInline(value.trim(), max);
|
|
3095
|
+
}
|
|
2514
3096
|
}
|
|
2515
3097
|
return void 0;
|
|
2516
3098
|
}
|
|
@@ -2895,7 +3477,7 @@ function pushMessage(messages, role, text, timestamp) {
|
|
|
2895
3477
|
}
|
|
2896
3478
|
|
|
2897
3479
|
// ../../src/adapters/openclaw/transcript/timestamps.ts
|
|
2898
|
-
import { promises as
|
|
3480
|
+
import { promises as fs } from "fs";
|
|
2899
3481
|
function parseTimestampValue(value) {
|
|
2900
3482
|
if (typeof value === "string" && value.trim().length > 0) {
|
|
2901
3483
|
const parsed = new Date(value);
|
|
@@ -2923,7 +3505,7 @@ function extractTimestamp(record) {
|
|
|
2923
3505
|
}
|
|
2924
3506
|
async function getFileMtimeTimestamp(filePath) {
|
|
2925
3507
|
try {
|
|
2926
|
-
const stat = await
|
|
3508
|
+
const stat = await fs.stat(filePath);
|
|
2927
3509
|
return parseTimestampValue(stat.mtime.toISOString());
|
|
2928
3510
|
} catch {
|
|
2929
3511
|
return void 0;
|
|
@@ -2957,9 +3539,11 @@ var TOOL_RESULT_POLICY = {
|
|
|
2957
3539
|
keepToolNames: new Set(DEFAULT_TOOL_RESULT_KEEP_NAMES.filter((name) => name !== "image"))
|
|
2958
3540
|
};
|
|
2959
3541
|
var RAW_TEXT_BLOCK_TYPES = /* @__PURE__ */ new Set(["input_text", "output_text", "text"]);
|
|
3542
|
+
var SENDER_METADATA_SENTINEL = "Sender (untrusted metadata):";
|
|
3543
|
+
var CONVERSATION_INFO_SENTINEL = "Conversation info (untrusted metadata):";
|
|
2960
3544
|
var USER_METADATA_PREFIX_SENTINELS = /* @__PURE__ */ new Set([
|
|
2961
|
-
|
|
2962
|
-
|
|
3545
|
+
SENDER_METADATA_SENTINEL,
|
|
3546
|
+
CONVERSATION_INFO_SENTINEL,
|
|
2963
3547
|
"Thread starter (untrusted, for context):",
|
|
2964
3548
|
"Replied message (untrusted, for context):",
|
|
2965
3549
|
"Forwarded message context (untrusted metadata):",
|
|
@@ -2982,7 +3566,10 @@ function createParseState() {
|
|
|
2982
3566
|
modelsUsed: [],
|
|
2983
3567
|
modelsUsedSet: /* @__PURE__ */ new Set(),
|
|
2984
3568
|
pendingToolCalls: [],
|
|
2985
|
-
pendingToolCallsById: /* @__PURE__ */ new Map()
|
|
3569
|
+
pendingToolCallsById: /* @__PURE__ */ new Map(),
|
|
3570
|
+
detectedSurface: null,
|
|
3571
|
+
surfaceDetected: false,
|
|
3572
|
+
firstUserRawText: null
|
|
2986
3573
|
};
|
|
2987
3574
|
}
|
|
2988
3575
|
function extractRawMessageText(content) {
|
|
@@ -3080,12 +3667,105 @@ function addModelUsed(state, value) {
|
|
|
3080
3667
|
state.modelsUsedSet.add(modelId);
|
|
3081
3668
|
state.modelsUsed.push(modelId);
|
|
3082
3669
|
}
|
|
3083
|
-
function
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3670
|
+
function setDetectedSurface(state, surface) {
|
|
3671
|
+
if (state.surfaceDetected || !surface) {
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
3674
|
+
state.detectedSurface = surface;
|
|
3675
|
+
state.surfaceDetected = true;
|
|
3676
|
+
}
|
|
3677
|
+
function readInboundSurface(record) {
|
|
3678
|
+
const inboundMeta = asRecord2(record.inbound_meta);
|
|
3679
|
+
const surface = getString(inboundMeta?.surface)?.trim().toLowerCase();
|
|
3680
|
+
return surface || null;
|
|
3681
|
+
}
|
|
3682
|
+
function extractMetadataPayload(rawText, sentinel) {
|
|
3683
|
+
const lines = rawText.split(/\r?\n/u);
|
|
3684
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
3685
|
+
if (lines[index]?.trim() !== sentinel) {
|
|
3686
|
+
continue;
|
|
3687
|
+
}
|
|
3688
|
+
let fenceIndex = index + 1;
|
|
3689
|
+
while (fenceIndex < lines.length && lines[fenceIndex]?.trim().length === 0) {
|
|
3690
|
+
fenceIndex += 1;
|
|
3691
|
+
}
|
|
3692
|
+
if (fenceIndex >= lines.length || !/^```(?:json)?\s*$/iu.test(lines[fenceIndex]?.trim() ?? "")) {
|
|
3693
|
+
continue;
|
|
3694
|
+
}
|
|
3695
|
+
fenceIndex += 1;
|
|
3696
|
+
const jsonLines = [];
|
|
3697
|
+
while (fenceIndex < lines.length && !/^```\s*$/u.test(lines[fenceIndex]?.trim() ?? "")) {
|
|
3698
|
+
jsonLines.push(lines[fenceIndex] ?? "");
|
|
3699
|
+
fenceIndex += 1;
|
|
3700
|
+
}
|
|
3701
|
+
if (fenceIndex >= lines.length) {
|
|
3702
|
+
continue;
|
|
3703
|
+
}
|
|
3704
|
+
try {
|
|
3705
|
+
const parsed = JSON.parse(jsonLines.join("\n").trim());
|
|
3706
|
+
return asRecord2(parsed);
|
|
3707
|
+
} catch {
|
|
3708
|
+
continue;
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
return null;
|
|
3712
|
+
}
|
|
3713
|
+
function mapKnownSurface(value) {
|
|
3714
|
+
if (!value) {
|
|
3715
|
+
return null;
|
|
3716
|
+
}
|
|
3717
|
+
if (value.includes("telegram")) {
|
|
3718
|
+
return "telegram";
|
|
3719
|
+
}
|
|
3720
|
+
if (value.includes("signal")) {
|
|
3721
|
+
return "signal";
|
|
3722
|
+
}
|
|
3723
|
+
if (value.includes("discord")) {
|
|
3724
|
+
return "discord";
|
|
3725
|
+
}
|
|
3726
|
+
if (value.includes("openclaw-tui")) {
|
|
3727
|
+
return "tui";
|
|
3728
|
+
}
|
|
3729
|
+
if (value.includes("gateway-client") || value.includes("openclaw-control-ui") || value.includes("webchat")) {
|
|
3730
|
+
return "webchat";
|
|
3731
|
+
}
|
|
3732
|
+
return null;
|
|
3733
|
+
}
|
|
3734
|
+
function extractSenderSurface(rawText) {
|
|
3735
|
+
const payload = extractMetadataPayload(rawText, SENDER_METADATA_SENTINEL);
|
|
3736
|
+
if (!payload) {
|
|
3737
|
+
return null;
|
|
3738
|
+
}
|
|
3739
|
+
const label = getString(payload.label)?.trim().toLowerCase() ?? getString(payload.id)?.trim().toLowerCase() ?? "";
|
|
3740
|
+
return mapKnownSurface(label);
|
|
3741
|
+
}
|
|
3742
|
+
function extractConversationInfoSurface(rawText) {
|
|
3743
|
+
const payload = extractMetadataPayload(rawText, CONVERSATION_INFO_SENTINEL);
|
|
3744
|
+
if (!payload) {
|
|
3745
|
+
return null;
|
|
3746
|
+
}
|
|
3747
|
+
const senderId = getString(payload.sender_id)?.trim().toLowerCase() ?? "";
|
|
3748
|
+
return mapKnownSurface(senderId);
|
|
3749
|
+
}
|
|
3750
|
+
function inferSurfaceFromContent(firstUserRawText) {
|
|
3751
|
+
const normalized = firstUserRawText?.trim().toLowerCase() ?? "";
|
|
3752
|
+
if (!normalized) {
|
|
3753
|
+
return null;
|
|
3754
|
+
}
|
|
3755
|
+
if (normalized.includes("[subagent context]")) {
|
|
3756
|
+
return "subagent";
|
|
3757
|
+
}
|
|
3758
|
+
if (normalized.includes("heartbeat.md")) {
|
|
3759
|
+
return "heartbeat";
|
|
3760
|
+
}
|
|
3761
|
+
return null;
|
|
3762
|
+
}
|
|
3763
|
+
function resolveToolContext(state, message) {
|
|
3764
|
+
const toolCallId = getString(message.toolCallId) ?? getString(message.tool_call_id) ?? getString(message.call_id) ?? getString(message.id);
|
|
3765
|
+
if (toolCallId && state.pendingToolCallsById.has(toolCallId)) {
|
|
3766
|
+
const context = state.pendingToolCallsById.get(toolCallId) ?? null;
|
|
3767
|
+
state.pendingToolCallsById.delete(toolCallId);
|
|
3768
|
+
if (context) {
|
|
3089
3769
|
const queuedIndex = state.pendingToolCalls.findIndex((toolCall) => toolCall.id === toolCallId);
|
|
3090
3770
|
if (queuedIndex >= 0) {
|
|
3091
3771
|
state.pendingToolCalls.splice(queuedIndex, 1);
|
|
@@ -3098,6 +3778,19 @@ function resolveToolContext(state, message) {
|
|
|
3098
3778
|
function handleMessageRecord(state, record, message) {
|
|
3099
3779
|
state.stats.totalMessageRecords += 1;
|
|
3100
3780
|
const role = normalizeOpenClawRole(message.role);
|
|
3781
|
+
if (!state.surfaceDetected) {
|
|
3782
|
+
setDetectedSurface(state, readInboundSurface(message));
|
|
3783
|
+
}
|
|
3784
|
+
if (!state.surfaceDetected && role === "user") {
|
|
3785
|
+
const rawText = extractRawMessageText(message.content);
|
|
3786
|
+
if (state.firstUserRawText === null) {
|
|
3787
|
+
state.firstUserRawText = rawText;
|
|
3788
|
+
}
|
|
3789
|
+
setDetectedSurface(state, extractSenderSurface(rawText));
|
|
3790
|
+
if (!state.surfaceDetected) {
|
|
3791
|
+
setDetectedSurface(state, extractConversationInfoSurface(rawText));
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3101
3794
|
if (role === "system") {
|
|
3102
3795
|
state.stats.systemDropped += 1;
|
|
3103
3796
|
return;
|
|
@@ -3168,8 +3861,14 @@ function handleRecord(state, record) {
|
|
|
3168
3861
|
state.sessionTimestamp = extractTimestamp(record) ?? state.sessionTimestamp;
|
|
3169
3862
|
state.sessionLabel = normalizeSessionLabel(getString(record.conversation_label) ?? "") ?? state.sessionLabel;
|
|
3170
3863
|
addModelUsed(state, record.model);
|
|
3864
|
+
if (!state.surfaceDetected) {
|
|
3865
|
+
setDetectedSurface(state, readInboundSurface(record));
|
|
3866
|
+
}
|
|
3171
3867
|
return;
|
|
3172
3868
|
}
|
|
3869
|
+
if (!state.surfaceDetected) {
|
|
3870
|
+
setDetectedSurface(state, readInboundSurface(record));
|
|
3871
|
+
}
|
|
3173
3872
|
if (record.type === "model_change") {
|
|
3174
3873
|
addModelUsed(state, record.modelId);
|
|
3175
3874
|
state.stats.skippedRecordTypes += 1;
|
|
@@ -3197,212 +3896,253 @@ var OpenClawTranscriptParser = class {
|
|
|
3197
3896
|
* @returns Parsed transcript messages, warnings, and metadata.
|
|
3198
3897
|
*/
|
|
3199
3898
|
async parseFile(filePath, options) {
|
|
3200
|
-
const raw = await
|
|
3899
|
+
const raw = await fs2.readFile(filePath, "utf8");
|
|
3201
3900
|
const verbose = options?.verbose === true;
|
|
3202
3901
|
const state = createParseState();
|
|
3902
|
+
const transcriptHash = createHash2("sha256").update(raw).digest("hex");
|
|
3203
3903
|
parseJsonlLines(raw, state.warnings, (record) => {
|
|
3204
3904
|
handleRecord(state, record);
|
|
3205
3905
|
});
|
|
3906
|
+
if (!state.surfaceDetected && state.firstUserRawText) {
|
|
3907
|
+
setDetectedSurface(state, inferSurfaceFromContent(state.firstUserRawText));
|
|
3908
|
+
}
|
|
3206
3909
|
const fallbackTimestamp = state.messages.length > 0 ? await applyMessageTimestampFallbacks(filePath, state.messages, { sessionTimestamp: state.sessionTimestamp }) : await resolveTimestampFallback(filePath, state.sessionTimestamp);
|
|
3207
3910
|
if (verbose) {
|
|
3208
3911
|
state.warnings.push(buildFilterWarning(state.stats));
|
|
3209
3912
|
}
|
|
3913
|
+
const startedAt = state.sessionTimestamp ?? state.messages[0]?.timestamp ?? fallbackTimestamp;
|
|
3914
|
+
const endedAt = state.messages[state.messages.length - 1]?.timestamp ?? state.sessionTimestamp ?? fallbackTimestamp;
|
|
3210
3915
|
return {
|
|
3211
3916
|
messages: state.messages,
|
|
3212
3917
|
warnings: state.warnings,
|
|
3213
3918
|
metadata: {
|
|
3214
3919
|
sessionId: state.sessionId,
|
|
3215
3920
|
sessionLabel: state.sessionLabel,
|
|
3216
|
-
startedAt
|
|
3217
|
-
|
|
3921
|
+
startedAt,
|
|
3922
|
+
endedAt,
|
|
3923
|
+
messageCount: state.messages.length,
|
|
3924
|
+
transcriptHash,
|
|
3925
|
+
modelsUsed: state.modelsUsed.length > 0 ? state.modelsUsed : void 0,
|
|
3926
|
+
reconstructedSurface: state.detectedSurface,
|
|
3927
|
+
surfaceReconstructionSource: state.surfaceDetected ? "reconstructed" : "none"
|
|
3218
3928
|
}
|
|
3219
3929
|
};
|
|
3220
3930
|
}
|
|
3221
3931
|
};
|
|
3222
3932
|
var openClawTranscriptParser = new OpenClawTranscriptParser();
|
|
3223
3933
|
|
|
3224
|
-
// ../../src/
|
|
3225
|
-
var
|
|
3226
|
-
var
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
"- what topics were discussed",
|
|
3234
|
-
"- what was learned, decided, agreed on, or corrected",
|
|
3235
|
-
"- what remains unfinished, open, or pending",
|
|
3236
|
-
"- user preferences, clarifications, and constraints that matter for continuity",
|
|
3237
|
-
"- the overall direction or intent of the conversation",
|
|
3238
|
-
"Do not replay the transcript turn by turn. Do not invent facts. If something is uncertain, say so briefly."
|
|
3239
|
-
].join("\n");
|
|
3240
|
-
async function generateAndWriteOpenClawSessionSummary(params) {
|
|
3241
|
-
const sessionFile = params.sessionFile.trim();
|
|
3242
|
-
const summaryPath = resolveOpenClawSessionSummaryPath(sessionFile, params.logger);
|
|
3243
|
-
if (!summaryPath) {
|
|
3244
|
-
return {
|
|
3245
|
-
status: "skipped",
|
|
3246
|
-
reason: "missing_session_id"
|
|
3247
|
-
};
|
|
3934
|
+
// ../../src/core/episode/transcript-render.ts
|
|
3935
|
+
var MIN_EPISODE_MESSAGES = 4;
|
|
3936
|
+
var MAX_EPISODE_TRANSCRIPT_CHARS = 14e3;
|
|
3937
|
+
function renderTranscript(messages) {
|
|
3938
|
+
return messages.map((message) => `${message.role === "user" ? "User" : "Assistant"}: ${message.text.trim()}`).join("\n");
|
|
3939
|
+
}
|
|
3940
|
+
function capEpisodeTranscript(transcript, maxChars) {
|
|
3941
|
+
if (transcript.length <= maxChars) {
|
|
3942
|
+
return transcript;
|
|
3248
3943
|
}
|
|
3249
|
-
const
|
|
3250
|
-
const
|
|
3251
|
-
const
|
|
3252
|
-
const
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
)
|
|
3258
|
-
|
|
3259
|
-
return {
|
|
3260
|
-
status: "skipped",
|
|
3261
|
-
reason: "empty",
|
|
3262
|
-
summaryPath,
|
|
3263
|
-
messageCount: cleanedMessages.length,
|
|
3264
|
-
transcriptChars: normalizedTranscript.length
|
|
3265
|
-
};
|
|
3944
|
+
const omissionMarker = "\n\n[Earlier middle transcript omitted for brevity]\n\n";
|
|
3945
|
+
const headBudget = Math.max(0, Math.floor((maxChars - omissionMarker.length) * 0.35));
|
|
3946
|
+
const tailBudget = Math.max(0, maxChars - omissionMarker.length - headBudget);
|
|
3947
|
+
const head = trimToBoundary(transcript.slice(0, headBudget), false);
|
|
3948
|
+
const tail = trimToBoundary(transcript.slice(-tailBudget), true);
|
|
3949
|
+
return `${head}${omissionMarker}${tail}`.trim();
|
|
3950
|
+
}
|
|
3951
|
+
function trimToBoundary(value, fromStart) {
|
|
3952
|
+
if (value.length === 0) {
|
|
3953
|
+
return value;
|
|
3266
3954
|
}
|
|
3267
|
-
if (
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3955
|
+
if (fromStart) {
|
|
3956
|
+
const boundary = value.search(/\s/u);
|
|
3957
|
+
return boundary >= 0 ? value.slice(boundary).trimStart() : value.trim();
|
|
3958
|
+
}
|
|
3959
|
+
const reversedBoundary = value.trimEnd().search(/\s\S*$/u);
|
|
3960
|
+
return reversedBoundary >= 0 ? value.slice(0, reversedBoundary).trimEnd() : value.trim();
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
// ../../src/adapters/openclaw/episode/episode-summary-prompt.ts
|
|
3964
|
+
var OPENCLAW_EPISODE_GENERATOR_VERSION = "openclaw-episodic-summary-v1";
|
|
3965
|
+
|
|
3966
|
+
// ../../src/adapters/openclaw/episode/episode-writer.ts
|
|
3967
|
+
var EPISODE_SUMMARY_TIMEOUT_MS = 45e3;
|
|
3968
|
+
var EPISODE_SUMMARY_TIMEOUT = /* @__PURE__ */ Symbol("episode-summary-timeout");
|
|
3969
|
+
var EPISODE_EMBEDDING_TIMEOUT = /* @__PURE__ */ Symbol("episode-embedding-timeout");
|
|
3970
|
+
var EPISODE_EMBEDDING_MIN_HEADROOM_MS = 5e3;
|
|
3971
|
+
var OpenClawEpisodeSummaryTimeoutError = class extends Error {
|
|
3972
|
+
/**
|
|
3973
|
+
* Creates a timeout error with a stable name for caller-side handling.
|
|
3974
|
+
*/
|
|
3975
|
+
constructor() {
|
|
3976
|
+
super("Episode summary generation timed out.");
|
|
3977
|
+
this.name = "OpenClawEpisodeSummaryTimeoutError";
|
|
3978
|
+
}
|
|
3979
|
+
};
|
|
3980
|
+
async function writeOpenClawPredecessorEpisode(params) {
|
|
3981
|
+
const sessionContext = formatSessionContext(params.ctx.sessionId, params.ctx.sessionKey);
|
|
3982
|
+
const writeStartedAtMs = Date.now();
|
|
3983
|
+
if (!params.predecessor) {
|
|
3984
|
+
params.logger.info(`[agenr] session-start predecessor episode write skipped for ${sessionContext} reason=no_predecessor`);
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
3987
|
+
params.logger.info(`[agenr] session-start predecessor episode write triggered for ${sessionContext} predecessor=${params.predecessor.sessionFile}`);
|
|
3988
|
+
try {
|
|
3989
|
+
const existingEpisode = await params.services.database.getEpisodeBySourceId("openclaw", params.predecessor.sessionId);
|
|
3990
|
+
if (existingEpisode) {
|
|
3991
|
+
params.logger.info(
|
|
3992
|
+
`[agenr] session-start predecessor episode write skipped for ${sessionContext} predecessor=${params.predecessor.sessionFile} reason=already_exists episode=${existingEpisode.id}`
|
|
3993
|
+
);
|
|
3994
|
+
return;
|
|
3995
|
+
}
|
|
3996
|
+
const parsedTranscript = await openClawTranscriptParser.parseFile(params.predecessor.sessionFile);
|
|
3997
|
+
const cleanedMessages = parsedTranscript.messages.filter((message) => message.text.trim().length > 0);
|
|
3998
|
+
if (cleanedMessages.length < MIN_EPISODE_MESSAGES) {
|
|
3999
|
+
params.logger.info(
|
|
4000
|
+
`[agenr] session-start predecessor episode write skipped for ${sessionContext} predecessor=${params.predecessor.sessionFile} reason=too_short cleanedMessages=${cleanedMessages.length}`
|
|
4001
|
+
);
|
|
4002
|
+
return;
|
|
4003
|
+
}
|
|
4004
|
+
const startedAt = parsedTranscript.metadata.startedAt?.trim();
|
|
4005
|
+
const endedAt = parsedTranscript.metadata.endedAt?.trim();
|
|
4006
|
+
if (!startedAt || !endedAt) {
|
|
4007
|
+
params.logger.info(
|
|
4008
|
+
`[agenr] session-start predecessor episode write skipped for ${sessionContext} predecessor=${params.predecessor.sessionFile} reason=missing_metadata`
|
|
4009
|
+
);
|
|
4010
|
+
return;
|
|
4011
|
+
}
|
|
4012
|
+
const episodeExecution = resolveEpisodeSummaryExecution(params.services.openClaw, params.ctx.agentId);
|
|
4013
|
+
const episodeModel = formatResolvedEpisodeSummaryModel(episodeExecution.provider, episodeExecution.model);
|
|
4014
|
+
const transcript = capEpisodeTranscript(renderTranscript(cleanedMessages), MAX_EPISODE_TRANSCRIPT_CHARS);
|
|
4015
|
+
const episodeSummaryLlm = createEpisodeSummaryLlm({
|
|
4016
|
+
logger: params.logger,
|
|
4017
|
+
model: episodeModel,
|
|
4018
|
+
openClaw: params.services.openClaw,
|
|
4019
|
+
sessionFile: params.predecessor.sessionFile,
|
|
4020
|
+
summaryExecution: episodeExecution
|
|
4021
|
+
});
|
|
4022
|
+
const structured = await generateEpisodeSummary(transcript, episodeSummaryLlm);
|
|
4023
|
+
if (!structured) {
|
|
4024
|
+
params.logger.info(
|
|
4025
|
+
`[agenr] session-start predecessor episode write failed for ${sessionContext} predecessor=${params.predecessor.sessionFile} reason=invalid_response model=${episodeModel}`
|
|
4026
|
+
);
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
const embedding = await maybeEmbedEpisodeSummary({
|
|
4030
|
+
summary: structured.summary,
|
|
4031
|
+
embedding: params.services.embedding,
|
|
4032
|
+
embeddingAvailable: params.services.embeddingStatus.available,
|
|
4033
|
+
logger: params.logger,
|
|
4034
|
+
sessionContext,
|
|
4035
|
+
predecessorFile: params.predecessor.sessionFile,
|
|
4036
|
+
deadlineMs: writeStartedAtMs + EPISODE_SUMMARY_TIMEOUT_MS
|
|
4037
|
+
});
|
|
4038
|
+
const writeResult = await params.services.database.upsertEpisode({
|
|
4039
|
+
source: "openclaw",
|
|
4040
|
+
sourceId: params.predecessor.sessionId,
|
|
4041
|
+
sourceRef: params.predecessor.sessionFile,
|
|
4042
|
+
transcriptHash: parsedTranscript.metadata.transcriptHash,
|
|
4043
|
+
agentId: params.ctx.agentId?.trim(),
|
|
4044
|
+
surface: resolveSessionSurface(params.ctx),
|
|
4045
|
+
startedAt,
|
|
4046
|
+
endedAt,
|
|
4047
|
+
summary: structured.summary,
|
|
4048
|
+
tags: structured.tags,
|
|
4049
|
+
activityLevel: structured.activityLevel,
|
|
4050
|
+
project: structured.project,
|
|
4051
|
+
genModel: episodeModel,
|
|
4052
|
+
genVersion: OPENCLAW_EPISODE_GENERATOR_VERSION,
|
|
3272
4053
|
messageCount: cleanedMessages.length,
|
|
3273
|
-
|
|
3274
|
-
};
|
|
4054
|
+
...embedding ? { embedding } : {}
|
|
4055
|
+
});
|
|
4056
|
+
const actionMessage = writeResult.action === "inserted" ? "written" : writeResult.action === "updated" ? "updated" : "unchanged";
|
|
4057
|
+
params.logger.info(
|
|
4058
|
+
`[agenr] session-start predecessor episode write ${actionMessage} for ${sessionContext} predecessor=${params.predecessor.sessionFile} episode=${writeResult.episode.id}`
|
|
4059
|
+
);
|
|
4060
|
+
} catch (error) {
|
|
4061
|
+
if (error instanceof OpenClawEpisodeSummaryTimeoutError) {
|
|
4062
|
+
params.logger.info(
|
|
4063
|
+
`[agenr] session-start predecessor episode write timed_out for ${sessionContext} predecessor=${params.predecessor.sessionFile} timeoutMs=${EPISODE_SUMMARY_TIMEOUT_MS}`
|
|
4064
|
+
);
|
|
4065
|
+
return;
|
|
4066
|
+
}
|
|
4067
|
+
params.logger.info(
|
|
4068
|
+
`[agenr] session-start predecessor episode write failed for ${sessionContext} predecessor=${params.predecessor.sessionFile} reason=${formatErrorMessage2(error)}`
|
|
4069
|
+
);
|
|
3275
4070
|
}
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
const
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
)
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
4071
|
+
}
|
|
4072
|
+
function resolveSessionSurface(ctx) {
|
|
4073
|
+
const sessionKey = ctx.sessionKey?.trim() ?? "";
|
|
4074
|
+
if (/^agent:[^:]+:tui/i.test(sessionKey)) {
|
|
4075
|
+
return "tui";
|
|
4076
|
+
}
|
|
4077
|
+
const provider = ctx.messageProvider?.trim();
|
|
4078
|
+
if (provider) {
|
|
4079
|
+
return provider.toLowerCase();
|
|
4080
|
+
}
|
|
4081
|
+
return void 0;
|
|
4082
|
+
}
|
|
4083
|
+
function createEpisodeSummaryLlm(params) {
|
|
4084
|
+
const complete = async (systemPrompt, userMessage) => {
|
|
4085
|
+
const response = await generateEpisodeSummaryResponse({
|
|
4086
|
+
logger: params.logger,
|
|
4087
|
+
model: params.model,
|
|
4088
|
+
openClaw: params.openClaw,
|
|
4089
|
+
systemPrompt,
|
|
4090
|
+
userMessage,
|
|
4091
|
+
sessionFile: params.sessionFile,
|
|
4092
|
+
summaryExecution: params.summaryExecution
|
|
4093
|
+
});
|
|
4094
|
+
if (response === EPISODE_SUMMARY_TIMEOUT) {
|
|
4095
|
+
throw new OpenClawEpisodeSummaryTimeoutError();
|
|
4096
|
+
}
|
|
4097
|
+
return response;
|
|
4098
|
+
};
|
|
4099
|
+
return {
|
|
4100
|
+
complete,
|
|
4101
|
+
completeJson: async (systemPrompt, userMessage) => {
|
|
4102
|
+
const response = await complete(systemPrompt, userMessage);
|
|
4103
|
+
return JSON.parse(response);
|
|
4104
|
+
}
|
|
4105
|
+
};
|
|
4106
|
+
}
|
|
4107
|
+
async function generateEpisodeSummaryResponse(params) {
|
|
3298
4108
|
const runEmbeddedPiAgent = params.openClaw.runtime.agent.runEmbeddedPiAgent;
|
|
3299
4109
|
if (typeof runEmbeddedPiAgent !== "function") {
|
|
3300
|
-
|
|
3301
|
-
return {
|
|
3302
|
-
status: "skipped",
|
|
3303
|
-
reason: "embedded_agent_unavailable",
|
|
3304
|
-
summaryPath,
|
|
3305
|
-
messageCount: cleanedMessages.length,
|
|
3306
|
-
transcriptChars: normalizedTranscript.length,
|
|
3307
|
-
model: summaryModel
|
|
3308
|
-
};
|
|
4110
|
+
throw new Error(`embedded_agent_unavailable model=${params.model}`);
|
|
3309
4111
|
}
|
|
3310
|
-
const
|
|
3311
|
-
let tempSessionFile;
|
|
4112
|
+
const tempSessionFile = await createTempEpisodeSummarySessionFile();
|
|
3312
4113
|
try {
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
sessionKey: "temp:agenr-summary",
|
|
3319
|
-
agentId: summaryExecution.agentId,
|
|
4114
|
+
const result = await awaitWithTimeout(
|
|
4115
|
+
runEmbeddedPiAgent({
|
|
4116
|
+
sessionId: `agenr-episode-summary-${Date.now()}`,
|
|
4117
|
+
sessionKey: "temp:agenr-episode-summary",
|
|
4118
|
+
agentId: params.summaryExecution.agentId,
|
|
3320
4119
|
sessionFile: tempSessionFile,
|
|
3321
|
-
workspaceDir: summaryExecution.workspaceDir,
|
|
3322
|
-
agentDir: summaryExecution.agentDir,
|
|
4120
|
+
workspaceDir: params.summaryExecution.workspaceDir,
|
|
4121
|
+
agentDir: params.summaryExecution.agentDir,
|
|
3323
4122
|
config: params.openClaw.config,
|
|
3324
|
-
prompt,
|
|
3325
|
-
provider: summaryExecution.provider,
|
|
3326
|
-
model: summaryExecution.model,
|
|
3327
|
-
timeoutMs:
|
|
3328
|
-
runId
|
|
4123
|
+
prompt: params.userMessage,
|
|
4124
|
+
provider: params.summaryExecution.provider,
|
|
4125
|
+
model: params.summaryExecution.model,
|
|
4126
|
+
timeoutMs: EPISODE_SUMMARY_TIMEOUT_MS,
|
|
4127
|
+
runId: `agenr-episode-summary-${Date.now()}`,
|
|
3329
4128
|
disableTools: true,
|
|
3330
|
-
extraSystemPrompt:
|
|
3331
|
-
})
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
if (normalizedSummary.length === 0) {
|
|
3337
|
-
return {
|
|
3338
|
-
status: "failed",
|
|
3339
|
-
reason: "empty_response",
|
|
3340
|
-
summaryPath,
|
|
3341
|
-
messageCount: cleanedMessages.length,
|
|
3342
|
-
transcriptChars: normalizedTranscript.length,
|
|
3343
|
-
model: summaryModel,
|
|
3344
|
-
durationMs
|
|
3345
|
-
};
|
|
4129
|
+
extraSystemPrompt: params.systemPrompt
|
|
4130
|
+
}),
|
|
4131
|
+
EPISODE_SUMMARY_TIMEOUT_MS
|
|
4132
|
+
);
|
|
4133
|
+
if (result === EPISODE_SUMMARY_TIMEOUT) {
|
|
4134
|
+
return EPISODE_SUMMARY_TIMEOUT;
|
|
3346
4135
|
}
|
|
3347
|
-
const
|
|
3348
|
-
if (
|
|
3349
|
-
|
|
3350
|
-
return {
|
|
3351
|
-
status: "skipped",
|
|
3352
|
-
reason: "already_exists",
|
|
3353
|
-
summaryPath,
|
|
3354
|
-
content: existingSummary.content,
|
|
3355
|
-
messageCount: cleanedMessages.length,
|
|
3356
|
-
transcriptChars: normalizedTranscript.length,
|
|
3357
|
-
model: summaryModel,
|
|
3358
|
-
durationMs
|
|
3359
|
-
};
|
|
4136
|
+
const text = extractEmbeddedAgentText(result).trim();
|
|
4137
|
+
if (!text) {
|
|
4138
|
+
throw new Error(`empty_response model=${params.model}`);
|
|
3360
4139
|
}
|
|
3361
|
-
|
|
3362
|
-
`, "utf8");
|
|
3363
|
-
await fs5.writeFile(summaryPath, `${normalizedSummary}
|
|
3364
|
-
`, "utf8");
|
|
3365
|
-
debugLog4(params.logger, "summary", `wrote summary file path=${summaryPath} chars=${normalizedSummary.length} bytes=${summaryBytes}`);
|
|
3366
|
-
return {
|
|
3367
|
-
status: "written",
|
|
3368
|
-
summaryPath,
|
|
3369
|
-
content: normalizedSummary,
|
|
3370
|
-
messageCount: cleanedMessages.length,
|
|
3371
|
-
transcriptChars: normalizedTranscript.length,
|
|
3372
|
-
model: summaryModel,
|
|
3373
|
-
durationMs,
|
|
3374
|
-
bytesWritten: summaryBytes
|
|
3375
|
-
};
|
|
3376
|
-
} catch (error) {
|
|
3377
|
-
const durationMs = Date.now() - startedAt;
|
|
3378
|
-
debugLog4(params.logger, "summary", `summary generation error for file=${sessionFile}: ${formatErrorMessage3(error)}`);
|
|
3379
|
-
return {
|
|
3380
|
-
status: "failed",
|
|
3381
|
-
reason: formatErrorMessage3(error),
|
|
3382
|
-
summaryPath,
|
|
3383
|
-
messageCount: cleanedMessages.length,
|
|
3384
|
-
transcriptChars: normalizedTranscript.length,
|
|
3385
|
-
model: summaryModel,
|
|
3386
|
-
durationMs
|
|
3387
|
-
};
|
|
4140
|
+
return text;
|
|
3388
4141
|
} finally {
|
|
3389
|
-
await
|
|
4142
|
+
await cleanupTempEpisodeSummarySessionFile(tempSessionFile);
|
|
3390
4143
|
}
|
|
3391
4144
|
}
|
|
3392
|
-
|
|
3393
|
-
return generateAndWriteOpenClawSessionSummary(params);
|
|
3394
|
-
}
|
|
3395
|
-
function renderTranscriptForSummary(messages) {
|
|
3396
|
-
return messages.map((message) => `${message.role === "user" ? "User" : "Assistant"}: ${message.text.trim()}`).join("\n");
|
|
3397
|
-
}
|
|
3398
|
-
function debugLog4(logger, subsystem, message) {
|
|
3399
|
-
logger.debug?.(`[agenr] ${subsystem}: ${message}`);
|
|
3400
|
-
}
|
|
3401
|
-
function normalizeSummary(value) {
|
|
3402
|
-
const trimmed = value.trim();
|
|
3403
|
-
return trimmed.replace(/^# .+\n+/u, "").trim();
|
|
3404
|
-
}
|
|
3405
|
-
function resolveSummaryExecution(openClaw, requestedAgentId) {
|
|
4145
|
+
function resolveEpisodeSummaryExecution(openClaw, requestedAgentId) {
|
|
3406
4146
|
const agentId = requestedAgentId?.trim() || resolveDefaultAgentId(openClaw.config);
|
|
3407
4147
|
const modelRef = resolveAgentEffectiveModelPrimary(openClaw.config, agentId);
|
|
3408
4148
|
const parsedModelRef = modelRef ? parseModelRef(modelRef, DEFAULT_PROVIDER) : null;
|
|
@@ -3410,224 +4150,965 @@ function resolveSummaryExecution(openClaw, requestedAgentId) {
|
|
|
3410
4150
|
agentId,
|
|
3411
4151
|
agentDir: openClaw.runtime.agent.resolveAgentDir(openClaw.config, agentId),
|
|
3412
4152
|
workspaceDir: openClaw.runtime.agent.resolveAgentWorkspaceDir(openClaw.config, agentId),
|
|
3413
|
-
modelRef,
|
|
3414
4153
|
provider: parsedModelRef?.provider ?? DEFAULT_PROVIDER,
|
|
3415
4154
|
model: parsedModelRef?.model ?? DEFAULT_MODEL
|
|
3416
4155
|
};
|
|
3417
4156
|
}
|
|
3418
|
-
function
|
|
4157
|
+
function formatResolvedEpisodeSummaryModel(provider, model) {
|
|
3419
4158
|
return `${provider}/${model}`;
|
|
3420
4159
|
}
|
|
3421
|
-
async function
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
}
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
return;
|
|
3428
|
-
}
|
|
3429
|
-
try {
|
|
3430
|
-
await fs5.rm(path4.dirname(tempSessionFile), {
|
|
3431
|
-
recursive: true,
|
|
3432
|
-
force: true
|
|
3433
|
-
});
|
|
3434
|
-
} catch {
|
|
4160
|
+
async function maybeEmbedEpisodeSummary(params) {
|
|
4161
|
+
if (!params.embeddingAvailable) {
|
|
4162
|
+
params.logger.info(
|
|
4163
|
+
`[agenr] session-start predecessor episode embedding skipped for ${params.sessionContext} predecessor=${params.predecessorFile} reason=embedding_unavailable`
|
|
4164
|
+
);
|
|
4165
|
+
return void 0;
|
|
3435
4166
|
}
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
}
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
}
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
return transcript;
|
|
4167
|
+
const remainingBudgetMs = params.deadlineMs - Date.now();
|
|
4168
|
+
if (remainingBudgetMs < EPISODE_EMBEDDING_MIN_HEADROOM_MS) {
|
|
4169
|
+
params.logger.info(
|
|
4170
|
+
`[agenr] session-start predecessor episode embedding skipped for ${params.sessionContext} predecessor=${params.predecessorFile} reason=budget_tight remainingMs=${Math.max(
|
|
4171
|
+
0,
|
|
4172
|
+
remainingBudgetMs
|
|
4173
|
+
)}`
|
|
4174
|
+
);
|
|
4175
|
+
return void 0;
|
|
3446
4176
|
}
|
|
3447
|
-
|
|
3448
|
-
|
|
4177
|
+
try {
|
|
4178
|
+
const result = await awaitEmbeddingWithTimeout(params.embedding.embed([params.summary]), remainingBudgetMs);
|
|
4179
|
+
if (result === EPISODE_EMBEDDING_TIMEOUT) {
|
|
4180
|
+
params.logger.info(
|
|
4181
|
+
`[agenr] session-start predecessor episode embedding skipped for ${params.sessionContext} predecessor=${params.predecessorFile} reason=embedding_timeout budgetMs=${remainingBudgetMs}`
|
|
4182
|
+
);
|
|
4183
|
+
return void 0;
|
|
4184
|
+
}
|
|
4185
|
+
const vector = result[0]?.map((value) => Number.isFinite(value) ? value : 0);
|
|
4186
|
+
if (!vector || vector.length === 0) {
|
|
4187
|
+
params.logger.info(
|
|
4188
|
+
`[agenr] session-start predecessor episode embedding skipped for ${params.sessionContext} predecessor=${params.predecessorFile} reason=empty_embedding`
|
|
4189
|
+
);
|
|
4190
|
+
return void 0;
|
|
4191
|
+
}
|
|
4192
|
+
return vector;
|
|
4193
|
+
} catch (error) {
|
|
4194
|
+
params.logger.info(
|
|
4195
|
+
`[agenr] session-start predecessor episode embedding skipped for ${params.sessionContext} predecessor=${params.predecessorFile} reason=${formatErrorMessage2(error)}`
|
|
4196
|
+
);
|
|
4197
|
+
return void 0;
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
async function awaitWithTimeout(promise, timeoutMs) {
|
|
4201
|
+
return new Promise((resolve, reject) => {
|
|
4202
|
+
const timeout = setTimeout(() => {
|
|
4203
|
+
resolve(EPISODE_SUMMARY_TIMEOUT);
|
|
4204
|
+
}, timeoutMs);
|
|
4205
|
+
promise.then(
|
|
4206
|
+
(value) => {
|
|
4207
|
+
clearTimeout(timeout);
|
|
4208
|
+
resolve(value);
|
|
4209
|
+
},
|
|
4210
|
+
(error) => {
|
|
4211
|
+
clearTimeout(timeout);
|
|
4212
|
+
reject(error);
|
|
4213
|
+
}
|
|
4214
|
+
);
|
|
4215
|
+
});
|
|
4216
|
+
}
|
|
4217
|
+
async function awaitEmbeddingWithTimeout(promise, timeoutMs) {
|
|
4218
|
+
return new Promise((resolve, reject) => {
|
|
4219
|
+
const timeout = setTimeout(() => {
|
|
4220
|
+
resolve(EPISODE_EMBEDDING_TIMEOUT);
|
|
4221
|
+
}, timeoutMs);
|
|
4222
|
+
promise.then(
|
|
4223
|
+
(value) => {
|
|
4224
|
+
clearTimeout(timeout);
|
|
4225
|
+
resolve(value);
|
|
4226
|
+
},
|
|
4227
|
+
(error) => {
|
|
4228
|
+
clearTimeout(timeout);
|
|
4229
|
+
reject(error);
|
|
4230
|
+
}
|
|
4231
|
+
);
|
|
4232
|
+
});
|
|
4233
|
+
}
|
|
4234
|
+
async function createTempEpisodeSummarySessionFile() {
|
|
4235
|
+
const tempDir = await fs3.mkdtemp(path.join(os.tmpdir(), "agenr-episode-summary-"));
|
|
4236
|
+
return path.join(tempDir, "session.jsonl");
|
|
4237
|
+
}
|
|
4238
|
+
async function cleanupTempEpisodeSummarySessionFile(tempEpisodeSummarySessionFile) {
|
|
4239
|
+
try {
|
|
4240
|
+
await fs3.rm(path.dirname(tempEpisodeSummarySessionFile), {
|
|
4241
|
+
recursive: true,
|
|
4242
|
+
force: true
|
|
4243
|
+
});
|
|
4244
|
+
} catch {
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
function extractEmbeddedAgentText(result) {
|
|
4248
|
+
return result.payloads?.find((payload) => payload.text?.trim())?.text ?? "";
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4251
|
+
// ../../src/adapters/openclaw/format/recall-format.ts
|
|
4252
|
+
var MAX_CONTENT_CHARS = 280;
|
|
4253
|
+
function formatAgenrSessionStartRecall(recall2) {
|
|
4254
|
+
const sections = buildSections(recall2);
|
|
4255
|
+
if (sections.length === 0) {
|
|
4256
|
+
return "";
|
|
4257
|
+
}
|
|
4258
|
+
const lines = ["## Agenr Session Recall", "Use this as prior context. Confirm anything important if the current conversation conflicts with it.", ""];
|
|
4259
|
+
for (const section of sections) {
|
|
4260
|
+
lines.push(`### ${section.title}`);
|
|
4261
|
+
for (const item of section.entries) {
|
|
4262
|
+
lines.push(formatEntryHeader(item));
|
|
4263
|
+
lines.push(formatEntryBody(item.entry));
|
|
4264
|
+
}
|
|
4265
|
+
lines.push("");
|
|
4266
|
+
}
|
|
4267
|
+
return lines.join("\n").trim();
|
|
4268
|
+
}
|
|
4269
|
+
function buildSections(recall2) {
|
|
4270
|
+
const sections = [];
|
|
4271
|
+
const coreEntries = recall2.core.map((entry) => ({ entry }));
|
|
4272
|
+
if (coreEntries.length > 0) {
|
|
4273
|
+
sections.push({ title: "Core Memory", entries: coreEntries });
|
|
4274
|
+
}
|
|
4275
|
+
return sections;
|
|
4276
|
+
}
|
|
4277
|
+
function formatEntryHeader(item) {
|
|
4278
|
+
const metadata = [
|
|
4279
|
+
item.entry.id,
|
|
4280
|
+
item.entry.type,
|
|
4281
|
+
item.entry.expiry,
|
|
4282
|
+
`importance ${item.entry.importance}`,
|
|
4283
|
+
item.score !== void 0 ? `score ${item.score.toFixed(2)}` : void 0
|
|
4284
|
+
].filter((value) => value !== void 0);
|
|
4285
|
+
return `- [${metadata.join(" | ")}] ${item.entry.subject}`;
|
|
4286
|
+
}
|
|
4287
|
+
function formatEntryBody(entry) {
|
|
4288
|
+
const content = truncate2(entry.content.trim(), MAX_CONTENT_CHARS);
|
|
4289
|
+
const extra = [
|
|
4290
|
+
entry.tags.length > 0 ? `tags: ${entry.tags.join(", ")}` : void 0,
|
|
4291
|
+
entry.created_at ? `created: ${entry.created_at.slice(0, 10)}` : void 0
|
|
4292
|
+
].filter((value) => value !== void 0);
|
|
4293
|
+
if (extra.length === 0) {
|
|
4294
|
+
return ` ${content}`;
|
|
4295
|
+
}
|
|
4296
|
+
return ` ${content} (${extra.join(" | ")})`;
|
|
4297
|
+
}
|
|
4298
|
+
function truncate2(value, maxChars) {
|
|
4299
|
+
if (value.length <= maxChars) {
|
|
4300
|
+
return value;
|
|
4301
|
+
}
|
|
4302
|
+
return `${value.slice(0, maxChars - 3).trimEnd()}...`;
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4305
|
+
// ../../src/adapters/openclaw/session/continuity/continuity-summary-generator.ts
|
|
4306
|
+
import { promises as fs5 } from "fs";
|
|
4307
|
+
import os2 from "os";
|
|
4308
|
+
import path4 from "path";
|
|
4309
|
+
import { DEFAULT_MODEL as DEFAULT_MODEL2, DEFAULT_PROVIDER as DEFAULT_PROVIDER2, parseModelRef as parseModelRef2, resolveAgentEffectiveModelPrimary as resolveAgentEffectiveModelPrimary2, resolveDefaultAgentId as resolveDefaultAgentId2 } from "openclaw/plugin-sdk/agent-runtime";
|
|
4310
|
+
|
|
4311
|
+
// ../../src/adapters/openclaw/session/continuity/continuity-summary-reader.ts
|
|
4312
|
+
import { promises as fs4 } from "fs";
|
|
4313
|
+
import path3 from "path";
|
|
4314
|
+
|
|
4315
|
+
// ../../src/adapters/openclaw/session/session-id.ts
|
|
4316
|
+
import path2 from "path";
|
|
4317
|
+
function deriveOpenClawSessionIdFromFilePath(sessionFile, logger) {
|
|
4318
|
+
const normalizedSessionFile = sessionFile.trim();
|
|
4319
|
+
if (normalizedSessionFile.length === 0) {
|
|
4320
|
+
debugLog(logger, "session-id", "cannot derive session id from empty session file path");
|
|
4321
|
+
return void 0;
|
|
4322
|
+
}
|
|
4323
|
+
const fileName = path2.basename(normalizedSessionFile);
|
|
4324
|
+
const sessionId = fileName.replace(/\.jsonl(?:\..*)?$/i, "").trim();
|
|
4325
|
+
debugLog(logger, "session-id", `derived session id "${sessionId || "<empty>"}" from file=${normalizedSessionFile}`);
|
|
4326
|
+
return sessionId.length > 0 ? sessionId : void 0;
|
|
4327
|
+
}
|
|
4328
|
+
function debugLog(logger, subsystem, message) {
|
|
4329
|
+
logger?.debug?.(`[agenr] ${subsystem}: ${message}`);
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
// ../../src/adapters/openclaw/session/continuity/continuity-summary-reader.ts
|
|
4333
|
+
function resolveOpenClawContinuitySummaryPath(sessionFile, logger) {
|
|
4334
|
+
const normalizedSessionFile = sessionFile.trim();
|
|
4335
|
+
const sessionId = deriveOpenClawSessionIdFromFilePath(normalizedSessionFile, logger);
|
|
4336
|
+
if (!sessionId) {
|
|
4337
|
+
return void 0;
|
|
4338
|
+
}
|
|
4339
|
+
const continuitySummaryPath = path3.join(path3.dirname(normalizedSessionFile), `${sessionId}.continuity-summary.md`);
|
|
4340
|
+
debugLog2(logger, "continuity-summary-reader", `resolved continuity summary path for session=${sessionId}: ${continuitySummaryPath}`);
|
|
4341
|
+
return continuitySummaryPath;
|
|
4342
|
+
}
|
|
4343
|
+
async function readOpenClawContinuitySummaryFile(sessionFile, logger) {
|
|
4344
|
+
const continuitySummaryPath = resolveOpenClawContinuitySummaryPath(sessionFile, logger);
|
|
4345
|
+
const sessionId = deriveOpenClawSessionIdFromFilePath(sessionFile, logger);
|
|
4346
|
+
if (!continuitySummaryPath || !sessionId) {
|
|
4347
|
+
return null;
|
|
4348
|
+
}
|
|
4349
|
+
try {
|
|
4350
|
+
const continuitySummaryContent = (await fs4.readFile(continuitySummaryPath, "utf8")).trim();
|
|
4351
|
+
if (continuitySummaryContent.length === 0) {
|
|
4352
|
+
debugLog2(logger, "continuity-summary-reader", `continuity summary file is empty for session=${sessionId} path=${continuitySummaryPath}`);
|
|
4353
|
+
return null;
|
|
4354
|
+
}
|
|
4355
|
+
debugLog2(
|
|
4356
|
+
logger,
|
|
4357
|
+
"continuity-summary-reader",
|
|
4358
|
+
`loaded continuity summary file for session=${sessionId} path=${continuitySummaryPath} chars=${continuitySummaryContent.length}`
|
|
4359
|
+
);
|
|
4360
|
+
return {
|
|
4361
|
+
sessionId,
|
|
4362
|
+
continuitySummaryPath,
|
|
4363
|
+
content: continuitySummaryContent
|
|
4364
|
+
};
|
|
4365
|
+
} catch (error) {
|
|
4366
|
+
if (isFileNotFound(error)) {
|
|
4367
|
+
debugLog2(logger, "continuity-summary-reader", `continuity summary file missing for session=${sessionId} path=${continuitySummaryPath}`);
|
|
4368
|
+
return null;
|
|
4369
|
+
}
|
|
4370
|
+
throw error;
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
function debugLog2(logger, subsystem, message) {
|
|
4374
|
+
logger?.debug?.(`[agenr] ${subsystem}: ${message}`);
|
|
4375
|
+
}
|
|
4376
|
+
function isFileNotFound(error) {
|
|
4377
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
// ../../src/adapters/openclaw/session/continuity/continuity-summary-generator.ts
|
|
4381
|
+
var MIN_CONTINUITY_SUMMARY_MESSAGES = 4;
|
|
4382
|
+
var MAX_CONTINUITY_TRANSCRIPT_CHARS = 14e3;
|
|
4383
|
+
var CONTINUITY_SUMMARY_TIMEOUT_MS = 15e3;
|
|
4384
|
+
var CONTINUITY_SUMMARY_SYSTEM_PROMPT = [
|
|
4385
|
+
"You write concise narrative continuity summaries that help the next session continue smoothly.",
|
|
4386
|
+
"The transcript can be about any domain. Do not assume technical, project, or coding context unless the transcript shows it.",
|
|
4387
|
+
"Write 200 to 500 words in plain Markdown with no code fences.",
|
|
4388
|
+
"Capture:",
|
|
4389
|
+
"- what topics were discussed",
|
|
4390
|
+
"- what was learned, decided, agreed on, or corrected",
|
|
4391
|
+
"- what remains unfinished, open, or pending",
|
|
4392
|
+
"- user preferences, clarifications, and constraints that matter for continuity",
|
|
4393
|
+
"- the overall direction or intent of the conversation",
|
|
4394
|
+
"Do not replay the transcript turn by turn. Do not invent facts. If something is uncertain, say so briefly."
|
|
4395
|
+
].join("\n");
|
|
4396
|
+
async function generateAndWriteOpenClawContinuitySummary(params) {
|
|
4397
|
+
const sessionFile = params.sessionFile.trim();
|
|
4398
|
+
const continuitySummaryPath = resolveOpenClawContinuitySummaryPath(sessionFile, params.logger);
|
|
4399
|
+
if (!continuitySummaryPath) {
|
|
4400
|
+
return {
|
|
4401
|
+
status: "skipped",
|
|
4402
|
+
reason: "missing_session_id"
|
|
4403
|
+
};
|
|
4404
|
+
}
|
|
4405
|
+
const parsedTranscript = await openClawTranscriptParser.parseFile(sessionFile);
|
|
4406
|
+
const cleanedMessages = parsedTranscript.messages.filter((message) => message.text.trim().length > 0);
|
|
4407
|
+
const transcript = renderTranscriptForContinuitySummary(cleanedMessages);
|
|
4408
|
+
const normalizedTranscript = capContinuityTranscript(transcript, MAX_CONTINUITY_TRANSCRIPT_CHARS);
|
|
4409
|
+
debugLog3(
|
|
4410
|
+
params.logger,
|
|
4411
|
+
"continuity-summary",
|
|
4412
|
+
`transcript adapter output for file=${sessionFile}: messages=${cleanedMessages.length} chars=${normalizedTranscript.length}`
|
|
4413
|
+
);
|
|
4414
|
+
if (cleanedMessages.length === 0 || normalizedTranscript.length === 0) {
|
|
4415
|
+
return {
|
|
4416
|
+
status: "skipped",
|
|
4417
|
+
reason: "empty",
|
|
4418
|
+
continuitySummaryPath,
|
|
4419
|
+
messageCount: cleanedMessages.length,
|
|
4420
|
+
transcriptChars: normalizedTranscript.length
|
|
4421
|
+
};
|
|
4422
|
+
}
|
|
4423
|
+
if (cleanedMessages.length < MIN_CONTINUITY_SUMMARY_MESSAGES) {
|
|
4424
|
+
return {
|
|
4425
|
+
status: "skipped",
|
|
4426
|
+
reason: "too_short",
|
|
4427
|
+
continuitySummaryPath,
|
|
4428
|
+
messageCount: cleanedMessages.length,
|
|
4429
|
+
transcriptChars: normalizedTranscript.length
|
|
4430
|
+
};
|
|
4431
|
+
}
|
|
4432
|
+
const continuitySummaryExecution = resolveContinuitySummaryExecution(params.openClaw, params.agentId);
|
|
4433
|
+
const continuitySummaryModel = formatResolvedContinuitySummaryModel(continuitySummaryExecution.provider, continuitySummaryExecution.model);
|
|
4434
|
+
const prompt = [
|
|
4435
|
+
"Produce a concise continuity summary for the next session.",
|
|
4436
|
+
"Prefer short paragraphs. Use a short 'Open loops' section only if it adds clarity.",
|
|
4437
|
+
"",
|
|
4438
|
+
"Transcript:",
|
|
4439
|
+
normalizedTranscript
|
|
4440
|
+
].join("\n");
|
|
4441
|
+
debugLog3(
|
|
4442
|
+
params.logger,
|
|
4443
|
+
"continuity-summary",
|
|
4444
|
+
`sending continuity summary prompt model=${continuitySummaryModel} promptChars=${prompt.length} transcriptChars=${normalizedTranscript.length}`
|
|
4445
|
+
);
|
|
4446
|
+
params.logger.info(
|
|
4447
|
+
`[agenr] continuity-summary: using OpenClaw embedded agent provider=${continuitySummaryExecution.provider} model=${continuitySummaryExecution.model} agent=${continuitySummaryExecution.agentId}`
|
|
4448
|
+
);
|
|
4449
|
+
debugLog3(
|
|
4450
|
+
params.logger,
|
|
4451
|
+
"continuity-summary",
|
|
4452
|
+
`resolved OpenClaw continuity summary model for file=${sessionFile}: agentId=${continuitySummaryExecution.agentId} modelRef=${continuitySummaryExecution.modelRef ?? "default"} provider=${continuitySummaryExecution.provider} model=${continuitySummaryExecution.model}`
|
|
4453
|
+
);
|
|
4454
|
+
const runEmbeddedPiAgent = params.openClaw.runtime.agent.runEmbeddedPiAgent;
|
|
4455
|
+
if (typeof runEmbeddedPiAgent !== "function") {
|
|
4456
|
+
params.logger.warn?.(`[agenr] continuity-summary: OpenClaw embedded agent runner unavailable for file=${sessionFile}`);
|
|
4457
|
+
return {
|
|
4458
|
+
status: "skipped",
|
|
4459
|
+
reason: "embedded_agent_unavailable",
|
|
4460
|
+
continuitySummaryPath,
|
|
4461
|
+
messageCount: cleanedMessages.length,
|
|
4462
|
+
transcriptChars: normalizedTranscript.length,
|
|
4463
|
+
model: continuitySummaryModel
|
|
4464
|
+
};
|
|
4465
|
+
}
|
|
4466
|
+
const startedAt = Date.now();
|
|
4467
|
+
let tempContinuitySummarySessionFile;
|
|
4468
|
+
try {
|
|
4469
|
+
tempContinuitySummarySessionFile = await createTempContinuitySummarySessionFile();
|
|
4470
|
+
const runId = `agenr-continuity-summary-${Date.now()}`;
|
|
4471
|
+
const response = extractEmbeddedAgentText2(
|
|
4472
|
+
await runEmbeddedPiAgent({
|
|
4473
|
+
sessionId: runId,
|
|
4474
|
+
sessionKey: "temp:agenr-continuity-summary",
|
|
4475
|
+
agentId: continuitySummaryExecution.agentId,
|
|
4476
|
+
sessionFile: tempContinuitySummarySessionFile,
|
|
4477
|
+
workspaceDir: continuitySummaryExecution.workspaceDir,
|
|
4478
|
+
agentDir: continuitySummaryExecution.agentDir,
|
|
4479
|
+
config: params.openClaw.config,
|
|
4480
|
+
prompt,
|
|
4481
|
+
provider: continuitySummaryExecution.provider,
|
|
4482
|
+
model: continuitySummaryExecution.model,
|
|
4483
|
+
timeoutMs: CONTINUITY_SUMMARY_TIMEOUT_MS,
|
|
4484
|
+
runId,
|
|
4485
|
+
disableTools: true,
|
|
4486
|
+
extraSystemPrompt: CONTINUITY_SUMMARY_SYSTEM_PROMPT
|
|
4487
|
+
})
|
|
4488
|
+
).trim();
|
|
4489
|
+
const durationMs = Date.now() - startedAt;
|
|
4490
|
+
const normalizedContinuitySummary = normalizeContinuitySummary(response);
|
|
4491
|
+
debugLog3(
|
|
4492
|
+
params.logger,
|
|
4493
|
+
"continuity-summary",
|
|
4494
|
+
`received continuity summary response model=${continuitySummaryModel} durationMs=${durationMs} chars=${normalizedContinuitySummary.length}`
|
|
4495
|
+
);
|
|
4496
|
+
if (normalizedContinuitySummary.length === 0) {
|
|
4497
|
+
return {
|
|
4498
|
+
status: "failed",
|
|
4499
|
+
reason: "empty_response",
|
|
4500
|
+
continuitySummaryPath,
|
|
4501
|
+
messageCount: cleanedMessages.length,
|
|
4502
|
+
transcriptChars: normalizedTranscript.length,
|
|
4503
|
+
model: continuitySummaryModel,
|
|
4504
|
+
durationMs
|
|
4505
|
+
};
|
|
4506
|
+
}
|
|
4507
|
+
const existingContinuitySummary = await readOpenClawContinuitySummaryFile(sessionFile, params.logger);
|
|
4508
|
+
if (existingContinuitySummary?.continuitySummaryPath === continuitySummaryPath) {
|
|
4509
|
+
debugLog3(
|
|
4510
|
+
params.logger,
|
|
4511
|
+
"continuity-summary",
|
|
4512
|
+
`continuity summary file already exists at write time path=${continuitySummaryPath} chars=${existingContinuitySummary.content.length}`
|
|
4513
|
+
);
|
|
4514
|
+
return {
|
|
4515
|
+
status: "skipped",
|
|
4516
|
+
reason: "already_exists",
|
|
4517
|
+
continuitySummaryPath,
|
|
4518
|
+
content: existingContinuitySummary.content,
|
|
4519
|
+
messageCount: cleanedMessages.length,
|
|
4520
|
+
transcriptChars: normalizedTranscript.length,
|
|
4521
|
+
model: continuitySummaryModel,
|
|
4522
|
+
durationMs
|
|
4523
|
+
};
|
|
4524
|
+
}
|
|
4525
|
+
const continuitySummaryBytes = Buffer.byteLength(`${normalizedContinuitySummary}
|
|
4526
|
+
`, "utf8");
|
|
4527
|
+
await fs5.writeFile(continuitySummaryPath, `${normalizedContinuitySummary}
|
|
4528
|
+
`, "utf8");
|
|
4529
|
+
debugLog3(
|
|
4530
|
+
params.logger,
|
|
4531
|
+
"continuity-summary",
|
|
4532
|
+
`wrote continuity summary file path=${continuitySummaryPath} chars=${normalizedContinuitySummary.length} bytes=${continuitySummaryBytes}`
|
|
4533
|
+
);
|
|
4534
|
+
return {
|
|
4535
|
+
status: "written",
|
|
4536
|
+
continuitySummaryPath,
|
|
4537
|
+
content: normalizedContinuitySummary,
|
|
4538
|
+
messageCount: cleanedMessages.length,
|
|
4539
|
+
transcriptChars: normalizedTranscript.length,
|
|
4540
|
+
model: continuitySummaryModel,
|
|
4541
|
+
durationMs,
|
|
4542
|
+
bytesWritten: continuitySummaryBytes
|
|
4543
|
+
};
|
|
4544
|
+
} catch (error) {
|
|
4545
|
+
const durationMs = Date.now() - startedAt;
|
|
4546
|
+
debugLog3(params.logger, "continuity-summary", `continuity summary generation error for file=${sessionFile}: ${formatErrorMessage3(error)}`);
|
|
4547
|
+
return {
|
|
4548
|
+
status: "failed",
|
|
4549
|
+
reason: formatErrorMessage3(error),
|
|
4550
|
+
continuitySummaryPath,
|
|
4551
|
+
messageCount: cleanedMessages.length,
|
|
4552
|
+
transcriptChars: normalizedTranscript.length,
|
|
4553
|
+
model: continuitySummaryModel,
|
|
4554
|
+
durationMs
|
|
4555
|
+
};
|
|
4556
|
+
} finally {
|
|
4557
|
+
await cleanupTempContinuitySummarySessionFile(tempContinuitySummarySessionFile);
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
function renderTranscriptForContinuitySummary(messages) {
|
|
4561
|
+
return messages.map((message) => `${message.role === "user" ? "User" : "Assistant"}: ${message.text.trim()}`).join("\n");
|
|
4562
|
+
}
|
|
4563
|
+
function debugLog3(logger, subsystem, message) {
|
|
4564
|
+
logger.debug?.(`[agenr] ${subsystem}: ${message}`);
|
|
4565
|
+
}
|
|
4566
|
+
function normalizeContinuitySummary(value) {
|
|
4567
|
+
const trimmed = value.trim();
|
|
4568
|
+
return trimmed.replace(/^# .+\n+/u, "").trim();
|
|
4569
|
+
}
|
|
4570
|
+
function resolveContinuitySummaryExecution(openClaw, requestedAgentId) {
|
|
4571
|
+
const agentId = requestedAgentId?.trim() || resolveDefaultAgentId2(openClaw.config);
|
|
4572
|
+
const modelRef = resolveAgentEffectiveModelPrimary2(openClaw.config, agentId);
|
|
4573
|
+
const parsedModelRef = modelRef ? parseModelRef2(modelRef, DEFAULT_PROVIDER2) : null;
|
|
4574
|
+
return {
|
|
4575
|
+
agentId,
|
|
4576
|
+
agentDir: openClaw.runtime.agent.resolveAgentDir(openClaw.config, agentId),
|
|
4577
|
+
workspaceDir: openClaw.runtime.agent.resolveAgentWorkspaceDir(openClaw.config, agentId),
|
|
4578
|
+
modelRef,
|
|
4579
|
+
provider: parsedModelRef?.provider ?? DEFAULT_PROVIDER2,
|
|
4580
|
+
model: parsedModelRef?.model ?? DEFAULT_MODEL2
|
|
4581
|
+
};
|
|
4582
|
+
}
|
|
4583
|
+
function formatResolvedContinuitySummaryModel(provider, model) {
|
|
4584
|
+
return `${provider}/${model}`;
|
|
4585
|
+
}
|
|
4586
|
+
async function createTempContinuitySummarySessionFile() {
|
|
4587
|
+
const tempDir = await fs5.mkdtemp(path4.join(os2.tmpdir(), "agenr-continuity-summary-"));
|
|
4588
|
+
return path4.join(tempDir, "session.jsonl");
|
|
4589
|
+
}
|
|
4590
|
+
async function cleanupTempContinuitySummarySessionFile(tempContinuitySummarySessionFile) {
|
|
4591
|
+
if (!tempContinuitySummarySessionFile) {
|
|
4592
|
+
return;
|
|
4593
|
+
}
|
|
4594
|
+
try {
|
|
4595
|
+
await fs5.rm(path4.dirname(tempContinuitySummarySessionFile), {
|
|
4596
|
+
recursive: true,
|
|
4597
|
+
force: true
|
|
4598
|
+
});
|
|
4599
|
+
} catch {
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
function extractEmbeddedAgentText2(result) {
|
|
4603
|
+
return result.payloads?.find((payload) => payload.text?.trim())?.text ?? "";
|
|
4604
|
+
}
|
|
4605
|
+
function formatErrorMessage3(error) {
|
|
4606
|
+
return error instanceof Error ? error.message : String(error);
|
|
4607
|
+
}
|
|
4608
|
+
function capContinuityTranscript(transcript, maxChars) {
|
|
4609
|
+
if (transcript.length <= maxChars) {
|
|
4610
|
+
return transcript;
|
|
4611
|
+
}
|
|
4612
|
+
const omissionMarker = "\n\n[Earlier middle transcript omitted for brevity]\n\n";
|
|
4613
|
+
const headBudget = Math.max(0, Math.floor((maxChars - omissionMarker.length) * 0.35));
|
|
3449
4614
|
const tailBudget = Math.max(0, maxChars - omissionMarker.length - headBudget);
|
|
3450
|
-
const head =
|
|
3451
|
-
const tail =
|
|
4615
|
+
const head = trimToBoundary2(transcript.slice(0, headBudget), false);
|
|
4616
|
+
const tail = trimToBoundary2(transcript.slice(-tailBudget), true);
|
|
3452
4617
|
return `${head}${omissionMarker}${tail}`.trim();
|
|
3453
4618
|
}
|
|
3454
|
-
function
|
|
3455
|
-
if (value.length === 0) {
|
|
3456
|
-
return value;
|
|
4619
|
+
function trimToBoundary2(value, fromStart) {
|
|
4620
|
+
if (value.length === 0) {
|
|
4621
|
+
return value;
|
|
4622
|
+
}
|
|
4623
|
+
if (fromStart) {
|
|
4624
|
+
const boundary = value.search(/\s/);
|
|
4625
|
+
return boundary >= 0 ? value.slice(boundary).trimStart() : value.trim();
|
|
4626
|
+
}
|
|
4627
|
+
const reversedBoundary = value.trimEnd().search(/\s\S*$/u);
|
|
4628
|
+
return reversedBoundary >= 0 ? value.slice(0, reversedBoundary).trimEnd() : value.trim();
|
|
4629
|
+
}
|
|
4630
|
+
|
|
4631
|
+
// ../../src/adapters/openclaw/session/continuity/predecessor-resolver.ts
|
|
4632
|
+
import path6 from "path";
|
|
4633
|
+
|
|
4634
|
+
// ../../src/adapters/openclaw/session/sessions-store-reader.ts
|
|
4635
|
+
import { promises as fs6 } from "fs";
|
|
4636
|
+
import path5 from "path";
|
|
4637
|
+
async function readOpenClawSessionsStore(sessionsDir, logger) {
|
|
4638
|
+
const normalizedSessionsDir = sessionsDir.trim();
|
|
4639
|
+
if (normalizedSessionsDir.length === 0) {
|
|
4640
|
+
debugLog4(logger, "sessions-store-reader", "skipping sessions.json read because sessionsDir is empty");
|
|
4641
|
+
return [];
|
|
4642
|
+
}
|
|
4643
|
+
const resolvedSessionsDir = path5.resolve(normalizedSessionsDir);
|
|
4644
|
+
const sessionsJsonPath = path5.join(resolvedSessionsDir, "sessions.json");
|
|
4645
|
+
try {
|
|
4646
|
+
const raw = await fs6.readFile(sessionsJsonPath, "utf8");
|
|
4647
|
+
const parsed = JSON.parse(raw);
|
|
4648
|
+
if (!isRecord2(parsed)) {
|
|
4649
|
+
debugLog4(logger, "sessions-store-reader", `sessions.json did not contain an object: path=${sessionsJsonPath}`);
|
|
4650
|
+
return [];
|
|
4651
|
+
}
|
|
4652
|
+
const entries = [];
|
|
4653
|
+
for (const [sessionKey, value] of Object.entries(parsed)) {
|
|
4654
|
+
const normalizedSessionKey = sessionKey.trim();
|
|
4655
|
+
if (normalizedSessionKey.length === 0) {
|
|
4656
|
+
debugLog4(logger, "sessions-store-reader", `skipping blank session key in ${sessionsJsonPath}`);
|
|
4657
|
+
continue;
|
|
4658
|
+
}
|
|
4659
|
+
if (!isRecord2(value)) {
|
|
4660
|
+
debugLog4(logger, "sessions-store-reader", `skipping non-object entry for key=${normalizedSessionKey}`);
|
|
4661
|
+
continue;
|
|
4662
|
+
}
|
|
4663
|
+
const sessionId = asTrimmedString(value["sessionId"]);
|
|
4664
|
+
const sessionFile = asTrimmedString(value["sessionFile"]);
|
|
4665
|
+
const origin = isRecord2(value["origin"]) ? value["origin"] : void 0;
|
|
4666
|
+
const surface = asTrimmedString(origin?.["surface"]);
|
|
4667
|
+
const provider = asTrimmedString(origin?.["provider"]);
|
|
4668
|
+
const chatType = asTrimmedString(value["chatType"]);
|
|
4669
|
+
const updatedAt = asFiniteNumber(value["updatedAt"]);
|
|
4670
|
+
entries.push({
|
|
4671
|
+
sessionKey: normalizedSessionKey,
|
|
4672
|
+
...sessionId ? { sessionId } : {},
|
|
4673
|
+
...sessionFile ? { sessionFile: resolveSessionStorePath(sessionFile, resolvedSessionsDir) } : {},
|
|
4674
|
+
...surface ? { surface } : {},
|
|
4675
|
+
...provider ? { provider } : {},
|
|
4676
|
+
...chatType ? { chatType } : {},
|
|
4677
|
+
...updatedAt !== void 0 ? { updatedAt } : {}
|
|
4678
|
+
});
|
|
4679
|
+
}
|
|
4680
|
+
debugLog4(logger, "sessions-store-reader", `loaded sessions.json entries=${entries.length} path=${sessionsJsonPath}`);
|
|
4681
|
+
return entries;
|
|
4682
|
+
} catch (error) {
|
|
4683
|
+
if (isFileNotFound2(error)) {
|
|
4684
|
+
debugLog4(logger, "sessions-store-reader", `sessions.json missing at ${sessionsJsonPath}`);
|
|
4685
|
+
return [];
|
|
4686
|
+
}
|
|
4687
|
+
if (error instanceof SyntaxError) {
|
|
4688
|
+
debugLog4(logger, "sessions-store-reader", `sessions.json parse failed at ${sessionsJsonPath}: ${error.message}`);
|
|
4689
|
+
return [];
|
|
4690
|
+
}
|
|
4691
|
+
debugLog4(logger, "sessions-store-reader", `sessions.json read failed at ${sessionsJsonPath}: ${formatErrorMessage4(error)}`);
|
|
4692
|
+
return [];
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
function resolveSessionStorePath(candidatePath, sessionsDir) {
|
|
4696
|
+
return path5.isAbsolute(candidatePath) ? path5.resolve(candidatePath) : path5.resolve(sessionsDir, candidatePath);
|
|
4697
|
+
}
|
|
4698
|
+
function isRecord2(value) {
|
|
4699
|
+
return typeof value === "object" && value !== null;
|
|
4700
|
+
}
|
|
4701
|
+
function asTrimmedString(value) {
|
|
4702
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
4703
|
+
}
|
|
4704
|
+
function asFiniteNumber(value) {
|
|
4705
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
4706
|
+
}
|
|
4707
|
+
function debugLog4(logger, subsystem, message) {
|
|
4708
|
+
logger?.debug?.(`[agenr] ${subsystem}: ${message}`);
|
|
4709
|
+
}
|
|
4710
|
+
function isFileNotFound2(error) {
|
|
4711
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
4712
|
+
}
|
|
4713
|
+
function formatErrorMessage4(error) {
|
|
4714
|
+
if (error instanceof Error) {
|
|
4715
|
+
return error.message;
|
|
4716
|
+
}
|
|
4717
|
+
return String(error);
|
|
4718
|
+
}
|
|
4719
|
+
|
|
4720
|
+
// ../../src/adapters/openclaw/session/tui-lane.ts
|
|
4721
|
+
var TUI_SESSION_KEY_PATTERN = /^agent:([^:]+):([^:]+)$/i;
|
|
4722
|
+
var TUI_UUID_LANE_PATTERN = /^tui[a-z0-9]*-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4723
|
+
var TUI_UUID_SUFFIX_PATTERN = /-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4724
|
+
function parseTuiSessionKey(sessionKey) {
|
|
4725
|
+
const normalizedSessionKey = sessionKey.trim();
|
|
4726
|
+
if (normalizedSessionKey.length === 0) {
|
|
4727
|
+
return null;
|
|
4728
|
+
}
|
|
4729
|
+
const match = TUI_SESSION_KEY_PATTERN.exec(normalizedSessionKey);
|
|
4730
|
+
if (!match) {
|
|
4731
|
+
return null;
|
|
4732
|
+
}
|
|
4733
|
+
const [, agentId, instanceLane] = match;
|
|
4734
|
+
const normalizedAgentId = agentId?.trim();
|
|
4735
|
+
const normalizedInstanceLane = instanceLane?.trim();
|
|
4736
|
+
if (!normalizedAgentId || !normalizedInstanceLane || !normalizedInstanceLane.toLowerCase().startsWith("tui")) {
|
|
4737
|
+
return null;
|
|
4738
|
+
}
|
|
4739
|
+
const stableLane = TUI_UUID_LANE_PATTERN.test(normalizedInstanceLane) ? normalizedInstanceLane.replace(TUI_UUID_SUFFIX_PATTERN, "") : normalizedInstanceLane;
|
|
4740
|
+
return {
|
|
4741
|
+
agentId: normalizedAgentId,
|
|
4742
|
+
stableLane,
|
|
4743
|
+
instanceLane: normalizedInstanceLane
|
|
4744
|
+
};
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4747
|
+
// ../../src/adapters/openclaw/session/continuity/predecessor-resolver.ts
|
|
4748
|
+
async function resolveOpenClawSessionPredecessor(ctx, tracker, params) {
|
|
4749
|
+
const sessionContext = formatSessionContext2(ctx.sessionId, ctx.sessionKey);
|
|
4750
|
+
debugLog5(params.logger, "predecessor", `resolving predecessor for ${sessionContext}`);
|
|
4751
|
+
const tuiIdentity = parseTuiSessionKey(ctx.sessionKey ?? "");
|
|
4752
|
+
if (!tuiIdentity) {
|
|
4753
|
+
debugLog5(params.logger, "predecessor", `skipping TUI predecessor resolution for ${sessionContext}: current session key is not TUI`);
|
|
4754
|
+
return void 0;
|
|
4755
|
+
}
|
|
4756
|
+
const resumedFrom = tracker.getResumedFrom(ctx.sessionId);
|
|
4757
|
+
if (resumedFrom) {
|
|
4758
|
+
debugLog5(params.logger, "predecessor", `session_start resumedFrom for ${sessionContext}: ${resumedFrom}`);
|
|
4759
|
+
} else {
|
|
4760
|
+
debugLog5(params.logger, "predecessor", `session_start resumedFrom unavailable for ${sessionContext}`);
|
|
4761
|
+
}
|
|
4762
|
+
const sessionsDir = resolveOpenClawSessionsDirectory(ctx, tuiIdentity.agentId, params.resolveStateDir);
|
|
4763
|
+
if (!sessionsDir) {
|
|
4764
|
+
params.logger?.info?.(`[agenr] predecessor: TUI no predecessor found for ${sessionContext} reason=no_sessions_dir`);
|
|
4765
|
+
return void 0;
|
|
4766
|
+
}
|
|
4767
|
+
params.logger?.info?.(
|
|
4768
|
+
`[agenr] predecessor: TUI predecessor resolution for ${sessionContext} sessionKey=${ctx.sessionKey?.trim() ?? "unknown"} stableLane=${tuiIdentity.stableLane}`
|
|
4769
|
+
);
|
|
4770
|
+
debugLog5(
|
|
4771
|
+
params.logger,
|
|
4772
|
+
"predecessor",
|
|
4773
|
+
`TUI stable lane for ${sessionContext}: agentId=${tuiIdentity.agentId} instanceLane=${tuiIdentity.instanceLane} stableLane=${tuiIdentity.stableLane} sessionsDir=${sessionsDir}`
|
|
4774
|
+
);
|
|
4775
|
+
const predecessorResolution = await findTuiPredecessor(ctx.sessionKey ?? "", sessionsDir, resumedFrom, params.logger);
|
|
4776
|
+
if (!predecessorResolution.predecessor) {
|
|
4777
|
+
params.logger?.info?.(`[agenr] predecessor: TUI no predecessor found for ${sessionContext} reason=${predecessorResolution.reason}`);
|
|
4778
|
+
return void 0;
|
|
4779
|
+
}
|
|
4780
|
+
params.logger?.info?.(
|
|
4781
|
+
`[agenr] predecessor: TUI predecessor found for ${sessionContext} predecessorKey=${predecessorResolution.predecessor.sessionKey} predecessor=${predecessorResolution.predecessor.sessionFile}`
|
|
4782
|
+
);
|
|
4783
|
+
return {
|
|
4784
|
+
sessionFile: predecessorResolution.predecessor.sessionFile,
|
|
4785
|
+
sessionId: predecessorResolution.predecessor.sessionId
|
|
4786
|
+
};
|
|
4787
|
+
}
|
|
4788
|
+
async function findTuiPredecessor(currentSessionKey, sessionsDir, resumedFrom, logger) {
|
|
4789
|
+
const currentIdentity = parseTuiSessionKey(currentSessionKey);
|
|
4790
|
+
if (!currentIdentity) {
|
|
4791
|
+
return { reason: "not_tui_session_key" };
|
|
4792
|
+
}
|
|
4793
|
+
const entries = await readOpenClawSessionsStore(sessionsDir, logger);
|
|
4794
|
+
debugLog5(logger, "predecessor", `TUI sessions.json read result for sessionKey=${currentSessionKey}: entries=${entries.length}`);
|
|
4795
|
+
const sameAgentEntries = entries.filter((entry) => {
|
|
4796
|
+
const parsedCandidate = parseSingleLaneSessionKey(entry.sessionKey);
|
|
4797
|
+
if (!parsedCandidate) {
|
|
4798
|
+
debugLog5(logger, "predecessor", `TUI excluded candidate=${entry.sessionKey} reason=unsupported_session_key_shape`);
|
|
4799
|
+
return false;
|
|
4800
|
+
}
|
|
4801
|
+
if (parsedCandidate.agentId !== currentIdentity.agentId) {
|
|
4802
|
+
debugLog5(
|
|
4803
|
+
logger,
|
|
4804
|
+
"predecessor",
|
|
4805
|
+
`TUI excluded candidate=${entry.sessionKey} reason=agent_mismatch expected=${currentIdentity.agentId} actual=${parsedCandidate.agentId}`
|
|
4806
|
+
);
|
|
4807
|
+
return false;
|
|
4808
|
+
}
|
|
4809
|
+
return true;
|
|
4810
|
+
});
|
|
4811
|
+
debugLog5(logger, "predecessor", `TUI candidate filtering for sessionKey=${currentSessionKey}: sameAgentCount=${sameAgentEntries.length}`);
|
|
4812
|
+
if (resumedFrom) {
|
|
4813
|
+
const resumedFromMatch = sameAgentEntries.find((entry) => entry.sessionId === resumedFrom);
|
|
4814
|
+
if (resumedFromMatch) {
|
|
4815
|
+
if (!resumedFromMatch.sessionFile?.trim()) {
|
|
4816
|
+
debugLog5(
|
|
4817
|
+
logger,
|
|
4818
|
+
"predecessor",
|
|
4819
|
+
`TUI ignored session_start resumedFrom match for sessionKey=${currentSessionKey}: resumedFrom=${resumedFrom} reason=missing_session_file`
|
|
4820
|
+
);
|
|
4821
|
+
} else {
|
|
4822
|
+
const resolvedPredecessor = toResolvedTuiPredecessor(resumedFromMatch, logger);
|
|
4823
|
+
if (!resolvedPredecessor) {
|
|
4824
|
+
debugLog5(
|
|
4825
|
+
logger,
|
|
4826
|
+
"predecessor",
|
|
4827
|
+
`TUI ignored session_start resumedFrom match for sessionKey=${currentSessionKey}: resumedFrom=${resumedFrom} reason=missing_session_id`
|
|
4828
|
+
);
|
|
4829
|
+
} else {
|
|
4830
|
+
debugLog5(
|
|
4831
|
+
logger,
|
|
4832
|
+
"predecessor",
|
|
4833
|
+
`TUI matched session_start resumedFrom for sessionKey=${currentSessionKey}: resumedFrom=${resumedFrom} predecessorKey=${resumedFromMatch.sessionKey}`
|
|
4834
|
+
);
|
|
4835
|
+
return {
|
|
4836
|
+
reason: "resolved",
|
|
4837
|
+
predecessor: resolvedPredecessor
|
|
4838
|
+
};
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
} else {
|
|
4842
|
+
debugLog5(logger, "predecessor", `TUI found no session_start resumedFrom match for sessionKey=${currentSessionKey}: resumedFrom=${resumedFrom}`);
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4845
|
+
const laneMatches = sameAgentEntries.filter((entry) => {
|
|
4846
|
+
const normalizedCandidateKey = entry.sessionKey.trim();
|
|
4847
|
+
if (normalizedCandidateKey === currentSessionKey.trim()) {
|
|
4848
|
+
debugLog5(logger, "predecessor", `TUI excluded candidate=${entry.sessionKey} reason=current_session`);
|
|
4849
|
+
return false;
|
|
4850
|
+
}
|
|
4851
|
+
const candidateKey = parseSingleLaneSessionKey(entry.sessionKey);
|
|
4852
|
+
if (!candidateKey) {
|
|
4853
|
+
return false;
|
|
4854
|
+
}
|
|
4855
|
+
if (currentIdentity.stableLane === "tui" && candidateKey.lane === "main") {
|
|
4856
|
+
return true;
|
|
4857
|
+
}
|
|
4858
|
+
const candidateIdentity = parseTuiSessionKey(entry.sessionKey);
|
|
4859
|
+
if (!candidateIdentity) {
|
|
4860
|
+
debugLog5(logger, "predecessor", `TUI excluded candidate=${entry.sessionKey} reason=not_tui_candidate`);
|
|
4861
|
+
return false;
|
|
4862
|
+
}
|
|
4863
|
+
if (!isSameTuiLane(currentIdentity.stableLane, candidateIdentity.stableLane)) {
|
|
4864
|
+
debugLog5(
|
|
4865
|
+
logger,
|
|
4866
|
+
"predecessor",
|
|
4867
|
+
`TUI excluded candidate=${entry.sessionKey} reason=lane_mismatch currentStableLane=${currentIdentity.stableLane} candidateStableLane=${candidateIdentity.stableLane}`
|
|
4868
|
+
);
|
|
4869
|
+
return false;
|
|
4870
|
+
}
|
|
4871
|
+
return true;
|
|
4872
|
+
});
|
|
4873
|
+
debugLog5(logger, "predecessor", `TUI candidate filtering for sessionKey=${currentSessionKey}: laneMatchCount=${laneMatches.length}`);
|
|
4874
|
+
const sortedCandidates = laneMatches.filter((entry) => {
|
|
4875
|
+
if (!entry.sessionFile?.trim()) {
|
|
4876
|
+
debugLog5(logger, "predecessor", `TUI excluded candidate=${entry.sessionKey} reason=missing_session_file`);
|
|
4877
|
+
return false;
|
|
4878
|
+
}
|
|
4879
|
+
if (entry.updatedAt !== void 0) {
|
|
4880
|
+
return true;
|
|
4881
|
+
}
|
|
4882
|
+
debugLog5(logger, "predecessor", `TUI excluded candidate=${entry.sessionKey} reason=missing_updated_at`);
|
|
4883
|
+
return false;
|
|
4884
|
+
}).sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0));
|
|
4885
|
+
if (sortedCandidates.length === 0) {
|
|
4886
|
+
return { reason: "no_matching_sessions" };
|
|
4887
|
+
}
|
|
4888
|
+
for (const predecessor of sortedCandidates) {
|
|
4889
|
+
const resolvedPredecessor = toResolvedTuiPredecessor(predecessor, logger);
|
|
4890
|
+
if (!resolvedPredecessor) {
|
|
4891
|
+
debugLog5(
|
|
4892
|
+
logger,
|
|
4893
|
+
"predecessor",
|
|
4894
|
+
`TUI excluded candidate=${predecessor.sessionKey} reason=missing_session_id predecessor=${predecessor.sessionFile ?? "unknown"}`
|
|
4895
|
+
);
|
|
4896
|
+
continue;
|
|
4897
|
+
}
|
|
4898
|
+
return {
|
|
4899
|
+
reason: "resolved",
|
|
4900
|
+
predecessor: resolvedPredecessor
|
|
4901
|
+
};
|
|
4902
|
+
}
|
|
4903
|
+
return { reason: "missing_session_id" };
|
|
4904
|
+
}
|
|
4905
|
+
function resolvePredecessorSessionId(sessionId, sessionFile, logger) {
|
|
4906
|
+
const normalizedSessionId = sessionId?.trim();
|
|
4907
|
+
if (normalizedSessionId) {
|
|
4908
|
+
return normalizedSessionId;
|
|
3457
4909
|
}
|
|
3458
|
-
if (
|
|
3459
|
-
|
|
3460
|
-
return boundary >= 0 ? value.slice(boundary).trimStart() : value.trim();
|
|
4910
|
+
if (!sessionFile?.trim()) {
|
|
4911
|
+
return void 0;
|
|
3461
4912
|
}
|
|
3462
|
-
|
|
3463
|
-
return reversedBoundary >= 0 ? value.slice(0, reversedBoundary).trimEnd() : value.trim();
|
|
4913
|
+
return deriveOpenClawSessionIdFromFilePath(sessionFile, logger);
|
|
3464
4914
|
}
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
var RECENT_SESSION_MESSAGE_LIMIT = 6;
|
|
3469
|
-
var RECENT_SESSION_MAX_CHARS = 1800;
|
|
3470
|
-
var READ_TIME_SUMMARY_TIMEOUT_MS = 2e4;
|
|
3471
|
-
var READ_TIME_SUMMARY_TIMEOUT = /* @__PURE__ */ Symbol("read-time-summary-timeout");
|
|
3472
|
-
async function handleAgenrBeforePromptBuild(_event, ctx, params) {
|
|
3473
|
-
const sessionContext = formatSessionContext2(ctx.sessionId, ctx.sessionKey);
|
|
3474
|
-
const trackerState = params.tracker.consume(ctx.sessionId, ctx.sessionKey);
|
|
3475
|
-
if (!trackerState.isFirst) {
|
|
3476
|
-
debugLog5(params.logger, "before_prompt_build", `session tracker duplicate blocked for ${sessionContext}`);
|
|
3477
|
-
debugLog5(params.logger, "before_prompt_build", `session tracker active count=${trackerState.activeCount}`);
|
|
3478
|
-
params.logger.info(`[agenr] session-start recall skipped (already ran) for ${sessionContext}`);
|
|
4915
|
+
function toResolvedTuiPredecessor(candidate, logger) {
|
|
4916
|
+
const sessionFile = candidate.sessionFile?.trim();
|
|
4917
|
+
if (!sessionFile) {
|
|
3479
4918
|
return void 0;
|
|
3480
4919
|
}
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
params.logger.info(`[agenr] session-start recall for ${sessionContext}`);
|
|
3484
|
-
try {
|
|
3485
|
-
const services = await params.servicesPromise;
|
|
3486
|
-
const sessionStartRecall = await runAgenrSessionStartRecall(services);
|
|
3487
|
-
const previousSessionContext = await buildPreviousSessionContext(ctx, params.tracker, services, params.logger);
|
|
3488
|
-
const memoryContext = formatAgenrSessionStartRecall(sessionStartRecall);
|
|
3489
|
-
const prependContext = [previousSessionContext, memoryContext].filter((value) => value.trim().length > 0).join("\n\n");
|
|
3490
|
-
params.logger.info(`[agenr] session-start recall: ${sessionStartRecall.core.length} core entries for ${sessionContext}`);
|
|
3491
|
-
debugLog5(params.logger, "before_prompt_build", `session-start core entries for ${sessionContext}: ${formatEntryRefs(sessionStartRecall.core)}`);
|
|
3492
|
-
debugLog5(params.logger, "before_prompt_build", `session-start prependContext length for ${sessionContext}: ${prependContext.length} chars`);
|
|
3493
|
-
if (prependContext.length === 0) {
|
|
3494
|
-
params.logger.info(`[agenr] session-start recall: nothing to inject for ${sessionContext}`);
|
|
3495
|
-
return void 0;
|
|
3496
|
-
}
|
|
3497
|
-
return { prependContext };
|
|
3498
|
-
} catch (error) {
|
|
3499
|
-
params.logger.warn(`[agenr] session-start recall failed for ${sessionContext}: ${formatErrorMessage4(error)}`);
|
|
4920
|
+
const sessionId = resolvePredecessorSessionId(candidate.sessionId, sessionFile, logger);
|
|
4921
|
+
if (!sessionId) {
|
|
3500
4922
|
return void 0;
|
|
3501
4923
|
}
|
|
4924
|
+
return {
|
|
4925
|
+
sessionFile,
|
|
4926
|
+
sessionId,
|
|
4927
|
+
sessionKey: candidate.sessionKey
|
|
4928
|
+
};
|
|
3502
4929
|
}
|
|
3503
|
-
|
|
3504
|
-
const
|
|
4930
|
+
function resolveOpenClawSessionsDirectory(ctx, parsedAgentId, resolveStateDir) {
|
|
4931
|
+
const agentId = ctx.agentId?.trim() || parsedAgentId.trim();
|
|
4932
|
+
if (!agentId) {
|
|
4933
|
+
return void 0;
|
|
4934
|
+
}
|
|
4935
|
+
return path6.join(resolveStateDir(process.env), "agents", agentId, "sessions");
|
|
4936
|
+
}
|
|
4937
|
+
function parseSingleLaneSessionKey(sessionKey) {
|
|
4938
|
+
const match = /^agent:([^:]+):([^:]+)$/i.exec(sessionKey.trim());
|
|
4939
|
+
if (!match) {
|
|
4940
|
+
return null;
|
|
4941
|
+
}
|
|
4942
|
+
const [, agentId, lane] = match;
|
|
4943
|
+
const normalizedAgentId = agentId?.trim();
|
|
4944
|
+
const normalizedLane = lane?.trim();
|
|
4945
|
+
if (!normalizedAgentId || !normalizedLane) {
|
|
4946
|
+
return null;
|
|
4947
|
+
}
|
|
3505
4948
|
return {
|
|
3506
|
-
|
|
4949
|
+
agentId: normalizedAgentId,
|
|
4950
|
+
lane: normalizedLane
|
|
3507
4951
|
};
|
|
3508
4952
|
}
|
|
3509
|
-
|
|
3510
|
-
|
|
4953
|
+
function isSameTuiLane(currentStableLane, candidateStableLane) {
|
|
4954
|
+
if (currentStableLane === "tui") {
|
|
4955
|
+
return candidateStableLane.toLowerCase().startsWith("tui");
|
|
4956
|
+
}
|
|
4957
|
+
return currentStableLane === candidateStableLane;
|
|
4958
|
+
}
|
|
4959
|
+
function debugLog5(logger, subsystem, message) {
|
|
4960
|
+
logger?.debug?.(`[agenr] ${subsystem}: ${message}`);
|
|
4961
|
+
}
|
|
4962
|
+
function formatSessionContext2(sessionId, sessionKey) {
|
|
4963
|
+
const normalizedSessionId = sessionId?.trim();
|
|
4964
|
+
const normalizedSessionKey = sessionKey?.trim();
|
|
4965
|
+
if (normalizedSessionId && normalizedSessionKey) {
|
|
4966
|
+
return `session=${normalizedSessionId} key=${normalizedSessionKey}`;
|
|
4967
|
+
}
|
|
4968
|
+
if (normalizedSessionId) {
|
|
4969
|
+
return `session=${normalizedSessionId}`;
|
|
4970
|
+
}
|
|
4971
|
+
if (normalizedSessionKey) {
|
|
4972
|
+
return `key=${normalizedSessionKey}`;
|
|
4973
|
+
}
|
|
4974
|
+
return "session=unknown";
|
|
4975
|
+
}
|
|
4976
|
+
|
|
4977
|
+
// ../../src/adapters/openclaw/session/continuity/recent-session.ts
|
|
4978
|
+
var RECENT_SESSION_MESSAGE_LIMIT = 6;
|
|
4979
|
+
var RECENT_SESSION_MAX_CHARS = 1800;
|
|
4980
|
+
async function renderRecentSessionSection(sessionFile, logger) {
|
|
4981
|
+
try {
|
|
4982
|
+
const transcript = await openClawTranscriptParser.parseFile(sessionFile);
|
|
4983
|
+
const tail = transcript.messages.slice(-RECENT_SESSION_MESSAGE_LIMIT);
|
|
4984
|
+
const body = capRecentSession(tail.map((message) => `${message.role === "user" ? "U" : "A"}: ${message.text}`).join("\n"), RECENT_SESSION_MAX_CHARS);
|
|
4985
|
+
logger.debug?.(`[agenr] before_prompt_build: recent session tail for file=${sessionFile}: messages=${tail.length} chars=${body.length}`);
|
|
4986
|
+
return body;
|
|
4987
|
+
} catch (error) {
|
|
4988
|
+
logger.debug?.(`[agenr] before_prompt_build: failed to build recent session tail for file=${sessionFile}: ${formatErrorMessage5(error)}`);
|
|
4989
|
+
return "";
|
|
4990
|
+
}
|
|
4991
|
+
}
|
|
4992
|
+
function formatErrorMessage5(error) {
|
|
4993
|
+
return error instanceof Error ? error.message : String(error);
|
|
4994
|
+
}
|
|
4995
|
+
function capRecentSession(value, maxChars) {
|
|
4996
|
+
if (value.length <= maxChars) {
|
|
4997
|
+
return value;
|
|
4998
|
+
}
|
|
4999
|
+
const marker = "[...truncated earlier recent session...]\n";
|
|
5000
|
+
return `${marker}${value.slice(-(maxChars - marker.length)).trimStart()}`;
|
|
5001
|
+
}
|
|
5002
|
+
|
|
5003
|
+
// ../../src/adapters/openclaw/session/continuity/index.ts
|
|
5004
|
+
var READ_TIME_CONTINUITY_SUMMARY_TIMEOUT_MS = 2e4;
|
|
5005
|
+
var READ_TIME_CONTINUITY_SUMMARY_TIMEOUT = /* @__PURE__ */ Symbol("read-time-continuity-summary-timeout");
|
|
5006
|
+
async function resolvePredecessorContinuity(ctx, tracker, services, logger) {
|
|
5007
|
+
const sessionContext = formatSessionContext(ctx.sessionId, ctx.sessionKey);
|
|
3511
5008
|
const predecessor = await resolveOpenClawSessionPredecessor(ctx, tracker, {
|
|
3512
5009
|
logger,
|
|
3513
5010
|
resolveStateDir: services.openClaw.runtime.state.resolveStateDir
|
|
3514
5011
|
});
|
|
3515
5012
|
if (!predecessor) {
|
|
3516
|
-
logger.info(`[agenr] session-start predecessor summary not found for ${sessionContext} reason=no_predecessor`);
|
|
3517
|
-
return
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
if (summaryContent.length > 0) {
|
|
3522
|
-
sections.push(`## Previous session summary
|
|
3523
|
-
${summaryContent}`);
|
|
3524
|
-
}
|
|
3525
|
-
const recentSession = await renderRecentSessionSection(predecessor.sessionFile, logger);
|
|
3526
|
-
if (recentSession.length > 0) {
|
|
3527
|
-
sections.push(`## Recent session
|
|
3528
|
-
${recentSession}`);
|
|
5013
|
+
logger.info(`[agenr] session-start predecessor continuity summary not found for ${sessionContext} reason=no_predecessor`);
|
|
5014
|
+
return {
|
|
5015
|
+
continuitySummaryContent: "",
|
|
5016
|
+
recentSessionContent: ""
|
|
5017
|
+
};
|
|
3529
5018
|
}
|
|
3530
|
-
return
|
|
5019
|
+
return {
|
|
5020
|
+
predecessor,
|
|
5021
|
+
continuitySummaryContent: await loadPredecessorContinuitySummaryContent(sessionContext, predecessor.sessionFile, ctx.agentId, services, logger),
|
|
5022
|
+
recentSessionContent: await renderRecentSessionSection(predecessor.sessionFile, logger)
|
|
5023
|
+
};
|
|
3531
5024
|
}
|
|
3532
|
-
async function
|
|
5025
|
+
async function loadPredecessorContinuitySummaryContent(sessionContext, sessionFile, agentId, services, logger) {
|
|
3533
5026
|
try {
|
|
3534
|
-
const
|
|
3535
|
-
if (
|
|
5027
|
+
const existingContinuitySummary = await readOpenClawContinuitySummaryFile(sessionFile, logger);
|
|
5028
|
+
if (existingContinuitySummary) {
|
|
3536
5029
|
logger.info(
|
|
3537
|
-
`[agenr] session-start read-time summary generation skipped for ${sessionContext} predecessor=${sessionFile} reason=already_exists path=${
|
|
5030
|
+
`[agenr] session-start read-time continuity summary generation skipped for ${sessionContext} predecessor=${sessionFile} reason=already_exists path=${existingContinuitySummary.continuitySummaryPath}`
|
|
3538
5031
|
);
|
|
3539
|
-
logger.info(`[agenr] session-start predecessor summary found for ${sessionContext} path=${
|
|
3540
|
-
return
|
|
5032
|
+
logger.info(`[agenr] session-start predecessor continuity summary found for ${sessionContext} path=${existingContinuitySummary.continuitySummaryPath}`);
|
|
5033
|
+
return existingContinuitySummary.content;
|
|
3541
5034
|
}
|
|
3542
|
-
logger.info(`[agenr] session-start predecessor summary not found for ${sessionContext} predecessor=${sessionFile}`);
|
|
5035
|
+
logger.info(`[agenr] session-start predecessor continuity summary not found for ${sessionContext} predecessor=${sessionFile}`);
|
|
3543
5036
|
} catch (error) {
|
|
3544
|
-
logger.info(
|
|
3545
|
-
|
|
5037
|
+
logger.info(
|
|
5038
|
+
`[agenr] session-start predecessor continuity summary not found for ${sessionContext} predecessor=${sessionFile} reason=${formatErrorMessage2(error)}`
|
|
5039
|
+
);
|
|
5040
|
+
logger.debug?.(`[agenr] before_prompt_build: failed reading predecessor continuity summary for ${sessionContext}: ${formatErrorMessage2(error)}`);
|
|
3546
5041
|
return "";
|
|
3547
5042
|
}
|
|
3548
|
-
logger.info(
|
|
5043
|
+
logger.info(
|
|
5044
|
+
`[agenr] session-start read-time continuity summary generation triggered for ${sessionContext} predecessor=${sessionFile} reason=no_existing_continuity_summary`
|
|
5045
|
+
);
|
|
3549
5046
|
const startedAt = Date.now();
|
|
3550
5047
|
try {
|
|
3551
|
-
const result = await
|
|
3552
|
-
|
|
5048
|
+
const result = await awaitWithTimeout2(
|
|
5049
|
+
generateAndWriteOpenClawContinuitySummary({
|
|
3553
5050
|
sessionFile,
|
|
3554
5051
|
agentId,
|
|
3555
5052
|
openClaw: services.openClaw,
|
|
3556
5053
|
logger
|
|
3557
5054
|
}),
|
|
3558
|
-
|
|
5055
|
+
READ_TIME_CONTINUITY_SUMMARY_TIMEOUT_MS
|
|
3559
5056
|
);
|
|
3560
5057
|
const elapsedMs2 = Date.now() - startedAt;
|
|
3561
|
-
if (result ===
|
|
5058
|
+
if (result === READ_TIME_CONTINUITY_SUMMARY_TIMEOUT) {
|
|
3562
5059
|
logger.info(
|
|
3563
|
-
`[agenr] session-start read-time summary generation failed for ${sessionContext} predecessor=${sessionFile} reason=timeout elapsedMs=${elapsedMs2}`
|
|
5060
|
+
`[agenr] session-start read-time continuity summary generation failed for ${sessionContext} predecessor=${sessionFile} reason=timeout elapsedMs=${elapsedMs2}`
|
|
3564
5061
|
);
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
"before_prompt_build",
|
|
3568
|
-
`read-time summary generation timed out for ${sessionContext}: predecessor=${sessionFile} timeoutMs=${READ_TIME_SUMMARY_TIMEOUT_MS}`
|
|
5062
|
+
logger.debug?.(
|
|
5063
|
+
`[agenr] before_prompt_build: read-time continuity summary generation timed out for ${sessionContext}: predecessor=${sessionFile} timeoutMs=${READ_TIME_CONTINUITY_SUMMARY_TIMEOUT_MS}`
|
|
3569
5064
|
);
|
|
3570
5065
|
return "";
|
|
3571
5066
|
}
|
|
3572
|
-
return
|
|
5067
|
+
return handleReadTimeContinuitySummaryResult(sessionContext, sessionFile, result, elapsedMs2, logger);
|
|
3573
5068
|
} catch (error) {
|
|
3574
5069
|
const elapsedMs2 = Date.now() - startedAt;
|
|
3575
5070
|
logger.info(
|
|
3576
|
-
`[agenr] session-start read-time summary generation failed for ${sessionContext} predecessor=${sessionFile} reason=${
|
|
5071
|
+
`[agenr] session-start read-time continuity summary generation failed for ${sessionContext} predecessor=${sessionFile} reason=${formatErrorMessage2(error)} elapsedMs=${elapsedMs2}`
|
|
5072
|
+
);
|
|
5073
|
+
logger.debug?.(
|
|
5074
|
+
`[agenr] before_prompt_build: unexpected read-time continuity summary generation failure for ${sessionContext}: ${formatErrorMessage2(error)}`
|
|
3577
5075
|
);
|
|
3578
|
-
debugLog5(logger, "before_prompt_build", `unexpected read-time summary generation failure for ${sessionContext}: ${formatErrorMessage4(error)}`);
|
|
3579
5076
|
return "";
|
|
3580
5077
|
}
|
|
3581
5078
|
}
|
|
3582
|
-
function
|
|
3583
|
-
if (result.status === "written" && result.content && result.
|
|
5079
|
+
function handleReadTimeContinuitySummaryResult(sessionContext, sessionFile, result, elapsedMs2, logger) {
|
|
5080
|
+
if (result.status === "written" && result.content && result.continuitySummaryPath) {
|
|
3584
5081
|
logger.info(
|
|
3585
|
-
`[agenr] session-start read-time summary generation completed for ${sessionContext} predecessor=${sessionFile} elapsedMs=${elapsedMs2} path=${result.
|
|
5082
|
+
`[agenr] session-start read-time continuity summary generation completed for ${sessionContext} predecessor=${sessionFile} elapsedMs=${elapsedMs2} path=${result.continuitySummaryPath}`
|
|
3586
5083
|
);
|
|
3587
|
-
logger.info(`[agenr] session-start predecessor summary found for ${sessionContext} path=${result.
|
|
5084
|
+
logger.info(`[agenr] session-start predecessor continuity summary found for ${sessionContext} path=${result.continuitySummaryPath}`);
|
|
3588
5085
|
return result.content;
|
|
3589
5086
|
}
|
|
3590
5087
|
if (result.status === "skipped") {
|
|
3591
5088
|
logger.info(
|
|
3592
|
-
`[agenr] session-start read-time summary generation skipped for ${sessionContext} predecessor=${sessionFile} reason=${result.reason ?? "unknown"} path=${result.
|
|
5089
|
+
`[agenr] session-start read-time continuity summary generation skipped for ${sessionContext} predecessor=${sessionFile} reason=${result.reason ?? "unknown"} path=${result.continuitySummaryPath ?? "n/a"}`
|
|
3593
5090
|
);
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
"before_prompt_build",
|
|
3597
|
-
`read-time summary generation skipped for ${sessionContext}: predecessor=${sessionFile} transcriptChars=${result.transcriptChars ?? 0} cleanedMessages=${result.messageCount ?? 0}`
|
|
5091
|
+
logger.debug?.(
|
|
5092
|
+
`[agenr] before_prompt_build: read-time continuity summary generation skipped for ${sessionContext}: predecessor=${sessionFile} transcriptChars=${result.transcriptChars ?? 0} cleanedMessages=${result.messageCount ?? 0}`
|
|
3598
5093
|
);
|
|
3599
|
-
if (result.reason === "already_exists" && result.content && result.
|
|
3600
|
-
logger.info(`[agenr] session-start predecessor summary found for ${sessionContext} path=${result.
|
|
5094
|
+
if (result.reason === "already_exists" && result.content && result.continuitySummaryPath) {
|
|
5095
|
+
logger.info(`[agenr] session-start predecessor continuity summary found for ${sessionContext} path=${result.continuitySummaryPath}`);
|
|
3601
5096
|
return result.content;
|
|
3602
5097
|
}
|
|
3603
5098
|
return "";
|
|
3604
5099
|
}
|
|
3605
5100
|
logger.info(
|
|
3606
|
-
`[agenr] session-start read-time summary generation failed for ${sessionContext} predecessor=${sessionFile} reason=${result.reason ?? "unknown"} elapsedMs=${elapsedMs2} model=${result.model ?? "unknown"}`
|
|
5101
|
+
`[agenr] session-start read-time continuity summary generation failed for ${sessionContext} predecessor=${sessionFile} reason=${result.reason ?? "unknown"} elapsedMs=${elapsedMs2} model=${result.model ?? "unknown"}`
|
|
3607
5102
|
);
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
"before_prompt_build",
|
|
3611
|
-
`read-time summary generation failed for ${sessionContext}: predecessor=${sessionFile} durationMs=${result.durationMs ?? 0} transcriptChars=${result.transcriptChars ?? 0}`
|
|
5103
|
+
logger.debug?.(
|
|
5104
|
+
`[agenr] before_prompt_build: read-time continuity summary generation failed for ${sessionContext}: predecessor=${sessionFile} durationMs=${result.durationMs ?? 0} transcriptChars=${result.transcriptChars ?? 0}`
|
|
3612
5105
|
);
|
|
3613
5106
|
return "";
|
|
3614
5107
|
}
|
|
3615
|
-
async function
|
|
3616
|
-
try {
|
|
3617
|
-
const transcript = await openClawTranscriptParser.parseFile(sessionFile);
|
|
3618
|
-
const tail = transcript.messages.slice(-RECENT_SESSION_MESSAGE_LIMIT);
|
|
3619
|
-
const body = capRecentSession(tail.map((message) => `${message.role === "user" ? "U" : "A"}: ${message.text}`).join("\n"), RECENT_SESSION_MAX_CHARS);
|
|
3620
|
-
debugLog5(logger, "before_prompt_build", `recent session tail for file=${sessionFile}: messages=${tail.length} chars=${body.length}`);
|
|
3621
|
-
return body;
|
|
3622
|
-
} catch (error) {
|
|
3623
|
-
debugLog5(logger, "before_prompt_build", `failed to build recent session tail for file=${sessionFile}: ${formatErrorMessage4(error)}`);
|
|
3624
|
-
return "";
|
|
3625
|
-
}
|
|
3626
|
-
}
|
|
3627
|
-
async function awaitWithTimeout(promise, timeoutMs) {
|
|
5108
|
+
async function awaitWithTimeout2(promise, timeoutMs) {
|
|
3628
5109
|
return new Promise((resolve, reject) => {
|
|
3629
5110
|
const timeout = setTimeout(() => {
|
|
3630
|
-
resolve(
|
|
5111
|
+
resolve(READ_TIME_CONTINUITY_SUMMARY_TIMEOUT);
|
|
3631
5112
|
}, timeoutMs);
|
|
3632
5113
|
promise.then(
|
|
3633
5114
|
(value) => {
|
|
@@ -3641,121 +5122,66 @@ async function awaitWithTimeout(promise, timeoutMs) {
|
|
|
3641
5122
|
);
|
|
3642
5123
|
});
|
|
3643
5124
|
}
|
|
3644
|
-
function debugLog5(logger, subsystem, message) {
|
|
3645
|
-
logger.debug?.(`[agenr] ${subsystem}: ${message}`);
|
|
3646
|
-
}
|
|
3647
|
-
function formatSessionContext2(sessionId, sessionKey) {
|
|
3648
|
-
const normalizedSessionId = sessionId?.trim();
|
|
3649
|
-
const normalizedSessionKey = sessionKey?.trim();
|
|
3650
|
-
if (normalizedSessionId && normalizedSessionKey) {
|
|
3651
|
-
return `session=${normalizedSessionId} key=${normalizedSessionKey}`;
|
|
3652
|
-
}
|
|
3653
|
-
if (normalizedSessionId) {
|
|
3654
|
-
return `session=${normalizedSessionId}`;
|
|
3655
|
-
}
|
|
3656
|
-
if (normalizedSessionKey) {
|
|
3657
|
-
return `key=${normalizedSessionKey}`;
|
|
3658
|
-
}
|
|
3659
|
-
return "session=unknown";
|
|
3660
|
-
}
|
|
3661
|
-
function formatEntryRefs(entries) {
|
|
3662
|
-
if (entries.length === 0) {
|
|
3663
|
-
return "none";
|
|
3664
|
-
}
|
|
3665
|
-
return entries.map((entry) => `${entry.subject} [${entry.id}]`).join(", ");
|
|
3666
|
-
}
|
|
3667
|
-
function formatErrorMessage4(error) {
|
|
3668
|
-
if (error instanceof Error) {
|
|
3669
|
-
return error.message;
|
|
3670
|
-
}
|
|
3671
|
-
return String(error);
|
|
3672
|
-
}
|
|
3673
|
-
function capRecentSession(value, maxChars) {
|
|
3674
|
-
if (value.length <= maxChars) {
|
|
3675
|
-
return value;
|
|
3676
|
-
}
|
|
3677
|
-
const marker = "[...truncated earlier recent session...]\n";
|
|
3678
|
-
return `${marker}${value.slice(-(maxChars - marker.length)).trimStart()}`;
|
|
3679
|
-
}
|
|
3680
5125
|
|
|
3681
|
-
// ../../src/adapters/openclaw/hooks/before-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
const
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
5126
|
+
// ../../src/adapters/openclaw/hooks/before-prompt-build.ts
|
|
5127
|
+
var CORE_ENTRY_LIMIT = 4;
|
|
5128
|
+
var INTERNAL_AGENR_SESSION_PREFIX = "temp:agenr-";
|
|
5129
|
+
async function handleAgenrBeforePromptBuild(_event, ctx, params) {
|
|
5130
|
+
const sessionContext = formatSessionContext(ctx.sessionId, ctx.sessionKey);
|
|
5131
|
+
const trackerState = params.tracker.consume(ctx.sessionId, ctx.sessionKey);
|
|
5132
|
+
if (!trackerState.isFirst) {
|
|
5133
|
+
params.logger.debug?.(`[agenr] before_prompt_build: session tracker duplicate blocked for ${sessionContext}`);
|
|
5134
|
+
params.logger.debug?.(`[agenr] before_prompt_build: session tracker active count=${trackerState.activeCount}`);
|
|
5135
|
+
params.logger.info(`[agenr] session-start recall skipped (already ran) for ${sessionContext}`);
|
|
5136
|
+
return void 0;
|
|
3690
5137
|
}
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
});
|
|
3696
|
-
params.logger.
|
|
3697
|
-
|
|
3698
|
-
);
|
|
3699
|
-
debugLog6(
|
|
3700
|
-
params.logger,
|
|
3701
|
-
"before_reset",
|
|
3702
|
-
`summary request details for ${sessionContext}: file=${sessionFile} workspace=${ctx.workspaceDir ?? "unknown"} reason=${event.reason ?? "unknown"}`
|
|
3703
|
-
);
|
|
5138
|
+
if (isInternalAgenrSession(ctx.sessionKey)) {
|
|
5139
|
+
params.logger.debug?.(`[agenr] before_prompt_build: skipping pipeline for internal session ${sessionContext}`);
|
|
5140
|
+
return void 0;
|
|
5141
|
+
}
|
|
5142
|
+
params.logger.debug?.(`[agenr] before_prompt_build: session tracker first start for ${sessionContext}`);
|
|
5143
|
+
params.logger.debug?.(`[agenr] before_prompt_build: session tracker active count=${trackerState.activeCount}`);
|
|
5144
|
+
params.logger.info(`[agenr] session-start recall for ${sessionContext}`);
|
|
3704
5145
|
try {
|
|
3705
5146
|
const services = await params.servicesPromise;
|
|
3706
|
-
const
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
5147
|
+
const continuity = await resolvePredecessorContinuity(ctx, params.tracker, services, params.logger);
|
|
5148
|
+
void writeOpenClawPredecessorEpisode({
|
|
5149
|
+
ctx,
|
|
5150
|
+
predecessor: continuity.predecessor,
|
|
5151
|
+
services,
|
|
3710
5152
|
logger: params.logger
|
|
3711
5153
|
});
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
5154
|
+
const sessionStartRecall = await runAgenrSessionStartRecall(services);
|
|
5155
|
+
const memoryContext = formatAgenrSessionStartRecall(sessionStartRecall);
|
|
5156
|
+
const sections = [
|
|
5157
|
+
continuity.continuitySummaryContent && `## Previous session summary
|
|
5158
|
+
${continuity.continuitySummaryContent}`,
|
|
5159
|
+
continuity.recentSessionContent && `## Recent session
|
|
5160
|
+
${continuity.recentSessionContent}`,
|
|
5161
|
+
memoryContext
|
|
5162
|
+
].filter((value) => Boolean(value && value.trim().length > 0));
|
|
5163
|
+
const prependContext = sections.join("\n\n");
|
|
5164
|
+
params.logger.info(`[agenr] session-start recall: ${sessionStartRecall.core.length} core entries for ${sessionContext}`);
|
|
5165
|
+
params.logger.debug?.(`[agenr] before_prompt_build: session-start core entries for ${sessionContext}: ${formatEntryRefs(sessionStartRecall.core)}`);
|
|
5166
|
+
params.logger.debug?.(`[agenr] before_prompt_build: session-start prependContext length for ${sessionContext}: ${prependContext.length} chars`);
|
|
5167
|
+
if (prependContext.length === 0) {
|
|
5168
|
+
params.logger.info(`[agenr] session-start recall: nothing to inject for ${sessionContext}`);
|
|
5169
|
+
return void 0;
|
|
3726
5170
|
}
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
);
|
|
3730
|
-
|
|
3731
|
-
params.logger,
|
|
3732
|
-
"before_reset",
|
|
3733
|
-
`failure details for ${sessionContext}: summaryPath=${result.summaryPath ?? "n/a"} durationMs=${result.durationMs ?? 0} transcriptChars=${result.transcriptChars ?? 0}`
|
|
3734
|
-
);
|
|
3735
|
-
} catch (error) {
|
|
3736
|
-
params.logger.info(`[agenr] before_reset: summary generation failed for ${sessionContext} reason=${formatErrorMessage5(error)} model=unknown`);
|
|
3737
|
-
debugLog6(params.logger, "before_reset", `unexpected failure for ${sessionContext}: ${formatErrorMessage5(error)}`);
|
|
3738
|
-
}
|
|
3739
|
-
}
|
|
3740
|
-
function debugLog6(logger, subsystem, message) {
|
|
3741
|
-
logger.debug?.(`[agenr] ${subsystem}: ${message}`);
|
|
3742
|
-
}
|
|
3743
|
-
function formatErrorMessage5(error) {
|
|
3744
|
-
return error instanceof Error ? error.message : String(error);
|
|
3745
|
-
}
|
|
3746
|
-
function formatSessionContext3(sessionId, sessionKey) {
|
|
3747
|
-
const normalizedSessionId = sessionId?.trim();
|
|
3748
|
-
const normalizedSessionKey = sessionKey?.trim();
|
|
3749
|
-
if (normalizedSessionId && normalizedSessionKey) {
|
|
3750
|
-
return `session=${normalizedSessionId} key=${normalizedSessionKey}`;
|
|
3751
|
-
}
|
|
3752
|
-
if (normalizedSessionId) {
|
|
3753
|
-
return `session=${normalizedSessionId}`;
|
|
3754
|
-
}
|
|
3755
|
-
if (normalizedSessionKey) {
|
|
3756
|
-
return `key=${normalizedSessionKey}`;
|
|
5171
|
+
return { prependContext };
|
|
5172
|
+
} catch (error) {
|
|
5173
|
+
params.logger.warn(`[agenr] session-start recall failed for ${sessionContext}: ${formatErrorMessage2(error)}`);
|
|
5174
|
+
return void 0;
|
|
3757
5175
|
}
|
|
3758
|
-
|
|
5176
|
+
}
|
|
5177
|
+
async function runAgenrSessionStartRecall(services) {
|
|
5178
|
+
return { core: await listOpenClawCoreEntries(services.database, CORE_ENTRY_LIMIT) };
|
|
5179
|
+
}
|
|
5180
|
+
function formatEntryRefs(entries) {
|
|
5181
|
+
return entries.length === 0 ? "none" : entries.map((entry) => `${entry.subject} [${entry.id}]`).join(", ");
|
|
5182
|
+
}
|
|
5183
|
+
function isInternalAgenrSession(sessionKey) {
|
|
5184
|
+
return Boolean(sessionKey?.startsWith(INTERNAL_AGENR_SESSION_PREFIX));
|
|
3759
5185
|
}
|
|
3760
5186
|
|
|
3761
5187
|
// ../../src/adapters/openclaw/memory/flush-plan.ts
|
|
@@ -3946,8 +5372,9 @@ async function sleep(durationMs) {
|
|
|
3946
5372
|
}
|
|
3947
5373
|
|
|
3948
5374
|
// ../../src/adapters/db/schema.ts
|
|
3949
|
-
var SCHEMA_VERSION = "
|
|
5375
|
+
var SCHEMA_VERSION = "4";
|
|
3950
5376
|
var VECTOR_INDEX_NAME = "idx_entries_embedding";
|
|
5377
|
+
var EPISODE_VECTOR_INDEX_NAME = "idx_episodes_embedding";
|
|
3951
5378
|
var BULK_WRITE_STATE_META_KEY = "bulk_write_state";
|
|
3952
5379
|
var CREATE_ENTRIES_TABLE_SQL = `
|
|
3953
5380
|
CREATE TABLE IF NOT EXISTS entries (
|
|
@@ -3969,6 +5396,8 @@ var CREATE_ENTRIES_TABLE_SQL = `
|
|
|
3969
5396
|
last_recalled_at TEXT,
|
|
3970
5397
|
superseded_by TEXT REFERENCES entries(id),
|
|
3971
5398
|
cluster_id TEXT,
|
|
5399
|
+
user_id TEXT,
|
|
5400
|
+
project TEXT,
|
|
3972
5401
|
retired INTEGER NOT NULL DEFAULT 0,
|
|
3973
5402
|
retired_at TEXT,
|
|
3974
5403
|
retired_reason TEXT,
|
|
@@ -4017,6 +5446,51 @@ var CREATE_INGEST_LOG_TABLE_SQL = `
|
|
|
4017
5446
|
entry_count INTEGER DEFAULT 0
|
|
4018
5447
|
)
|
|
4019
5448
|
`;
|
|
5449
|
+
var CREATE_EPISODES_TABLE_SQL = `
|
|
5450
|
+
CREATE TABLE IF NOT EXISTS episodes (
|
|
5451
|
+
id TEXT PRIMARY KEY,
|
|
5452
|
+
source TEXT NOT NULL,
|
|
5453
|
+
source_id TEXT,
|
|
5454
|
+
source_ref TEXT,
|
|
5455
|
+
transcript_hash TEXT,
|
|
5456
|
+
summary_hash TEXT,
|
|
5457
|
+
agent_id TEXT,
|
|
5458
|
+
surface TEXT,
|
|
5459
|
+
started_at TEXT NOT NULL,
|
|
5460
|
+
ended_at TEXT,
|
|
5461
|
+
summary TEXT NOT NULL,
|
|
5462
|
+
tags TEXT,
|
|
5463
|
+
activity_level TEXT,
|
|
5464
|
+
user_id TEXT,
|
|
5465
|
+
project TEXT,
|
|
5466
|
+
gen_model TEXT,
|
|
5467
|
+
gen_version TEXT,
|
|
5468
|
+
message_count INTEGER,
|
|
5469
|
+
embedding F32_BLOB(1024),
|
|
5470
|
+
retired INTEGER NOT NULL DEFAULT 0,
|
|
5471
|
+
retired_at TEXT,
|
|
5472
|
+
retired_reason TEXT,
|
|
5473
|
+
superseded_by TEXT REFERENCES episodes(id),
|
|
5474
|
+
created_at TEXT NOT NULL,
|
|
5475
|
+
updated_at TEXT NOT NULL
|
|
5476
|
+
)
|
|
5477
|
+
`;
|
|
5478
|
+
var CREATE_TASKS_TABLE_SQL = `
|
|
5479
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
5480
|
+
id TEXT PRIMARY KEY,
|
|
5481
|
+
subject TEXT NOT NULL,
|
|
5482
|
+
content TEXT NOT NULL,
|
|
5483
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
5484
|
+
priority INTEGER NOT NULL DEFAULT 5,
|
|
5485
|
+
tags TEXT,
|
|
5486
|
+
source_context TEXT,
|
|
5487
|
+
project TEXT,
|
|
5488
|
+
created_at TEXT NOT NULL,
|
|
5489
|
+
updated_at TEXT NOT NULL,
|
|
5490
|
+
completed_at TEXT,
|
|
5491
|
+
due_at TEXT
|
|
5492
|
+
)
|
|
5493
|
+
`;
|
|
4020
5494
|
var CREATE_RECALL_EVENTS_TABLE_SQL = `
|
|
4021
5495
|
CREATE TABLE IF NOT EXISTS recall_events (
|
|
4022
5496
|
id TEXT PRIMARY KEY,
|
|
@@ -4102,6 +5576,43 @@ var CREATE_ENTRIES_CREATED_AT_INDEX_SQL = `
|
|
|
4102
5576
|
CREATE INDEX IF NOT EXISTS idx_entries_created_at
|
|
4103
5577
|
ON entries(created_at)
|
|
4104
5578
|
`;
|
|
5579
|
+
var CREATE_EPISODES_STARTED_AT_INDEX_SQL = `
|
|
5580
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_started_at
|
|
5581
|
+
ON episodes(started_at)
|
|
5582
|
+
`;
|
|
5583
|
+
var CREATE_EPISODES_ENDED_AT_INDEX_SQL = `
|
|
5584
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_ended_at
|
|
5585
|
+
ON episodes(ended_at)
|
|
5586
|
+
`;
|
|
5587
|
+
var CREATE_EPISODES_SOURCE_INDEX_SQL = `
|
|
5588
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_source
|
|
5589
|
+
ON episodes(source)
|
|
5590
|
+
`;
|
|
5591
|
+
var CREATE_EPISODES_SOURCE_ID_INDEX_SQL = `
|
|
5592
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_source_id
|
|
5593
|
+
ON episodes(source_id)
|
|
5594
|
+
`;
|
|
5595
|
+
var CREATE_EPISODES_RETIRED_INDEX_SQL = `
|
|
5596
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_retired
|
|
5597
|
+
ON episodes(retired)
|
|
5598
|
+
`;
|
|
5599
|
+
var CREATE_EPISODES_SOURCE_SOURCE_ID_UNIQUE_INDEX_SQL = `
|
|
5600
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_episodes_source_source_id
|
|
5601
|
+
ON episodes(source, source_id)
|
|
5602
|
+
WHERE source_id IS NOT NULL
|
|
5603
|
+
`;
|
|
5604
|
+
var CREATE_TASKS_STATUS_INDEX_SQL = `
|
|
5605
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status
|
|
5606
|
+
ON tasks(status)
|
|
5607
|
+
`;
|
|
5608
|
+
var CREATE_TASKS_CREATED_AT_INDEX_SQL = `
|
|
5609
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_created_at
|
|
5610
|
+
ON tasks(created_at)
|
|
5611
|
+
`;
|
|
5612
|
+
var CREATE_TASKS_PROJECT_INDEX_SQL = `
|
|
5613
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_project
|
|
5614
|
+
ON tasks(project)
|
|
5615
|
+
`;
|
|
4105
5616
|
var CREATE_RECALL_EVENTS_ENTRY_ID_INDEX_SQL = `
|
|
4106
5617
|
CREATE INDEX IF NOT EXISTS idx_recall_events_entry_id
|
|
4107
5618
|
ON recall_events(entry_id)
|
|
@@ -4123,6 +5634,19 @@ var CREATE_ENTRIES_EMBEDDING_INDEX_SQL = `
|
|
|
4123
5634
|
AND retired = 0
|
|
4124
5635
|
AND superseded_by IS NULL
|
|
4125
5636
|
`;
|
|
5637
|
+
var CREATE_EPISODES_EMBEDDING_INDEX_SQL = `
|
|
5638
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_embedding ON episodes (
|
|
5639
|
+
libsql_vector_idx(
|
|
5640
|
+
embedding,
|
|
5641
|
+
'metric=cosine',
|
|
5642
|
+
'compress_neighbors=float8',
|
|
5643
|
+
'max_neighbors=50'
|
|
5644
|
+
)
|
|
5645
|
+
)
|
|
5646
|
+
WHERE embedding IS NOT NULL
|
|
5647
|
+
AND retired = 0
|
|
5648
|
+
AND superseded_by IS NULL
|
|
5649
|
+
`;
|
|
4126
5650
|
var SCHEMA_STATEMENTS = [
|
|
4127
5651
|
CREATE_ENTRIES_TABLE_SQL,
|
|
4128
5652
|
CREATE_ENTRIES_FTS_TABLE_SQL,
|
|
@@ -4130,6 +5654,8 @@ var SCHEMA_STATEMENTS = [
|
|
|
4130
5654
|
CREATE_ENTRIES_FTS_DELETE_TRIGGER_SQL,
|
|
4131
5655
|
CREATE_ENTRIES_FTS_UPDATE_TRIGGER_SQL,
|
|
4132
5656
|
CREATE_INGEST_LOG_TABLE_SQL,
|
|
5657
|
+
CREATE_EPISODES_TABLE_SQL,
|
|
5658
|
+
CREATE_TASKS_TABLE_SQL,
|
|
4133
5659
|
CREATE_RECALL_EVENTS_TABLE_SQL,
|
|
4134
5660
|
CREATE_SURGEON_RUNS_TABLE_SQL,
|
|
4135
5661
|
CREATE_META_TABLE_SQL,
|
|
@@ -4139,6 +5665,15 @@ var SCHEMA_STATEMENTS = [
|
|
|
4139
5665
|
CREATE_ENTRIES_EXPIRY_INDEX_SQL,
|
|
4140
5666
|
CREATE_ENTRIES_RETIRED_INDEX_SQL,
|
|
4141
5667
|
CREATE_ENTRIES_CREATED_AT_INDEX_SQL,
|
|
5668
|
+
CREATE_EPISODES_STARTED_AT_INDEX_SQL,
|
|
5669
|
+
CREATE_EPISODES_ENDED_AT_INDEX_SQL,
|
|
5670
|
+
CREATE_EPISODES_SOURCE_INDEX_SQL,
|
|
5671
|
+
CREATE_EPISODES_SOURCE_ID_INDEX_SQL,
|
|
5672
|
+
CREATE_EPISODES_RETIRED_INDEX_SQL,
|
|
5673
|
+
CREATE_EPISODES_SOURCE_SOURCE_ID_UNIQUE_INDEX_SQL,
|
|
5674
|
+
CREATE_TASKS_STATUS_INDEX_SQL,
|
|
5675
|
+
CREATE_TASKS_CREATED_AT_INDEX_SQL,
|
|
5676
|
+
CREATE_TASKS_PROJECT_INDEX_SQL,
|
|
4142
5677
|
CREATE_RECALL_EVENTS_ENTRY_ID_INDEX_SQL,
|
|
4143
5678
|
CREATE_RECALL_EVENTS_RECALLED_AT_INDEX_SQL
|
|
4144
5679
|
];
|
|
@@ -4150,6 +5685,12 @@ async function initSchema(db) {
|
|
|
4150
5685
|
await db.execute(statement);
|
|
4151
5686
|
}
|
|
4152
5687
|
await ensureSurgeonSchema(db);
|
|
5688
|
+
if (currentVersion === "2") {
|
|
5689
|
+
await migrateSchemaV2ToV3(db);
|
|
5690
|
+
}
|
|
5691
|
+
if (currentVersion === "3") {
|
|
5692
|
+
await migrateSchemaV3ToV4(db);
|
|
5693
|
+
}
|
|
4153
5694
|
await db.execute({
|
|
4154
5695
|
sql: `
|
|
4155
5696
|
INSERT INTO _meta (key, value)
|
|
@@ -4165,7 +5706,7 @@ async function initSchema(db) {
|
|
|
4165
5706
|
if (currentVersion !== SCHEMA_VERSION || !hadEntriesFts) {
|
|
4166
5707
|
await rebuildFts(db);
|
|
4167
5708
|
}
|
|
4168
|
-
await
|
|
5709
|
+
await ensureVectorIndexes(db);
|
|
4169
5710
|
}
|
|
4170
5711
|
async function ensureSurgeonSchema(db) {
|
|
4171
5712
|
const columns = await db.execute("PRAGMA table_info('surgeon_runs')");
|
|
@@ -4206,6 +5747,44 @@ async function ensureSurgeonSchema(db) {
|
|
|
4206
5747
|
await db.execute(CREATE_SURGEON_RUN_ACTIONS_ENTRY_ID_INDEX_SQL);
|
|
4207
5748
|
await db.execute(CREATE_SURGEON_RUN_ACTIONS_CREATED_AT_INDEX_SQL);
|
|
4208
5749
|
}
|
|
5750
|
+
async function migrateSchemaV2ToV3(db) {
|
|
5751
|
+
await runImmediateTransaction(db, async () => {
|
|
5752
|
+
await db.execute("DROP TRIGGER IF EXISTS entries_ai");
|
|
5753
|
+
await db.execute("DROP TRIGGER IF EXISTS entries_ad");
|
|
5754
|
+
await db.execute("DROP TRIGGER IF EXISTS entries_au");
|
|
5755
|
+
await db.execute("UPDATE entries SET type = 'milestone' WHERE type = 'event'");
|
|
5756
|
+
await db.execute(`
|
|
5757
|
+
INSERT INTO tasks (id, subject, content, status, priority, tags, source_context, created_at, updated_at)
|
|
5758
|
+
SELECT id, subject, content, 'open', importance, tags, source_context, created_at, updated_at
|
|
5759
|
+
FROM entries
|
|
5760
|
+
WHERE type = 'todo' AND retired = 0
|
|
5761
|
+
`);
|
|
5762
|
+
await db.execute("DELETE FROM entries WHERE type = 'todo'");
|
|
5763
|
+
await db.execute("DELETE FROM entries WHERE type = 'reflection'");
|
|
5764
|
+
if (!await columnExists(db, "entries", "user_id")) {
|
|
5765
|
+
await db.execute("ALTER TABLE entries ADD COLUMN user_id TEXT");
|
|
5766
|
+
}
|
|
5767
|
+
if (!await columnExists(db, "entries", "project")) {
|
|
5768
|
+
await db.execute("ALTER TABLE entries ADD COLUMN project TEXT");
|
|
5769
|
+
}
|
|
5770
|
+
await db.execute(CREATE_ENTRIES_FTS_INSERT_TRIGGER_SQL);
|
|
5771
|
+
await db.execute(CREATE_ENTRIES_FTS_DELETE_TRIGGER_SQL);
|
|
5772
|
+
await db.execute(CREATE_ENTRIES_FTS_UPDATE_TRIGGER_SQL);
|
|
5773
|
+
});
|
|
5774
|
+
}
|
|
5775
|
+
async function migrateSchemaV3ToV4(db) {
|
|
5776
|
+
for (const statement of [
|
|
5777
|
+
CREATE_EPISODES_TABLE_SQL,
|
|
5778
|
+
CREATE_EPISODES_STARTED_AT_INDEX_SQL,
|
|
5779
|
+
CREATE_EPISODES_ENDED_AT_INDEX_SQL,
|
|
5780
|
+
CREATE_EPISODES_SOURCE_INDEX_SQL,
|
|
5781
|
+
CREATE_EPISODES_SOURCE_ID_INDEX_SQL,
|
|
5782
|
+
CREATE_EPISODES_RETIRED_INDEX_SQL,
|
|
5783
|
+
CREATE_EPISODES_SOURCE_SOURCE_ID_UNIQUE_INDEX_SQL
|
|
5784
|
+
]) {
|
|
5785
|
+
await db.execute(statement);
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
4209
5788
|
async function rebuildFts(db) {
|
|
4210
5789
|
await db.execute("INSERT INTO entries_fts(entries_fts) VALUES ('rebuild')");
|
|
4211
5790
|
}
|
|
@@ -4222,7 +5801,7 @@ async function prepareBulkWrites(db) {
|
|
|
4222
5801
|
await db.execute("DROP TRIGGER IF EXISTS entries_ai");
|
|
4223
5802
|
await db.execute("DROP TRIGGER IF EXISTS entries_ad");
|
|
4224
5803
|
await db.execute("DROP TRIGGER IF EXISTS entries_au");
|
|
4225
|
-
await
|
|
5804
|
+
await dropVectorIndexes(db);
|
|
4226
5805
|
});
|
|
4227
5806
|
}
|
|
4228
5807
|
async function finalizeBulkWrites(db) {
|
|
@@ -4231,7 +5810,7 @@ async function finalizeBulkWrites(db) {
|
|
|
4231
5810
|
await db.execute(CREATE_ENTRIES_FTS_DELETE_TRIGGER_SQL);
|
|
4232
5811
|
await db.execute(CREATE_ENTRIES_FTS_UPDATE_TRIGGER_SQL);
|
|
4233
5812
|
await rebuildFts(db);
|
|
4234
|
-
await
|
|
5813
|
+
await ensureVectorIndexes(db);
|
|
4235
5814
|
await db.execute({
|
|
4236
5815
|
sql: "DELETE FROM _meta WHERE key = ?",
|
|
4237
5816
|
args: [BULK_WRITE_STATE_META_KEY]
|
|
@@ -4264,6 +5843,13 @@ async function tableExists(db, tableName) {
|
|
|
4264
5843
|
});
|
|
4265
5844
|
return result.rows.length > 0;
|
|
4266
5845
|
}
|
|
5846
|
+
async function columnExists(db, tableName, columnName) {
|
|
5847
|
+
const result = await db.execute(`PRAGMA table_info('${tableName.replaceAll("'", "''")}')`);
|
|
5848
|
+
return result.rows.some((row) => {
|
|
5849
|
+
const name = row.name;
|
|
5850
|
+
return name === columnName;
|
|
5851
|
+
});
|
|
5852
|
+
}
|
|
4267
5853
|
async function hasActiveBulkWriteState(db) {
|
|
4268
5854
|
try {
|
|
4269
5855
|
const result = await db.execute({
|
|
@@ -4276,18 +5862,20 @@ async function hasActiveBulkWriteState(db) {
|
|
|
4276
5862
|
return false;
|
|
4277
5863
|
}
|
|
4278
5864
|
}
|
|
4279
|
-
async function
|
|
5865
|
+
async function ensureVectorIndexes(db) {
|
|
4280
5866
|
try {
|
|
4281
5867
|
await db.execute(CREATE_ENTRIES_EMBEDDING_INDEX_SQL);
|
|
5868
|
+
await db.execute(CREATE_EPISODES_EMBEDDING_INDEX_SQL);
|
|
4282
5869
|
} catch (error) {
|
|
4283
5870
|
if (!isVectorUnavailableError(error)) {
|
|
4284
5871
|
throw error;
|
|
4285
5872
|
}
|
|
4286
5873
|
}
|
|
4287
5874
|
}
|
|
4288
|
-
async function
|
|
5875
|
+
async function dropVectorIndexes(db) {
|
|
4289
5876
|
try {
|
|
4290
5877
|
await db.execute(`DROP INDEX IF EXISTS ${VECTOR_INDEX_NAME}`);
|
|
5878
|
+
await db.execute(`DROP INDEX IF EXISTS ${EPISODE_VECTOR_INDEX_NAME}`);
|
|
4291
5879
|
} catch (error) {
|
|
4292
5880
|
if (!isVectorUnavailableError(error)) {
|
|
4293
5881
|
throw error;
|
|
@@ -4383,9 +5971,9 @@ async function probeVectorAvailability(services) {
|
|
|
4383
5971
|
}
|
|
4384
5972
|
|
|
4385
5973
|
// ../../src/config.ts
|
|
4386
|
-
import
|
|
4387
|
-
import
|
|
4388
|
-
import
|
|
5974
|
+
import fs7 from "fs";
|
|
5975
|
+
import os3 from "os";
|
|
5976
|
+
import path7 from "path";
|
|
4389
5977
|
import { fileURLToPath } from "url";
|
|
4390
5978
|
var AUTH_METHOD_DEFINITIONS = [
|
|
4391
5979
|
{
|
|
@@ -4425,7 +6013,7 @@ var AUTH_METHOD_DEFINITIONS = [
|
|
|
4425
6013
|
}
|
|
4426
6014
|
];
|
|
4427
6015
|
var AUTH_METHOD_SET = new Set(AUTH_METHOD_DEFINITIONS.map((definition) => definition.id));
|
|
4428
|
-
var DEFAULT_CONFIG_DIR =
|
|
6016
|
+
var DEFAULT_CONFIG_DIR = path7.join(os3.homedir(), ".agenr");
|
|
4429
6017
|
var DEFAULT_DB_NAME = "knowledge.db";
|
|
4430
6018
|
function resolveConfigDir() {
|
|
4431
6019
|
return process.env.AGENR_CONFIG_DIR ?? DEFAULT_CONFIG_DIR;
|
|
@@ -4443,18 +6031,18 @@ function resolveConfigPath(options = {}) {
|
|
|
4443
6031
|
if (adjacentConfigPath) {
|
|
4444
6032
|
return adjacentConfigPath;
|
|
4445
6033
|
}
|
|
4446
|
-
return
|
|
6034
|
+
return path7.join(resolveConfigDir(), "config.json");
|
|
4447
6035
|
}
|
|
4448
6036
|
function resolveDbPath(config) {
|
|
4449
|
-
return process.env.AGENR_DB_PATH ?? config?.dbPath ??
|
|
6037
|
+
return process.env.AGENR_DB_PATH ?? config?.dbPath ?? path7.join(resolveConfigDir(), DEFAULT_DB_NAME);
|
|
4450
6038
|
}
|
|
4451
6039
|
function readConfig(options = {}) {
|
|
4452
6040
|
const configPath = resolveFilesystemPath(resolveConfigPath(options));
|
|
4453
|
-
if (!
|
|
6041
|
+
if (!fs7.existsSync(configPath)) {
|
|
4454
6042
|
return {};
|
|
4455
6043
|
}
|
|
4456
6044
|
try {
|
|
4457
|
-
const raw =
|
|
6045
|
+
const raw = fs7.readFileSync(configPath, "utf-8");
|
|
4458
6046
|
return JSON.parse(raw);
|
|
4459
6047
|
} catch {
|
|
4460
6048
|
return {};
|
|
@@ -4467,12 +6055,12 @@ function resolveAdjacentConfigPath(dbPath) {
|
|
|
4467
6055
|
}
|
|
4468
6056
|
if (normalizedDbPath.startsWith("file:")) {
|
|
4469
6057
|
try {
|
|
4470
|
-
return
|
|
6058
|
+
return path7.join(path7.dirname(fileURLToPath(normalizedDbPath)), "config.json");
|
|
4471
6059
|
} catch {
|
|
4472
6060
|
return void 0;
|
|
4473
6061
|
}
|
|
4474
6062
|
}
|
|
4475
|
-
return
|
|
6063
|
+
return path7.join(path7.dirname(normalizedDbPath), "config.json");
|
|
4476
6064
|
}
|
|
4477
6065
|
function normalizeOptionalString2(value) {
|
|
4478
6066
|
const normalized = value?.trim();
|
|
@@ -4520,6 +6108,8 @@ async function insertEntry(executor, entry, embedding, contentHash) {
|
|
|
4520
6108
|
last_recalled_at,
|
|
4521
6109
|
superseded_by,
|
|
4522
6110
|
cluster_id,
|
|
6111
|
+
user_id,
|
|
6112
|
+
project,
|
|
4523
6113
|
retired,
|
|
4524
6114
|
retired_at,
|
|
4525
6115
|
retired_reason,
|
|
@@ -4529,7 +6119,7 @@ async function insertEntry(executor, entry, embedding, contentHash) {
|
|
|
4529
6119
|
VALUES (
|
|
4530
6120
|
?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
4531
6121
|
CASE WHEN ? IS NULL THEN NULL ELSE vector32(?) END,
|
|
4532
|
-
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
6122
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
4533
6123
|
)
|
|
4534
6124
|
`,
|
|
4535
6125
|
args: [
|
|
@@ -4552,6 +6142,8 @@ async function insertEntry(executor, entry, embedding, contentHash) {
|
|
|
4552
6142
|
normalizeOptionalString3(entry.last_recalled_at),
|
|
4553
6143
|
normalizeOptionalString3(entry.superseded_by),
|
|
4554
6144
|
normalizeOptionalString3(entry.cluster_id),
|
|
6145
|
+
normalizeOptionalString3(entry.user_id),
|
|
6146
|
+
normalizeOptionalString3(entry.project),
|
|
4555
6147
|
entry.retired ? 1 : 0,
|
|
4556
6148
|
normalizeOptionalString3(entry.retired_at),
|
|
4557
6149
|
normalizeOptionalString3(entry.retired_reason),
|
|
@@ -4589,6 +6181,8 @@ async function getEntries(executor, ids) {
|
|
|
4589
6181
|
last_recalled_at,
|
|
4590
6182
|
superseded_by,
|
|
4591
6183
|
cluster_id,
|
|
6184
|
+
user_id,
|
|
6185
|
+
project,
|
|
4592
6186
|
retired,
|
|
4593
6187
|
retired_at,
|
|
4594
6188
|
retired_reason,
|
|
@@ -4815,6 +6409,8 @@ var ENTRY_SELECT_COLUMNS2 = `
|
|
|
4815
6409
|
e.last_recalled_at,
|
|
4816
6410
|
e.superseded_by,
|
|
4817
6411
|
e.cluster_id,
|
|
6412
|
+
e.user_id,
|
|
6413
|
+
e.project,
|
|
4818
6414
|
e.retired,
|
|
4819
6415
|
e.retired_at,
|
|
4820
6416
|
e.retired_reason,
|
|
@@ -5043,9 +6639,470 @@ function wrapVectorError(error) {
|
|
|
5043
6639
|
}
|
|
5044
6640
|
|
|
5045
6641
|
// ../../src/adapters/db/client.ts
|
|
5046
|
-
import
|
|
5047
|
-
import
|
|
6642
|
+
import fs8 from "fs/promises";
|
|
6643
|
+
import path8 from "path";
|
|
5048
6644
|
import { createClient } from "@libsql/client";
|
|
6645
|
+
|
|
6646
|
+
// ../../src/adapters/db/episode-queries.ts
|
|
6647
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
|
|
6648
|
+
var EPISODE_SELECT_COLUMNS = `
|
|
6649
|
+
id,
|
|
6650
|
+
source,
|
|
6651
|
+
source_id,
|
|
6652
|
+
source_ref,
|
|
6653
|
+
transcript_hash,
|
|
6654
|
+
summary_hash,
|
|
6655
|
+
agent_id,
|
|
6656
|
+
surface,
|
|
6657
|
+
started_at,
|
|
6658
|
+
ended_at,
|
|
6659
|
+
summary,
|
|
6660
|
+
tags,
|
|
6661
|
+
activity_level,
|
|
6662
|
+
user_id,
|
|
6663
|
+
project,
|
|
6664
|
+
gen_model,
|
|
6665
|
+
gen_version,
|
|
6666
|
+
message_count,
|
|
6667
|
+
embedding,
|
|
6668
|
+
retired,
|
|
6669
|
+
retired_at,
|
|
6670
|
+
retired_reason,
|
|
6671
|
+
superseded_by,
|
|
6672
|
+
created_at,
|
|
6673
|
+
updated_at
|
|
6674
|
+
`;
|
|
6675
|
+
async function getEpisodeBySourceId(executor, source, sourceId) {
|
|
6676
|
+
const normalizedSourceId = normalizeOptionalString4(sourceId);
|
|
6677
|
+
if (!normalizedSourceId) {
|
|
6678
|
+
return null;
|
|
6679
|
+
}
|
|
6680
|
+
const result = await executor.execute({
|
|
6681
|
+
sql: `
|
|
6682
|
+
SELECT ${EPISODE_SELECT_COLUMNS}
|
|
6683
|
+
FROM episodes
|
|
6684
|
+
WHERE source = ?
|
|
6685
|
+
AND source_id = ?
|
|
6686
|
+
LIMIT 1
|
|
6687
|
+
`,
|
|
6688
|
+
args: [source, normalizedSourceId]
|
|
6689
|
+
});
|
|
6690
|
+
const row = result.rows[0];
|
|
6691
|
+
return row ? mapEpisodeRow(row) : null;
|
|
6692
|
+
}
|
|
6693
|
+
async function getEpisodeByTranscriptHash(executor, source, transcriptHash) {
|
|
6694
|
+
const normalizedTranscriptHash = normalizeOptionalString4(transcriptHash);
|
|
6695
|
+
if (!normalizedTranscriptHash) {
|
|
6696
|
+
return null;
|
|
6697
|
+
}
|
|
6698
|
+
const result = await executor.execute({
|
|
6699
|
+
sql: `
|
|
6700
|
+
SELECT ${EPISODE_SELECT_COLUMNS}
|
|
6701
|
+
FROM episodes
|
|
6702
|
+
WHERE source = ?
|
|
6703
|
+
AND transcript_hash = ?
|
|
6704
|
+
LIMIT 1
|
|
6705
|
+
`,
|
|
6706
|
+
args: [source, normalizedTranscriptHash]
|
|
6707
|
+
});
|
|
6708
|
+
const row = result.rows[0];
|
|
6709
|
+
return row ? mapEpisodeRow(row) : null;
|
|
6710
|
+
}
|
|
6711
|
+
async function upsertEpisode(executor, input) {
|
|
6712
|
+
const payload = normalizeEpisodePayload(input);
|
|
6713
|
+
const summaryHash = createEpisodePayloadHash(payload);
|
|
6714
|
+
const existing = payload.sourceId !== void 0 ? await getEpisodeBySourceId(executor, payload.source, payload.sourceId) : await getEpisodeByTranscriptHash(executor, payload.source, readRequiredIdentityHash(payload));
|
|
6715
|
+
if (!existing) {
|
|
6716
|
+
const id = randomUUID3();
|
|
6717
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
6718
|
+
const vectorJson2 = serializeEmbeddingForVector(payload.embedding ?? []);
|
|
6719
|
+
await executor.execute({
|
|
6720
|
+
sql: `
|
|
6721
|
+
INSERT INTO episodes (
|
|
6722
|
+
id,
|
|
6723
|
+
source,
|
|
6724
|
+
source_id,
|
|
6725
|
+
source_ref,
|
|
6726
|
+
transcript_hash,
|
|
6727
|
+
summary_hash,
|
|
6728
|
+
agent_id,
|
|
6729
|
+
surface,
|
|
6730
|
+
started_at,
|
|
6731
|
+
ended_at,
|
|
6732
|
+
summary,
|
|
6733
|
+
tags,
|
|
6734
|
+
activity_level,
|
|
6735
|
+
user_id,
|
|
6736
|
+
project,
|
|
6737
|
+
gen_model,
|
|
6738
|
+
gen_version,
|
|
6739
|
+
message_count,
|
|
6740
|
+
embedding,
|
|
6741
|
+
retired,
|
|
6742
|
+
retired_at,
|
|
6743
|
+
retired_reason,
|
|
6744
|
+
superseded_by,
|
|
6745
|
+
created_at,
|
|
6746
|
+
updated_at
|
|
6747
|
+
)
|
|
6748
|
+
VALUES (
|
|
6749
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
6750
|
+
CASE WHEN ? IS NULL THEN NULL ELSE vector32(?) END,
|
|
6751
|
+
0, NULL, NULL, NULL, ?, ?
|
|
6752
|
+
)
|
|
6753
|
+
`,
|
|
6754
|
+
args: [
|
|
6755
|
+
id,
|
|
6756
|
+
payload.source,
|
|
6757
|
+
toNullableString(payload.sourceId),
|
|
6758
|
+
toNullableString(payload.sourceRef),
|
|
6759
|
+
toNullableString(payload.transcriptHash),
|
|
6760
|
+
summaryHash,
|
|
6761
|
+
toNullableString(payload.agentId),
|
|
6762
|
+
toNullableString(payload.surface),
|
|
6763
|
+
payload.startedAt,
|
|
6764
|
+
toNullableString(payload.endedAt),
|
|
6765
|
+
payload.summary,
|
|
6766
|
+
serializeTags(payload.tags),
|
|
6767
|
+
toNullableString(payload.activityLevel),
|
|
6768
|
+
toNullableString(payload.userId),
|
|
6769
|
+
toNullableString(payload.project),
|
|
6770
|
+
toNullableString(payload.genModel),
|
|
6771
|
+
toNullableString(payload.genVersion),
|
|
6772
|
+
toNullableInteger(payload.messageCount),
|
|
6773
|
+
vectorJson2,
|
|
6774
|
+
vectorJson2,
|
|
6775
|
+
now2,
|
|
6776
|
+
now2
|
|
6777
|
+
]
|
|
6778
|
+
});
|
|
6779
|
+
return {
|
|
6780
|
+
episode: await getEpisodeById(executor, id),
|
|
6781
|
+
action: "inserted"
|
|
6782
|
+
};
|
|
6783
|
+
}
|
|
6784
|
+
const existingSummaryHash = existing.summaryHash ?? createEpisodePayloadHash(normalizeEpisodePayload(fromStoredEpisode(existing)));
|
|
6785
|
+
if (existingSummaryHash === summaryHash) {
|
|
6786
|
+
return {
|
|
6787
|
+
episode: existing,
|
|
6788
|
+
action: "unchanged"
|
|
6789
|
+
};
|
|
6790
|
+
}
|
|
6791
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6792
|
+
const vectorJson = serializeEmbeddingForVector(payload.embedding ?? []);
|
|
6793
|
+
await executor.execute({
|
|
6794
|
+
sql: `
|
|
6795
|
+
UPDATE episodes
|
|
6796
|
+
SET source_ref = ?,
|
|
6797
|
+
transcript_hash = ?,
|
|
6798
|
+
summary_hash = ?,
|
|
6799
|
+
agent_id = ?,
|
|
6800
|
+
surface = ?,
|
|
6801
|
+
started_at = ?,
|
|
6802
|
+
ended_at = ?,
|
|
6803
|
+
summary = ?,
|
|
6804
|
+
tags = ?,
|
|
6805
|
+
activity_level = ?,
|
|
6806
|
+
user_id = ?,
|
|
6807
|
+
project = ?,
|
|
6808
|
+
gen_model = ?,
|
|
6809
|
+
gen_version = ?,
|
|
6810
|
+
message_count = ?,
|
|
6811
|
+
embedding = CASE WHEN ? IS NULL THEN NULL ELSE vector32(?) END,
|
|
6812
|
+
updated_at = ?
|
|
6813
|
+
WHERE id = ?
|
|
6814
|
+
`,
|
|
6815
|
+
args: [
|
|
6816
|
+
toNullableString(payload.sourceRef),
|
|
6817
|
+
toNullableString(payload.transcriptHash),
|
|
6818
|
+
summaryHash,
|
|
6819
|
+
toNullableString(payload.agentId),
|
|
6820
|
+
toNullableString(payload.surface),
|
|
6821
|
+
payload.startedAt,
|
|
6822
|
+
toNullableString(payload.endedAt),
|
|
6823
|
+
payload.summary,
|
|
6824
|
+
serializeTags(payload.tags),
|
|
6825
|
+
toNullableString(payload.activityLevel),
|
|
6826
|
+
toNullableString(payload.userId),
|
|
6827
|
+
toNullableString(payload.project),
|
|
6828
|
+
toNullableString(payload.genModel),
|
|
6829
|
+
toNullableString(payload.genVersion),
|
|
6830
|
+
toNullableInteger(payload.messageCount),
|
|
6831
|
+
vectorJson,
|
|
6832
|
+
vectorJson,
|
|
6833
|
+
now,
|
|
6834
|
+
existing.id
|
|
6835
|
+
]
|
|
6836
|
+
});
|
|
6837
|
+
return {
|
|
6838
|
+
episode: await getEpisodeById(executor, existing.id),
|
|
6839
|
+
action: "updated"
|
|
6840
|
+
};
|
|
6841
|
+
}
|
|
6842
|
+
async function listEpisodesByTimeWindow(executor, window, limit) {
|
|
6843
|
+
const bounds = resolveWindowBounds(window);
|
|
6844
|
+
if (!bounds) {
|
|
6845
|
+
return [];
|
|
6846
|
+
}
|
|
6847
|
+
const whereClauses = [buildActiveEpisodeClause()];
|
|
6848
|
+
const args = [];
|
|
6849
|
+
if (bounds.start && bounds.end) {
|
|
6850
|
+
whereClauses.push("started_at <= ?");
|
|
6851
|
+
whereClauses.push("COALESCE(ended_at, started_at) >= ?");
|
|
6852
|
+
args.push(bounds.end, bounds.start);
|
|
6853
|
+
} else if (bounds.start) {
|
|
6854
|
+
whereClauses.push("COALESCE(ended_at, started_at) >= ?");
|
|
6855
|
+
args.push(bounds.start);
|
|
6856
|
+
} else if (bounds.end) {
|
|
6857
|
+
whereClauses.push("started_at <= ?");
|
|
6858
|
+
args.push(bounds.end);
|
|
6859
|
+
} else {
|
|
6860
|
+
return [];
|
|
6861
|
+
}
|
|
6862
|
+
const normalizedLimit = normalizePositiveInteger(limit);
|
|
6863
|
+
const result = await executor.execute({
|
|
6864
|
+
sql: `
|
|
6865
|
+
SELECT ${EPISODE_SELECT_COLUMNS}
|
|
6866
|
+
FROM episodes
|
|
6867
|
+
WHERE ${whereClauses.join(" AND ")}
|
|
6868
|
+
ORDER BY started_at DESC, id ASC
|
|
6869
|
+
${normalizedLimit ? "LIMIT ?" : ""}
|
|
6870
|
+
`,
|
|
6871
|
+
args: normalizedLimit ? [...args, normalizedLimit] : args
|
|
6872
|
+
});
|
|
6873
|
+
return result.rows.map((row) => mapEpisodeRow(row));
|
|
6874
|
+
}
|
|
6875
|
+
async function episodeVectorSearch(executor, params) {
|
|
6876
|
+
if (params.limit <= 0 || params.embedding.length === 0) {
|
|
6877
|
+
return [];
|
|
6878
|
+
}
|
|
6879
|
+
const serializedEmbedding = serializeEmbeddingForVector(params.embedding);
|
|
6880
|
+
if (!serializedEmbedding) {
|
|
6881
|
+
return [];
|
|
6882
|
+
}
|
|
6883
|
+
let result;
|
|
6884
|
+
try {
|
|
6885
|
+
result = await executor.execute({
|
|
6886
|
+
sql: `
|
|
6887
|
+
SELECT ${prefixColumns(EPISODE_SELECT_COLUMNS, "e")}
|
|
6888
|
+
FROM vector_top_k('idx_episodes_embedding', vector32(?), ?) AS v
|
|
6889
|
+
JOIN episodes AS e ON e.rowid = v.id
|
|
6890
|
+
WHERE ${buildActiveEpisodeClause("e")}
|
|
6891
|
+
LIMIT ?
|
|
6892
|
+
`,
|
|
6893
|
+
args: [serializedEmbedding, params.limit, params.limit]
|
|
6894
|
+
});
|
|
6895
|
+
} catch (error) {
|
|
6896
|
+
throw wrapEpisodeVectorError(error);
|
|
6897
|
+
}
|
|
6898
|
+
return result.rows.map((row) => {
|
|
6899
|
+
const episode = mapEpisodeRow(row);
|
|
6900
|
+
return {
|
|
6901
|
+
episode,
|
|
6902
|
+
vectorSim: cosineSimilarity(params.embedding, episode.embedding ?? [])
|
|
6903
|
+
};
|
|
6904
|
+
}).filter((candidate) => candidate.vectorSim > 0).sort((left, right) => right.vectorSim - left.vectorSim).slice(0, params.limit);
|
|
6905
|
+
}
|
|
6906
|
+
async function listEpisodesWithoutEmbeddings(executor, limit) {
|
|
6907
|
+
const normalizedLimit = normalizePositiveInteger(limit);
|
|
6908
|
+
const result = await executor.execute({
|
|
6909
|
+
sql: `
|
|
6910
|
+
SELECT ${EPISODE_SELECT_COLUMNS}
|
|
6911
|
+
FROM episodes
|
|
6912
|
+
WHERE ${buildActiveEpisodeClause()}
|
|
6913
|
+
AND embedding IS NULL
|
|
6914
|
+
ORDER BY started_at DESC, id ASC
|
|
6915
|
+
${normalizedLimit ? "LIMIT ?" : ""}
|
|
6916
|
+
`,
|
|
6917
|
+
args: normalizedLimit ? [normalizedLimit] : []
|
|
6918
|
+
});
|
|
6919
|
+
return result.rows.map((row) => mapEpisodeRow(row));
|
|
6920
|
+
}
|
|
6921
|
+
async function updateEpisodeEmbedding(executor, id, embedding) {
|
|
6922
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6923
|
+
const vectorJson = serializeEmbeddingForVector(embedding);
|
|
6924
|
+
await executor.execute({
|
|
6925
|
+
sql: `
|
|
6926
|
+
UPDATE episodes
|
|
6927
|
+
SET embedding = CASE WHEN ? IS NULL THEN NULL ELSE vector32(?) END,
|
|
6928
|
+
updated_at = ?
|
|
6929
|
+
WHERE id = ?
|
|
6930
|
+
`,
|
|
6931
|
+
args: [vectorJson, vectorJson, now, id]
|
|
6932
|
+
});
|
|
6933
|
+
}
|
|
6934
|
+
function createEpisodePayloadHash(payload) {
|
|
6935
|
+
return createHash3("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
6936
|
+
}
|
|
6937
|
+
function fromStoredEpisode(episode) {
|
|
6938
|
+
return {
|
|
6939
|
+
source: episode.source,
|
|
6940
|
+
sourceId: episode.sourceId,
|
|
6941
|
+
sourceRef: episode.sourceRef,
|
|
6942
|
+
transcriptHash: episode.transcriptHash,
|
|
6943
|
+
agentId: episode.agentId,
|
|
6944
|
+
surface: episode.surface,
|
|
6945
|
+
startedAt: episode.startedAt,
|
|
6946
|
+
endedAt: episode.endedAt,
|
|
6947
|
+
summary: episode.summary,
|
|
6948
|
+
tags: episode.tags,
|
|
6949
|
+
activityLevel: episode.activityLevel,
|
|
6950
|
+
userId: episode.userId,
|
|
6951
|
+
project: episode.project,
|
|
6952
|
+
genModel: episode.genModel,
|
|
6953
|
+
genVersion: episode.genVersion,
|
|
6954
|
+
messageCount: episode.messageCount,
|
|
6955
|
+
embedding: episode.embedding
|
|
6956
|
+
};
|
|
6957
|
+
}
|
|
6958
|
+
async function getEpisodeById(executor, id) {
|
|
6959
|
+
const result = await executor.execute({
|
|
6960
|
+
sql: `
|
|
6961
|
+
SELECT ${EPISODE_SELECT_COLUMNS}
|
|
6962
|
+
FROM episodes
|
|
6963
|
+
WHERE id = ?
|
|
6964
|
+
LIMIT 1
|
|
6965
|
+
`,
|
|
6966
|
+
args: [id]
|
|
6967
|
+
});
|
|
6968
|
+
const row = result.rows[0];
|
|
6969
|
+
if (!row) {
|
|
6970
|
+
throw new Error(`Episode ${id} was not found after persistence.`);
|
|
6971
|
+
}
|
|
6972
|
+
return mapEpisodeRow(row);
|
|
6973
|
+
}
|
|
6974
|
+
function normalizeEpisodePayload(input) {
|
|
6975
|
+
const sourceId = normalizeOptionalString4(input.sourceId);
|
|
6976
|
+
const transcriptHash = normalizeOptionalString4(input.transcriptHash);
|
|
6977
|
+
if (!sourceId && !transcriptHash) {
|
|
6978
|
+
throw new Error("Episode writes require either sourceId or transcriptHash.");
|
|
6979
|
+
}
|
|
6980
|
+
const startedAt = normalizeRequiredString(input.startedAt, "startedAt");
|
|
6981
|
+
const endedAt = normalizeOptionalString4(input.endedAt);
|
|
6982
|
+
const summary = normalizeSummary2(input.summary);
|
|
6983
|
+
return {
|
|
6984
|
+
source: input.source,
|
|
6985
|
+
...sourceId ? { sourceId } : {},
|
|
6986
|
+
...normalizeOptionalString4(input.sourceRef) ? { sourceRef: normalizeOptionalString4(input.sourceRef) ?? void 0 } : {},
|
|
6987
|
+
...transcriptHash ? { transcriptHash } : {},
|
|
6988
|
+
...normalizeOptionalString4(input.agentId) ? { agentId: normalizeOptionalString4(input.agentId) ?? void 0 } : {},
|
|
6989
|
+
...normalizeOptionalString4(input.surface) ? { surface: normalizeOptionalString4(input.surface) ?? void 0 } : {},
|
|
6990
|
+
startedAt,
|
|
6991
|
+
...endedAt ? { endedAt } : {},
|
|
6992
|
+
summary,
|
|
6993
|
+
tags: normalizeTags3(input.tags),
|
|
6994
|
+
...normalizeActivityLevel2(input.activityLevel) ? { activityLevel: normalizeActivityLevel2(input.activityLevel) } : {},
|
|
6995
|
+
...normalizeOptionalString4(input.userId) ? { userId: normalizeOptionalString4(input.userId) ?? void 0 } : {},
|
|
6996
|
+
...normalizeOptionalText(input.project) ? { project: normalizeOptionalText(input.project) ?? void 0 } : {},
|
|
6997
|
+
...normalizeOptionalString4(input.genModel) ? { genModel: normalizeOptionalString4(input.genModel) ?? void 0 } : {},
|
|
6998
|
+
...normalizeOptionalString4(input.genVersion) ? { genVersion: normalizeOptionalString4(input.genVersion) ?? void 0 } : {},
|
|
6999
|
+
...normalizeOptionalInteger(input.messageCount) !== void 0 ? { messageCount: normalizeOptionalInteger(input.messageCount) } : {},
|
|
7000
|
+
...normalizeEmbedding2(input.embedding) ? { embedding: normalizeEmbedding2(input.embedding) } : {}
|
|
7001
|
+
};
|
|
7002
|
+
}
|
|
7003
|
+
function resolveWindowBounds(window) {
|
|
7004
|
+
switch (window.kind) {
|
|
7005
|
+
case "interval": {
|
|
7006
|
+
if (!window.start || !window.end) {
|
|
7007
|
+
return null;
|
|
7008
|
+
}
|
|
7009
|
+
return { start: window.start.toISOString(), end: window.end.toISOString() };
|
|
7010
|
+
}
|
|
7011
|
+
case "anchor": {
|
|
7012
|
+
if (!window.anchor || window.radiusDays === void 0 || window.radiusDays < 0) {
|
|
7013
|
+
return null;
|
|
7014
|
+
}
|
|
7015
|
+
const radiusMs = Math.trunc(window.radiusDays) * 24 * 60 * 60 * 1e3;
|
|
7016
|
+
return {
|
|
7017
|
+
start: new Date(window.anchor.getTime() - radiusMs).toISOString(),
|
|
7018
|
+
end: new Date(window.anchor.getTime() + radiusMs).toISOString()
|
|
7019
|
+
};
|
|
7020
|
+
}
|
|
7021
|
+
case "open_end":
|
|
7022
|
+
return window.start ? { start: window.start.toISOString() } : null;
|
|
7023
|
+
case "open_start":
|
|
7024
|
+
return window.end ? { end: window.end.toISOString() } : null;
|
|
7025
|
+
default:
|
|
7026
|
+
return null;
|
|
7027
|
+
}
|
|
7028
|
+
}
|
|
7029
|
+
function readRequiredIdentityHash(payload) {
|
|
7030
|
+
if (!payload.transcriptHash) {
|
|
7031
|
+
throw new Error("Episode writes without sourceId require transcriptHash.");
|
|
7032
|
+
}
|
|
7033
|
+
return payload.transcriptHash;
|
|
7034
|
+
}
|
|
7035
|
+
function normalizeActivityLevel2(value) {
|
|
7036
|
+
if (!value) {
|
|
7037
|
+
return void 0;
|
|
7038
|
+
}
|
|
7039
|
+
return value;
|
|
7040
|
+
}
|
|
7041
|
+
function normalizeOptionalString4(value) {
|
|
7042
|
+
const trimmed = value?.trim();
|
|
7043
|
+
return trimmed ? trimmed : void 0;
|
|
7044
|
+
}
|
|
7045
|
+
function normalizeOptionalText(value) {
|
|
7046
|
+
const normalized = value?.replace(/\s+/gu, " ").trim();
|
|
7047
|
+
return normalized ? normalized : void 0;
|
|
7048
|
+
}
|
|
7049
|
+
function normalizeSummary2(value) {
|
|
7050
|
+
const normalized = value.replace(/\s+/gu, " ").trim();
|
|
7051
|
+
if (!normalized) {
|
|
7052
|
+
throw new Error("Episode summary must not be empty.");
|
|
7053
|
+
}
|
|
7054
|
+
return normalized;
|
|
7055
|
+
}
|
|
7056
|
+
function normalizeTags3(tags) {
|
|
7057
|
+
if (!tags || tags.length === 0) {
|
|
7058
|
+
return [];
|
|
7059
|
+
}
|
|
7060
|
+
return Array.from(new Set(tags.map((tag) => tag.trim().toLowerCase()).filter((tag) => tag.length > 0))).sort((left, right) => left.localeCompare(right)).slice(0, 8);
|
|
7061
|
+
}
|
|
7062
|
+
function normalizeOptionalInteger(value) {
|
|
7063
|
+
if (value === void 0 || !Number.isFinite(value)) {
|
|
7064
|
+
return void 0;
|
|
7065
|
+
}
|
|
7066
|
+
return Math.trunc(value);
|
|
7067
|
+
}
|
|
7068
|
+
function normalizeEmbedding2(embedding) {
|
|
7069
|
+
if (!embedding || embedding.length === 0) {
|
|
7070
|
+
return void 0;
|
|
7071
|
+
}
|
|
7072
|
+
return embedding.map((value) => Number.isFinite(value) ? value : 0);
|
|
7073
|
+
}
|
|
7074
|
+
function normalizeRequiredString(value, fieldName) {
|
|
7075
|
+
const normalized = value.trim();
|
|
7076
|
+
if (!normalized) {
|
|
7077
|
+
throw new Error(`Episode field "${fieldName}" must not be empty.`);
|
|
7078
|
+
}
|
|
7079
|
+
return normalized;
|
|
7080
|
+
}
|
|
7081
|
+
function normalizePositiveInteger(value) {
|
|
7082
|
+
const normalized = normalizeOptionalInteger(value);
|
|
7083
|
+
if (normalized === void 0 || normalized <= 0) {
|
|
7084
|
+
return void 0;
|
|
7085
|
+
}
|
|
7086
|
+
return normalized;
|
|
7087
|
+
}
|
|
7088
|
+
function prefixColumns(columns, alias) {
|
|
7089
|
+
return columns.split(",").map((col) => {
|
|
7090
|
+
const trimmed = col.trim();
|
|
7091
|
+
return trimmed ? `${alias}.${trimmed}` : "";
|
|
7092
|
+
}).filter(Boolean).join(", ");
|
|
7093
|
+
}
|
|
7094
|
+
function wrapEpisodeVectorError(error) {
|
|
7095
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7096
|
+
return new Error(`Episode vector search is unavailable: ${message}`);
|
|
7097
|
+
}
|
|
7098
|
+
function toNullableString(value) {
|
|
7099
|
+
return value ?? null;
|
|
7100
|
+
}
|
|
7101
|
+
function toNullableInteger(value) {
|
|
7102
|
+
return value ?? null;
|
|
7103
|
+
}
|
|
7104
|
+
|
|
7105
|
+
// ../../src/adapters/db/client.ts
|
|
5049
7106
|
var DEFAULT_BUSY_TIMEOUT_MS = 3e3;
|
|
5050
7107
|
async function createDatabase(dbPath) {
|
|
5051
7108
|
const client = await openClient(dbPath);
|
|
@@ -5083,6 +7140,34 @@ var LibsqlDatabase = class _LibsqlDatabase {
|
|
|
5083
7140
|
async findExistingHashes(hashes) {
|
|
5084
7141
|
return findExistingHashes(this.executor, hashes);
|
|
5085
7142
|
}
|
|
7143
|
+
/** Loads one episode by stable `(source, sourceId)` identity. */
|
|
7144
|
+
async getEpisodeBySourceId(source, sourceId) {
|
|
7145
|
+
return getEpisodeBySourceId(this.executor, source, sourceId);
|
|
7146
|
+
}
|
|
7147
|
+
/** Loads one episode by fallback `(source, transcriptHash)` identity. */
|
|
7148
|
+
async getEpisodeByTranscriptHash(source, transcriptHash) {
|
|
7149
|
+
return getEpisodeByTranscriptHash(this.executor, source, transcriptHash);
|
|
7150
|
+
}
|
|
7151
|
+
/** Inserts or updates an episodic-memory row using normalized change detection. */
|
|
7152
|
+
async upsertEpisode(input) {
|
|
7153
|
+
return upsertEpisode(this.executor, input);
|
|
7154
|
+
}
|
|
7155
|
+
/** Lists active episodes whose time range overlaps the requested window. */
|
|
7156
|
+
async listEpisodesByTimeWindow(window, limit) {
|
|
7157
|
+
return listEpisodesByTimeWindow(this.executor, window, limit);
|
|
7158
|
+
}
|
|
7159
|
+
/** Finds active episodes by vector similarity. */
|
|
7160
|
+
async episodeVectorSearch(params) {
|
|
7161
|
+
return episodeVectorSearch(this.executor, params);
|
|
7162
|
+
}
|
|
7163
|
+
/** Lists active episodes that are still missing embeddings. */
|
|
7164
|
+
async listEpisodesWithoutEmbeddings(limit) {
|
|
7165
|
+
return listEpisodesWithoutEmbeddings(this.executor, limit);
|
|
7166
|
+
}
|
|
7167
|
+
/** Updates only the embedding payload for one episode row. */
|
|
7168
|
+
async updateEpisodeEmbedding(id, embedding) {
|
|
7169
|
+
await updateEpisodeEmbedding(this.executor, id, embedding);
|
|
7170
|
+
}
|
|
5086
7171
|
/** Finds which normalized content hashes already exist in storage. */
|
|
5087
7172
|
async findExistingNormHashes(hashes) {
|
|
5088
7173
|
return findExistingNormHashes(this.executor, hashes);
|
|
@@ -5143,8 +7228,8 @@ async function openClient(dbPath) {
|
|
|
5143
7228
|
throw new Error("Database path must not be empty.");
|
|
5144
7229
|
}
|
|
5145
7230
|
if (trimmedPath !== ":memory:" && !trimmedPath.startsWith("file:")) {
|
|
5146
|
-
const resolvedPath =
|
|
5147
|
-
await
|
|
7231
|
+
const resolvedPath = path8.resolve(trimmedPath);
|
|
7232
|
+
await fs8.mkdir(path8.dirname(resolvedPath), { recursive: true });
|
|
5148
7233
|
}
|
|
5149
7234
|
const client = createClient({ url: resolveClientUrl(trimmedPath) });
|
|
5150
7235
|
await client.execute("PRAGMA foreign_keys = ON");
|
|
@@ -5161,7 +7246,7 @@ function resolveClientUrl(dbPath) {
|
|
|
5161
7246
|
if (dbPath.startsWith("file:")) {
|
|
5162
7247
|
return dbPath;
|
|
5163
7248
|
}
|
|
5164
|
-
return `file:${
|
|
7249
|
+
return `file:${path8.resolve(dbPath)}`;
|
|
5165
7250
|
}
|
|
5166
7251
|
async function rollbackTransaction(transaction) {
|
|
5167
7252
|
if (transaction.closed) {
|
|
@@ -5274,7 +7359,6 @@ function requireApiKey(status) {
|
|
|
5274
7359
|
function createSessionStartTracker() {
|
|
5275
7360
|
const seenSessionIds = /* @__PURE__ */ new Set();
|
|
5276
7361
|
const seenSessionKeys = /* @__PURE__ */ new Set();
|
|
5277
|
-
const latestResetBySessionKey = /* @__PURE__ */ new Map();
|
|
5278
7362
|
const resumedFromBySessionId = /* @__PURE__ */ new Map();
|
|
5279
7363
|
const countActiveSessions = () => seenSessionIds.size + seenSessionKeys.size;
|
|
5280
7364
|
return {
|
|
@@ -5312,18 +7396,6 @@ function createSessionStartTracker() {
|
|
|
5312
7396
|
activeCount: countActiveSessions()
|
|
5313
7397
|
};
|
|
5314
7398
|
},
|
|
5315
|
-
rememberReset(sessionKey, record) {
|
|
5316
|
-
const normalizedSessionKey = sessionKey?.trim();
|
|
5317
|
-
const normalizedSessionFile = record.sessionFile.trim();
|
|
5318
|
-
if (!normalizedSessionKey || normalizedSessionFile.length === 0) {
|
|
5319
|
-
return;
|
|
5320
|
-
}
|
|
5321
|
-
latestResetBySessionKey.set(normalizedSessionKey, {
|
|
5322
|
-
sessionFile: normalizedSessionFile,
|
|
5323
|
-
recordedAt: record.recordedAt,
|
|
5324
|
-
...record.sessionId?.trim() ? { sessionId: record.sessionId.trim() } : {}
|
|
5325
|
-
});
|
|
5326
|
-
},
|
|
5327
7399
|
rememberSessionStart(sessionId, _sessionKey, resumedFrom) {
|
|
5328
7400
|
const normalizedSessionId = sessionId?.trim();
|
|
5329
7401
|
const normalizedResumedFrom = resumedFrom?.trim();
|
|
@@ -5332,10 +7404,6 @@ function createSessionStartTracker() {
|
|
|
5332
7404
|
}
|
|
5333
7405
|
resumedFromBySessionId.set(normalizedSessionId, normalizedResumedFrom);
|
|
5334
7406
|
},
|
|
5335
|
-
getLatestReset(sessionKey) {
|
|
5336
|
-
const normalizedSessionKey = sessionKey?.trim();
|
|
5337
|
-
return normalizedSessionKey ? latestResetBySessionKey.get(normalizedSessionKey) : void 0;
|
|
5338
|
-
},
|
|
5339
7407
|
getResumedFrom(sessionId) {
|
|
5340
7408
|
const normalizedSessionId = sessionId?.trim();
|
|
5341
7409
|
return normalizedSessionId ? resumedFromBySessionId.get(normalizedSessionId) : void 0;
|
|
@@ -5372,14 +7440,6 @@ var openclaw_default = definePluginEntry({
|
|
|
5372
7440
|
tracker
|
|
5373
7441
|
})
|
|
5374
7442
|
);
|
|
5375
|
-
api.on(
|
|
5376
|
-
"before_reset",
|
|
5377
|
-
(event, ctx) => handleAgenrBeforeReset(event, ctx, {
|
|
5378
|
-
logger: api.logger,
|
|
5379
|
-
servicesPromise,
|
|
5380
|
-
tracker
|
|
5381
|
-
})
|
|
5382
|
-
);
|
|
5383
7443
|
api.on("session_start", (event) => {
|
|
5384
7444
|
tracker.rememberSessionStart(event.sessionId, event.sessionKey, event.resumedFrom);
|
|
5385
7445
|
});
|