@agenr/openclaw-plugin 1.1.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 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", "todo", "relationship", "event", "reflection"];
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 architecture, preference for user choices, lesson for learned guidance, fact for durable state, todo for important follow-up, reflection for synthesized summaries, and event or relationship only when that context will matter later.";
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 todo."
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. Supports semantic search via query and temporal filtering via since, until, around, and aroundRadius; for time-based questions, always combine a focused query with temporal filters. Use this mid-session when you need context you do not already have. Session-start recall is already handled automatically.",
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
- if (!services.embeddingStatus.available) {
1505
- const message = services.embeddingStatus.error ?? "Embeddings are unavailable, so agenr recall cannot run.";
1506
- logToolFailure(logger, "agenr_recall", ctx, message);
1507
- return failedTextResult(message, {
1508
- status: "failed"
1509
- });
1510
- }
1511
- const results = await recall(request, services.recall);
1512
- logger.info(`[agenr] tool=agenr_recall ${formatToolSessionContext(ctx)} result: ${results.length} entries${formatRecallResultSubjects(results)}`);
1513
- if (results.length === 0) {
1514
- return textResult("No matching agenr memories found.", {
1515
- status: "ok",
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: results.length,
1523
- results: results.map((result) => ({
1524
- id: result.entry.id,
1525
- subject: result.entry.subject,
1526
- type: result.entry.type,
1527
- expiry: result.entry.expiry,
1528
- importance: result.entry.importance,
1529
- score: result.score,
1530
- tags: result.entry.tags,
1531
- content: result.entry.content
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.since) {
1779
- parts.push(`since=${params.since}`);
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 formatRecallResults(results) {
1841
- const lines = [`Found ${results.length} matching agenr memories:`];
1842
- for (const [index, result] of results.entries()) {
1843
- lines.push(
1844
- `${index + 1}. ${result.entry.id} | ${result.entry.type} | ${result.entry.subject} | score ${result.score.toFixed(2)} | importance ${result.entry.importance}`
1845
- );
1846
- lines.push(` ${truncate(result.entry.content, 220)}`);
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 formatRecallResultSubjects(results) {
1851
- const subjects = results.map((result) => result.entry.subject.trim()).filter((subject) => subject.length > 0);
1852
- if (subjects.length === 0) {
1853
- return "";
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
- const displayed = subjects.slice(0, RESULT_SUBJECT_LOG_LIMIT).map((subject) => JSON.stringify(truncate(subject, 80)));
1856
- const remaining = subjects.length - RESULT_SUBJECT_LOG_LIMIT;
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 ` [subjects: ${displayed.join(", ")}]`;
2724
+ return "Matched episodic recall ranking.";
1861
2725
  }
1862
2726
  function formatTrace(entry, supersededBy, supersedes, recallEvents) {
1863
2727
  const lines = [
@@ -1906,7 +2770,7 @@ function asRecord(value) {
1906
2770
  var openclaw_plugin_default = {
1907
2771
  id: "agenr",
1908
2772
  name: "agenr",
1909
- version: "1.1.0",
2773
+ version: "1.2.0",
1910
2774
  description: "agenr memory plugin for OpenClaw",
1911
2775
  kind: "memory",
1912
2776
  contracts: {
@@ -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 temporal recall: when the user asks about a specific time period, always use temporal filters - semantic search alone matches meaning, not dates.",
2018
- 'Use around plus aroundRadius to bias recall toward a period: last week -> around: "7d", aroundRadius: 3; two weeks ago -> around: "14d", aroundRadius: 4.',
2019
- 'Use since and until for hard bounds: in the last month -> since: "30d"; before March -> until: "2026-03-01". Combine temporal filters with a focused query.'
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/format/recall-format.ts
2045
- var MAX_CONTENT_CHARS = 280;
2046
- function formatAgenrSessionStartRecall(recall2) {
2047
- const sections = buildSections(recall2);
2048
- if (sections.length === 0) {
2049
- return "";
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 lines = ["## Agenr Session Recall", "Use this as prior context. Confirm anything important if the current conversation conflicts with it.", ""];
2052
- for (const section of sections) {
2053
- lines.push(`### ${section.title}`);
2054
- for (const item of section.entries) {
2055
- lines.push(formatEntryHeader(item));
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 lines.join("\n").trim();
2953
+ return {
2954
+ summary,
2955
+ tags: normalizeTags2(parsedRecord.tags),
2956
+ activityLevel,
2957
+ ...normalizeProject(parsedRecord.project) ? { project: normalizeProject(parsedRecord.project) } : {}
2958
+ };
2061
2959
  }
2062
- function buildSections(recall2) {
2063
- const sections = [];
2064
- const coreEntries = recall2.core.map((entry) => ({ entry }));
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
- return sections;
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 formatEntryBody(entry) {
2081
- const content = truncate2(entry.content.trim(), MAX_CONTENT_CHARS);
2082
- const extra = [
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
- return ` ${content} (${extra.join(" | ")})`;
2971
+ const normalized = value.trim().toLowerCase();
2972
+ return EPISODE_ACTIVITY_LEVELS.includes(normalized) ? normalized : null;
2090
2973
  }
2091
- function truncate2(value, maxChars) {
2092
- if (value.length <= maxChars) {
2093
- return value;
2974
+ function normalizeTags2(value) {
2975
+ if (!Array.isArray(value)) {
2976
+ return [];
2094
2977
  }
2095
- return `${value.slice(0, maxChars - 3).trimEnd()}...`;
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
- // ../../src/adapters/openclaw/session/predecessor.ts
2099
- import path2 from "path";
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 resolvedSessionsDir = path.resolve(normalizedSessionsDir);
2111
- const sessionsJsonPath = path.join(resolvedSessionsDir, "sessions.json");
2112
- try {
2113
- const raw = await fs.readFile(sessionsJsonPath, "utf8");
2114
- const parsed = JSON.parse(raw);
2115
- if (!isRecord2(parsed)) {
2116
- debugLog(logger, "sessions-store-reader", `sessions.json did not contain an object: path=${sessionsJsonPath}`);
2117
- return [];
2118
- }
2119
- const entries = [];
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
- if (error instanceof SyntaxError) {
2148
- debugLog(logger, "sessions-store-reader", `sessions.json parse failed at ${sessionsJsonPath}: ${error.message}`);
2149
- return [];
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
- function resolveSessionStorePath(candidatePath, sessionsDir) {
2156
- return path.isAbsolute(candidatePath) ? path.resolve(candidatePath) : path.resolve(sessionsDir, candidatePath);
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
- function isRecord2(value) {
2159
- return typeof value === "object" && value !== null;
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 asTrimmedString(value) {
2162
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
3044
+ function formatErrorMessage2(error) {
3045
+ return error instanceof Error ? error.message : String(error);
2163
3046
  }
2164
- function asFiniteNumber(value) {
2165
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
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
- function debugLog(logger, subsystem, message) {
2168
- logger?.debug?.(`[agenr] ${subsystem}: ${message}`);
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 isFileNotFound(error) {
2171
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
3082
+ function getString(value) {
3083
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
2172
3084
  }
2173
- function formatErrorMessage2(error) {
2174
- if (error instanceof Error) {
2175
- return error.message;
3085
+ function truncateInline(value, max) {
3086
+ if (value.length <= max) {
3087
+ return value;
2176
3088
  }
2177
- return String(error);
3089
+ return value.slice(0, max);
2178
3090
  }
2179
-
2180
- // ../../src/adapters/openclaw/session/tui-lane.ts
2181
- var TUI_SESSION_KEY_PATTERN = /^agent:([^:]+):([^:]+)$/i;
2182
- 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;
2183
- 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;
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 fs3 } from "fs";
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 fs3.stat(filePath);
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
- "Sender (untrusted metadata):",
2962
- "Conversation info (untrusted metadata):",
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 resolveToolContext(state, message) {
3084
- const toolCallId = getString(message.toolCallId) ?? getString(message.tool_call_id) ?? getString(message.call_id) ?? getString(message.id);
3085
- if (toolCallId && state.pendingToolCallsById.has(toolCallId)) {
3086
- const context = state.pendingToolCallsById.get(toolCallId) ?? null;
3087
- state.pendingToolCallsById.delete(toolCallId);
3088
- if (context) {
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,437 +3896,1219 @@ var OpenClawTranscriptParser = class {
3197
3896
  * @returns Parsed transcript messages, warnings, and metadata.
3198
3897
  */
3199
3898
  async parseFile(filePath, options) {
3200
- const raw = await fs4.readFile(filePath, "utf8");
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: state.sessionTimestamp ?? state.messages[0]?.timestamp ?? fallbackTimestamp,
3217
- modelsUsed: state.modelsUsed.length > 0 ? state.modelsUsed : void 0
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/adapters/openclaw/session/summary.ts
3225
- var MIN_SUMMARY_MESSAGES = 4;
3226
- var MAX_TRANSCRIPT_CHARS = 14e3;
3227
- var SUMMARY_TIMEOUT_MS = 15e3;
3228
- var SUMMARY_SYSTEM_PROMPT = [
3229
- "You write concise narrative summaries that help the next session continue smoothly.",
3230
- "The transcript can be about any domain. Do not assume technical, project, or coding context unless the transcript shows it.",
3231
- "Write 200 to 500 words in plain Markdown with no code fences.",
3232
- "Capture:",
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 parsedTranscript = await openClawTranscriptParser.parseFile(sessionFile);
3250
- const cleanedMessages = parsedTranscript.messages.filter((message) => message.text.trim().length > 0);
3251
- const transcript = renderTranscriptForSummary(cleanedMessages);
3252
- const normalizedTranscript = capTranscript(transcript, MAX_TRANSCRIPT_CHARS);
3253
- debugLog4(
3254
- params.logger,
3255
- "summary",
3256
- `transcript adapter output for file=${sessionFile}: messages=${cleanedMessages.length} chars=${normalizedTranscript.length}`
3257
- );
3258
- if (cleanedMessages.length === 0 || normalizedTranscript.length === 0) {
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 (cleanedMessages.length < MIN_SUMMARY_MESSAGES) {
3268
- return {
3269
- status: "skipped",
3270
- reason: "too_short",
3271
- summaryPath,
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
- transcriptChars: normalizedTranscript.length
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
- const summaryExecution = resolveSummaryExecution(params.openClaw, params.agentId);
3277
- const summaryModel = formatResolvedModel(summaryExecution.provider, summaryExecution.model);
3278
- const prompt = [
3279
- "Produce a concise continuity summary for the next session.",
3280
- "Prefer short paragraphs. Use a short 'Open loops' section only if it adds clarity.",
3281
- "",
3282
- "Transcript:",
3283
- normalizedTranscript
3284
- ].join("\n");
3285
- debugLog4(
3286
- params.logger,
3287
- "summary",
3288
- `sending summary prompt model=${summaryModel} promptChars=${prompt.length} transcriptChars=${normalizedTranscript.length}`
3289
- );
3290
- params.logger.info(
3291
- `[agenr] summary: using OpenClaw embedded agent provider=${summaryExecution.provider} model=${summaryExecution.model} agent=${summaryExecution.agentId}`
3292
- );
3293
- debugLog4(
3294
- params.logger,
3295
- "summary",
3296
- `resolved OpenClaw summary model for file=${sessionFile}: agentId=${summaryExecution.agentId} modelRef=${summaryExecution.modelRef ?? "default"} provider=${summaryExecution.provider} model=${summaryExecution.model}`
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
- params.logger.warn?.(`[agenr] summary: OpenClaw embedded agent runner unavailable for file=${sessionFile}`);
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 startedAt = Date.now();
3311
- let tempSessionFile;
4112
+ const tempSessionFile = await createTempEpisodeSummarySessionFile();
3312
4113
  try {
3313
- tempSessionFile = await createTempSummarySessionFile();
3314
- const runId = `agenr-summary-${Date.now()}`;
3315
- const response = extractEmbeddedAgentText(
3316
- await runEmbeddedPiAgent({
3317
- sessionId: runId,
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: SUMMARY_TIMEOUT_MS,
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: SUMMARY_SYSTEM_PROMPT
3331
- })
3332
- ).trim();
3333
- const durationMs = Date.now() - startedAt;
3334
- const normalizedSummary = normalizeSummary(response);
3335
- debugLog4(params.logger, "summary", `received summary response model=${summaryModel} durationMs=${durationMs} chars=${normalizedSummary.length}`);
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 existingSummary = await readOpenClawSessionSummaryFile(sessionFile, params.logger);
3348
- if (existingSummary?.summaryPath === summaryPath) {
3349
- debugLog4(params.logger, "summary", `summary file already exists at write time path=${summaryPath} chars=${existingSummary.content.length}`);
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
- const summaryBytes = Buffer.byteLength(`${normalizedSummary}
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 cleanupTempSummarySessionFile(tempSessionFile);
4142
+ await cleanupTempEpisodeSummarySessionFile(tempSessionFile);
3390
4143
  }
3391
4144
  }
3392
- async function writeOpenClawSessionSummary(params) {
3393
- return generateAndWriteOpenClawSessionSummary(params);
4145
+ function resolveEpisodeSummaryExecution(openClaw, requestedAgentId) {
4146
+ const agentId = requestedAgentId?.trim() || resolveDefaultAgentId(openClaw.config);
4147
+ const modelRef = resolveAgentEffectiveModelPrimary(openClaw.config, agentId);
4148
+ const parsedModelRef = modelRef ? parseModelRef(modelRef, DEFAULT_PROVIDER) : null;
4149
+ return {
4150
+ agentId,
4151
+ agentDir: openClaw.runtime.agent.resolveAgentDir(openClaw.config, agentId),
4152
+ workspaceDir: openClaw.runtime.agent.resolveAgentWorkspaceDir(openClaw.config, agentId),
4153
+ provider: parsedModelRef?.provider ?? DEFAULT_PROVIDER,
4154
+ model: parsedModelRef?.model ?? DEFAULT_MODEL
4155
+ };
3394
4156
  }
3395
- function renderTranscriptForSummary(messages) {
3396
- return messages.map((message) => `${message.role === "user" ? "User" : "Assistant"}: ${message.text.trim()}`).join("\n");
4157
+ function formatResolvedEpisodeSummaryModel(provider, model) {
4158
+ return `${provider}/${model}`;
3397
4159
  }
3398
- function debugLog4(logger, subsystem, message) {
3399
- logger.debug?.(`[agenr] ${subsystem}: ${message}`);
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;
4166
+ }
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;
4176
+ }
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
+ }
3400
4199
  }
3401
- function normalizeSummary(value) {
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) {
3402
4567
  const trimmed = value.trim();
3403
4568
  return trimmed.replace(/^# .+\n+/u, "").trim();
3404
4569
  }
3405
- function resolveSummaryExecution(openClaw, requestedAgentId) {
3406
- const agentId = requestedAgentId?.trim() || resolveDefaultAgentId(openClaw.config);
3407
- const modelRef = resolveAgentEffectiveModelPrimary(openClaw.config, agentId);
3408
- const parsedModelRef = modelRef ? parseModelRef(modelRef, DEFAULT_PROVIDER) : null;
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));
4614
+ const tailBudget = Math.max(0, maxChars - omissionMarker.length - headBudget);
4615
+ const head = trimToBoundary2(transcript.slice(0, headBudget), false);
4616
+ const tail = trimToBoundary2(transcript.slice(-tailBudget), true);
4617
+ return `${head}${omissionMarker}${tail}`.trim();
4618
+ }
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;
4909
+ }
4910
+ if (!sessionFile?.trim()) {
4911
+ return void 0;
4912
+ }
4913
+ return deriveOpenClawSessionIdFromFilePath(sessionFile, logger);
4914
+ }
4915
+ function toResolvedTuiPredecessor(candidate, logger) {
4916
+ const sessionFile = candidate.sessionFile?.trim();
4917
+ if (!sessionFile) {
4918
+ return void 0;
4919
+ }
4920
+ const sessionId = resolvePredecessorSessionId(candidate.sessionId, sessionFile, logger);
4921
+ if (!sessionId) {
4922
+ return void 0;
4923
+ }
3409
4924
  return {
3410
- agentId,
3411
- agentDir: openClaw.runtime.agent.resolveAgentDir(openClaw.config, agentId),
3412
- workspaceDir: openClaw.runtime.agent.resolveAgentWorkspaceDir(openClaw.config, agentId),
3413
- modelRef,
3414
- provider: parsedModelRef?.provider ?? DEFAULT_PROVIDER,
3415
- model: parsedModelRef?.model ?? DEFAULT_MODEL
4925
+ sessionFile,
4926
+ sessionId,
4927
+ sessionKey: candidate.sessionKey
3416
4928
  };
3417
4929
  }
3418
- function formatResolvedModel(provider, model) {
3419
- return `${provider}/${model}`;
3420
- }
3421
- async function createTempSummarySessionFile() {
3422
- const tempDir = await fs5.mkdtemp(path4.join(os.tmpdir(), "agenr-summary-"));
3423
- return path4.join(tempDir, "session.jsonl");
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");
3424
4936
  }
3425
- async function cleanupTempSummarySessionFile(tempSessionFile) {
3426
- if (!tempSessionFile) {
3427
- return;
4937
+ function parseSingleLaneSessionKey(sessionKey) {
4938
+ const match = /^agent:([^:]+):([^:]+)$/i.exec(sessionKey.trim());
4939
+ if (!match) {
4940
+ return null;
3428
4941
  }
3429
- try {
3430
- await fs5.rm(path4.dirname(tempSessionFile), {
3431
- recursive: true,
3432
- force: true
3433
- });
3434
- } catch {
4942
+ const [, agentId, lane] = match;
4943
+ const normalizedAgentId = agentId?.trim();
4944
+ const normalizedLane = lane?.trim();
4945
+ if (!normalizedAgentId || !normalizedLane) {
4946
+ return null;
3435
4947
  }
4948
+ return {
4949
+ agentId: normalizedAgentId,
4950
+ lane: normalizedLane
4951
+ };
3436
4952
  }
3437
- function extractEmbeddedAgentText(result) {
3438
- return result.payloads?.find((payload) => payload.text?.trim())?.text ?? "";
4953
+ function isSameTuiLane(currentStableLane, candidateStableLane) {
4954
+ if (currentStableLane === "tui") {
4955
+ return candidateStableLane.toLowerCase().startsWith("tui");
4956
+ }
4957
+ return currentStableLane === candidateStableLane;
3439
4958
  }
3440
- function formatErrorMessage3(error) {
3441
- return error instanceof Error ? error.message : String(error);
4959
+ function debugLog5(logger, subsystem, message) {
4960
+ logger?.debug?.(`[agenr] ${subsystem}: ${message}`);
3442
4961
  }
3443
- function capTranscript(transcript, maxChars) {
3444
- if (transcript.length <= maxChars) {
3445
- return transcript;
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}`;
3446
4967
  }
3447
- const omissionMarker = "\n\n[Earlier middle transcript omitted for brevity]\n\n";
3448
- const headBudget = Math.max(0, Math.floor((maxChars - omissionMarker.length) * 0.35));
3449
- const tailBudget = Math.max(0, maxChars - omissionMarker.length - headBudget);
3450
- const head = trimToBoundary(transcript.slice(0, headBudget), false);
3451
- const tail = trimToBoundary(transcript.slice(-tailBudget), true);
3452
- return `${head}${omissionMarker}${tail}`.trim();
3453
- }
3454
- function trimToBoundary(value, fromStart) {
3455
- if (value.length === 0) {
3456
- return value;
4968
+ if (normalizedSessionId) {
4969
+ return `session=${normalizedSessionId}`;
3457
4970
  }
3458
- if (fromStart) {
3459
- const boundary = value.search(/\s/);
3460
- return boundary >= 0 ? value.slice(boundary).trimStart() : value.trim();
4971
+ if (normalizedSessionKey) {
4972
+ return `key=${normalizedSessionKey}`;
3461
4973
  }
3462
- const reversedBoundary = value.trimEnd().search(/\s\S*$/u);
3463
- return reversedBoundary >= 0 ? value.slice(0, reversedBoundary).trimEnd() : value.trim();
4974
+ return "session=unknown";
3464
4975
  }
3465
4976
 
3466
- // ../../src/adapters/openclaw/hooks/before-prompt-build.ts
3467
- var CORE_ENTRY_LIMIT = 4;
4977
+ // ../../src/adapters/openclaw/session/continuity/recent-session.ts
3468
4978
  var RECENT_SESSION_MESSAGE_LIMIT = 6;
3469
4979
  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}`);
3479
- return void 0;
3480
- }
3481
- debugLog5(params.logger, "before_prompt_build", `session tracker first start for ${sessionContext}`);
3482
- debugLog5(params.logger, "before_prompt_build", `session tracker active count=${trackerState.activeCount}`);
3483
- params.logger.info(`[agenr] session-start recall for ${sessionContext}`);
4980
+ async function renderRecentSessionSection(sessionFile, logger) {
3484
4981
  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 };
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;
3498
4987
  } catch (error) {
3499
- params.logger.warn(`[agenr] session-start recall failed for ${sessionContext}: ${formatErrorMessage4(error)}`);
3500
- return void 0;
4988
+ logger.debug?.(`[agenr] before_prompt_build: failed to build recent session tail for file=${sessionFile}: ${formatErrorMessage5(error)}`);
4989
+ return "";
3501
4990
  }
3502
4991
  }
3503
- async function runAgenrSessionStartRecall(services) {
3504
- const core = await listOpenClawCoreEntries(services.database, CORE_ENTRY_LIMIT);
3505
- return {
3506
- core
3507
- };
4992
+ function formatErrorMessage5(error) {
4993
+ return error instanceof Error ? error.message : String(error);
3508
4994
  }
3509
- async function buildPreviousSessionContext(ctx, tracker, services, logger) {
3510
- const sessionContext = formatSessionContext2(ctx.sessionId, ctx.sessionKey);
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
- const sections = [];
3520
- const summaryContent = await loadPredecessorSummaryContent(sessionContext, predecessor.sessionFile, ctx.agentId, services, logger);
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 sections.join("\n\n");
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 loadPredecessorSummaryContent(sessionContext, sessionFile, agentId, services, logger) {
5025
+ async function loadPredecessorContinuitySummaryContent(sessionContext, sessionFile, agentId, services, logger) {
3533
5026
  try {
3534
- const summary = await readOpenClawSessionSummaryFile(sessionFile, logger);
3535
- if (summary) {
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=${summary.summaryPath}`
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=${summary.summaryPath}`);
3540
- return summary.content;
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(`[agenr] session-start predecessor summary not found for ${sessionContext} predecessor=${sessionFile} reason=${formatErrorMessage4(error)}`);
3545
- debugLog5(logger, "before_prompt_build", `failed reading predecessor summary for ${sessionContext}: ${formatErrorMessage4(error)}`);
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(`[agenr] session-start read-time summary generation triggered for ${sessionContext} predecessor=${sessionFile} reason=no_existing_summary`);
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 awaitWithTimeout(
3552
- generateAndWriteOpenClawSessionSummary({
5048
+ const result = await awaitWithTimeout2(
5049
+ generateAndWriteOpenClawContinuitySummary({
3553
5050
  sessionFile,
3554
5051
  agentId,
3555
5052
  openClaw: services.openClaw,
3556
5053
  logger
3557
5054
  }),
3558
- READ_TIME_SUMMARY_TIMEOUT_MS
5055
+ READ_TIME_CONTINUITY_SUMMARY_TIMEOUT_MS
3559
5056
  );
3560
5057
  const elapsedMs2 = Date.now() - startedAt;
3561
- if (result === READ_TIME_SUMMARY_TIMEOUT) {
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
- debugLog5(
3566
- logger,
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 handleReadTimeSummaryResult(sessionContext, sessionFile, result, elapsedMs2, logger);
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=${formatErrorMessage4(error)} elapsedMs=${elapsedMs2}`
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 handleReadTimeSummaryResult(sessionContext, sessionFile, result, elapsedMs2, logger) {
3583
- if (result.status === "written" && result.content && result.summaryPath) {
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.summaryPath}`
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.summaryPath}`);
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.summaryPath ?? "n/a"}`
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
- debugLog5(
3595
- logger,
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.summaryPath) {
3600
- logger.info(`[agenr] session-start predecessor summary found for ${sessionContext} path=${result.summaryPath}`);
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
- debugLog5(
3609
- logger,
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 renderRecentSessionSection(sessionFile, logger) {
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(READ_TIME_SUMMARY_TIMEOUT);
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-reset.ts
3682
- async function handleAgenrBeforeReset(event, ctx, params) {
3683
- const sessionContext = formatSessionContext3(ctx.sessionId, ctx.sessionKey);
3684
- const sessionFile = event.sessionFile?.trim();
3685
- const rawMessageCount = Array.isArray(event.messages) ? event.messages.length : 0;
3686
- if (!sessionFile) {
3687
- params.logger.info(`[agenr] before_reset: summary generation skipped for ${sessionContext} reason=no_session_file rawMessages=${rawMessageCount}`);
3688
- debugLog6(params.logger, "before_reset", `skipping summary for ${sessionContext} because no session file was provided`);
3689
- return;
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
- params.tracker.rememberReset(ctx.sessionKey, {
3692
- sessionFile,
3693
- recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
3694
- ...ctx.sessionId?.trim() ? { sessionId: ctx.sessionId.trim() } : {}
3695
- });
3696
- params.logger.info(
3697
- `[agenr] before_reset: summary generation started for ${sessionContext} reason=${event.reason ?? "unknown"} rawMessages=${rawMessageCount}`
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 result = await writeOpenClawSessionSummary({
3707
- sessionFile,
3708
- agentId: ctx.agentId,
3709
- openClaw: services.openClaw,
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
- if (result.status === "written") {
3713
- params.logger.info(`[agenr] before_reset: summary file written for ${sessionContext} path=${result.summaryPath} bytes=${result.bytesWritten ?? 0}`);
3714
- return;
3715
- }
3716
- if (result.status === "skipped") {
3717
- params.logger.info(
3718
- `[agenr] before_reset: summary generation skipped for ${sessionContext} reason=${result.reason ?? "unknown"} cleanedMessages=${result.messageCount ?? 0}`
3719
- );
3720
- debugLog6(
3721
- params.logger,
3722
- "before_reset",
3723
- `skip details for ${sessionContext}: summaryPath=${result.summaryPath ?? "n/a"} transcriptChars=${result.transcriptChars ?? 0}`
3724
- );
3725
- return;
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
- params.logger.info(
3728
- `[agenr] before_reset: summary generation failed for ${sessionContext} reason=${result.reason ?? "unknown"} model=${result.model ?? "unknown"}`
3729
- );
3730
- debugLog6(
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
- );
5171
+ return { prependContext };
3735
5172
  } 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)}`);
5173
+ params.logger.warn(`[agenr] session-start recall failed for ${sessionContext}: ${formatErrorMessage2(error)}`);
5174
+ return void 0;
3738
5175
  }
3739
5176
  }
3740
- function debugLog6(logger, subsystem, message) {
3741
- logger.debug?.(`[agenr] ${subsystem}: ${message}`);
5177
+ async function runAgenrSessionStartRecall(services) {
5178
+ return { core: await listOpenClawCoreEntries(services.database, CORE_ENTRY_LIMIT) };
3742
5179
  }
3743
- function formatErrorMessage5(error) {
3744
- return error instanceof Error ? error.message : String(error);
5180
+ function formatEntryRefs(entries) {
5181
+ return entries.length === 0 ? "none" : entries.map((entry) => `${entry.subject} [${entry.id}]`).join(", ");
3745
5182
  }
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}`;
3757
- }
3758
- return "session=unknown";
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 = "1";
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,
@@ -4009,12 +5438,57 @@ var CREATE_ENTRIES_FTS_UPDATE_TRIGGER_SQL = `
4009
5438
  WHERE new.retired = 0 AND new.superseded_by IS NULL;
4010
5439
  END
4011
5440
  `;
4012
- var CREATE_INGEST_LOG_TABLE_SQL = `
4013
- CREATE TABLE IF NOT EXISTS ingest_log (
4014
- file_path TEXT PRIMARY KEY,
4015
- file_hash TEXT NOT NULL,
4016
- ingested_at TEXT NOT NULL,
4017
- entry_count INTEGER DEFAULT 0
5441
+ var CREATE_INGEST_LOG_TABLE_SQL = `
5442
+ CREATE TABLE IF NOT EXISTS ingest_log (
5443
+ file_path TEXT PRIMARY KEY,
5444
+ file_hash TEXT NOT NULL,
5445
+ ingested_at TEXT NOT NULL,
5446
+ entry_count INTEGER DEFAULT 0
5447
+ )
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
4018
5492
  )
4019
5493
  `;
4020
5494
  var CREATE_RECALL_EVENTS_TABLE_SQL = `
@@ -4029,12 +5503,49 @@ var CREATE_RECALL_EVENTS_TABLE_SQL = `
4029
5503
  var CREATE_SURGEON_RUNS_TABLE_SQL = `
4030
5504
  CREATE TABLE IF NOT EXISTS surgeon_runs (
4031
5505
  id TEXT PRIMARY KEY,
5506
+ pass_type TEXT NOT NULL DEFAULT 'retirement',
5507
+ project TEXT,
4032
5508
  started_at TEXT NOT NULL,
4033
5509
  completed_at TEXT,
5510
+ status TEXT NOT NULL DEFAULT 'running',
5511
+ input_tokens INTEGER DEFAULT 0,
5512
+ output_tokens INTEGER DEFAULT 0,
5513
+ estimated_cost_usd REAL DEFAULT 0,
5514
+ model TEXT,
4034
5515
  actions_taken INTEGER DEFAULT 0,
4035
- summary TEXT
5516
+ actions_skipped INTEGER DEFAULT 0,
5517
+ entries_retired INTEGER DEFAULT 0,
5518
+ summary TEXT,
5519
+ summary_json TEXT,
5520
+ error TEXT,
5521
+ dry_run INTEGER NOT NULL DEFAULT 1,
5522
+ config_json TEXT
5523
+ )
5524
+ `;
5525
+ var CREATE_SURGEON_RUN_ACTIONS_TABLE_SQL = `
5526
+ CREATE TABLE IF NOT EXISTS surgeon_run_actions (
5527
+ id TEXT PRIMARY KEY,
5528
+ run_id TEXT NOT NULL REFERENCES surgeon_runs(id),
5529
+ action_type TEXT NOT NULL,
5530
+ entry_id TEXT,
5531
+ entry_ids TEXT NOT NULL DEFAULT '[]',
5532
+ reasoning TEXT NOT NULL DEFAULT '',
5533
+ recall_delta TEXT,
5534
+ created_at TEXT NOT NULL
4036
5535
  )
4037
5536
  `;
5537
+ var CREATE_SURGEON_RUN_ACTIONS_RUN_ID_INDEX_SQL = `
5538
+ CREATE INDEX IF NOT EXISTS idx_surgeon_run_actions_run_id
5539
+ ON surgeon_run_actions(run_id)
5540
+ `;
5541
+ var CREATE_SURGEON_RUN_ACTIONS_ENTRY_ID_INDEX_SQL = `
5542
+ CREATE INDEX IF NOT EXISTS idx_surgeon_run_actions_entry_id
5543
+ ON surgeon_run_actions(entry_id)
5544
+ `;
5545
+ var CREATE_SURGEON_RUN_ACTIONS_CREATED_AT_INDEX_SQL = `
5546
+ CREATE INDEX IF NOT EXISTS idx_surgeon_run_actions_created_at
5547
+ ON surgeon_run_actions(created_at)
5548
+ `;
4038
5549
  var CREATE_META_TABLE_SQL = `
4039
5550
  CREATE TABLE IF NOT EXISTS _meta (
4040
5551
  key TEXT PRIMARY KEY,
@@ -4065,6 +5576,43 @@ var CREATE_ENTRIES_CREATED_AT_INDEX_SQL = `
4065
5576
  CREATE INDEX IF NOT EXISTS idx_entries_created_at
4066
5577
  ON entries(created_at)
4067
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
+ `;
4068
5616
  var CREATE_RECALL_EVENTS_ENTRY_ID_INDEX_SQL = `
4069
5617
  CREATE INDEX IF NOT EXISTS idx_recall_events_entry_id
4070
5618
  ON recall_events(entry_id)
@@ -4086,6 +5634,19 @@ var CREATE_ENTRIES_EMBEDDING_INDEX_SQL = `
4086
5634
  AND retired = 0
4087
5635
  AND superseded_by IS NULL
4088
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
+ `;
4089
5650
  var SCHEMA_STATEMENTS = [
4090
5651
  CREATE_ENTRIES_TABLE_SQL,
4091
5652
  CREATE_ENTRIES_FTS_TABLE_SQL,
@@ -4093,6 +5654,8 @@ var SCHEMA_STATEMENTS = [
4093
5654
  CREATE_ENTRIES_FTS_DELETE_TRIGGER_SQL,
4094
5655
  CREATE_ENTRIES_FTS_UPDATE_TRIGGER_SQL,
4095
5656
  CREATE_INGEST_LOG_TABLE_SQL,
5657
+ CREATE_EPISODES_TABLE_SQL,
5658
+ CREATE_TASKS_TABLE_SQL,
4096
5659
  CREATE_RECALL_EVENTS_TABLE_SQL,
4097
5660
  CREATE_SURGEON_RUNS_TABLE_SQL,
4098
5661
  CREATE_META_TABLE_SQL,
@@ -4102,6 +5665,15 @@ var SCHEMA_STATEMENTS = [
4102
5665
  CREATE_ENTRIES_EXPIRY_INDEX_SQL,
4103
5666
  CREATE_ENTRIES_RETIRED_INDEX_SQL,
4104
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,
4105
5677
  CREATE_RECALL_EVENTS_ENTRY_ID_INDEX_SQL,
4106
5678
  CREATE_RECALL_EVENTS_RECALLED_AT_INDEX_SQL
4107
5679
  ];
@@ -4112,6 +5684,13 @@ async function initSchema(db) {
4112
5684
  for (const statement of SCHEMA_STATEMENTS) {
4113
5685
  await db.execute(statement);
4114
5686
  }
5687
+ await ensureSurgeonSchema(db);
5688
+ if (currentVersion === "2") {
5689
+ await migrateSchemaV2ToV3(db);
5690
+ }
5691
+ if (currentVersion === "3") {
5692
+ await migrateSchemaV3ToV4(db);
5693
+ }
4115
5694
  await db.execute({
4116
5695
  sql: `
4117
5696
  INSERT INTO _meta (key, value)
@@ -4127,7 +5706,84 @@ async function initSchema(db) {
4127
5706
  if (currentVersion !== SCHEMA_VERSION || !hadEntriesFts) {
4128
5707
  await rebuildFts(db);
4129
5708
  }
4130
- await ensureVectorIndex(db);
5709
+ await ensureVectorIndexes(db);
5710
+ }
5711
+ async function ensureSurgeonSchema(db) {
5712
+ const columns = await db.execute("PRAGMA table_info('surgeon_runs')");
5713
+ const existingColumns = new Set(
5714
+ columns.rows.map((row) => {
5715
+ const name = row.name;
5716
+ return typeof name === "string" ? name : "";
5717
+ }).filter((name) => name.length > 0)
5718
+ );
5719
+ const migrations = [
5720
+ {
5721
+ column: "pass_type",
5722
+ sql: "ALTER TABLE surgeon_runs ADD COLUMN pass_type TEXT NOT NULL DEFAULT 'retirement'"
5723
+ },
5724
+ { column: "project", sql: "ALTER TABLE surgeon_runs ADD COLUMN project TEXT" },
5725
+ {
5726
+ column: "status",
5727
+ sql: "ALTER TABLE surgeon_runs ADD COLUMN status TEXT NOT NULL DEFAULT 'completed'"
5728
+ },
5729
+ { column: "input_tokens", sql: "ALTER TABLE surgeon_runs ADD COLUMN input_tokens INTEGER DEFAULT 0" },
5730
+ { column: "output_tokens", sql: "ALTER TABLE surgeon_runs ADD COLUMN output_tokens INTEGER DEFAULT 0" },
5731
+ { column: "estimated_cost_usd", sql: "ALTER TABLE surgeon_runs ADD COLUMN estimated_cost_usd REAL DEFAULT 0" },
5732
+ { column: "model", sql: "ALTER TABLE surgeon_runs ADD COLUMN model TEXT" },
5733
+ { column: "actions_skipped", sql: "ALTER TABLE surgeon_runs ADD COLUMN actions_skipped INTEGER DEFAULT 0" },
5734
+ { column: "entries_retired", sql: "ALTER TABLE surgeon_runs ADD COLUMN entries_retired INTEGER DEFAULT 0" },
5735
+ { column: "summary_json", sql: "ALTER TABLE surgeon_runs ADD COLUMN summary_json TEXT" },
5736
+ { column: "error", sql: "ALTER TABLE surgeon_runs ADD COLUMN error TEXT" },
5737
+ { column: "dry_run", sql: "ALTER TABLE surgeon_runs ADD COLUMN dry_run INTEGER NOT NULL DEFAULT 1" },
5738
+ { column: "config_json", sql: "ALTER TABLE surgeon_runs ADD COLUMN config_json TEXT" }
5739
+ ];
5740
+ for (const migration of migrations) {
5741
+ if (!existingColumns.has(migration.column)) {
5742
+ await db.execute(migration.sql);
5743
+ }
5744
+ }
5745
+ await db.execute(CREATE_SURGEON_RUN_ACTIONS_TABLE_SQL);
5746
+ await db.execute(CREATE_SURGEON_RUN_ACTIONS_RUN_ID_INDEX_SQL);
5747
+ await db.execute(CREATE_SURGEON_RUN_ACTIONS_ENTRY_ID_INDEX_SQL);
5748
+ await db.execute(CREATE_SURGEON_RUN_ACTIONS_CREATED_AT_INDEX_SQL);
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
+ }
4131
5787
  }
4132
5788
  async function rebuildFts(db) {
4133
5789
  await db.execute("INSERT INTO entries_fts(entries_fts) VALUES ('rebuild')");
@@ -4145,7 +5801,7 @@ async function prepareBulkWrites(db) {
4145
5801
  await db.execute("DROP TRIGGER IF EXISTS entries_ai");
4146
5802
  await db.execute("DROP TRIGGER IF EXISTS entries_ad");
4147
5803
  await db.execute("DROP TRIGGER IF EXISTS entries_au");
4148
- await dropVectorIndex(db);
5804
+ await dropVectorIndexes(db);
4149
5805
  });
4150
5806
  }
4151
5807
  async function finalizeBulkWrites(db) {
@@ -4154,7 +5810,7 @@ async function finalizeBulkWrites(db) {
4154
5810
  await db.execute(CREATE_ENTRIES_FTS_DELETE_TRIGGER_SQL);
4155
5811
  await db.execute(CREATE_ENTRIES_FTS_UPDATE_TRIGGER_SQL);
4156
5812
  await rebuildFts(db);
4157
- await ensureVectorIndex(db);
5813
+ await ensureVectorIndexes(db);
4158
5814
  await db.execute({
4159
5815
  sql: "DELETE FROM _meta WHERE key = ?",
4160
5816
  args: [BULK_WRITE_STATE_META_KEY]
@@ -4187,6 +5843,13 @@ async function tableExists(db, tableName) {
4187
5843
  });
4188
5844
  return result.rows.length > 0;
4189
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
+ }
4190
5853
  async function hasActiveBulkWriteState(db) {
4191
5854
  try {
4192
5855
  const result = await db.execute({
@@ -4199,18 +5862,20 @@ async function hasActiveBulkWriteState(db) {
4199
5862
  return false;
4200
5863
  }
4201
5864
  }
4202
- async function ensureVectorIndex(db) {
5865
+ async function ensureVectorIndexes(db) {
4203
5866
  try {
4204
5867
  await db.execute(CREATE_ENTRIES_EMBEDDING_INDEX_SQL);
5868
+ await db.execute(CREATE_EPISODES_EMBEDDING_INDEX_SQL);
4205
5869
  } catch (error) {
4206
5870
  if (!isVectorUnavailableError(error)) {
4207
5871
  throw error;
4208
5872
  }
4209
5873
  }
4210
5874
  }
4211
- async function dropVectorIndex(db) {
5875
+ async function dropVectorIndexes(db) {
4212
5876
  try {
4213
5877
  await db.execute(`DROP INDEX IF EXISTS ${VECTOR_INDEX_NAME}`);
5878
+ await db.execute(`DROP INDEX IF EXISTS ${EPISODE_VECTOR_INDEX_NAME}`);
4214
5879
  } catch (error) {
4215
5880
  if (!isVectorUnavailableError(error)) {
4216
5881
  throw error;
@@ -4306,9 +5971,9 @@ async function probeVectorAvailability(services) {
4306
5971
  }
4307
5972
 
4308
5973
  // ../../src/config.ts
4309
- import fs6 from "fs";
4310
- import os2 from "os";
4311
- import path5 from "path";
5974
+ import fs7 from "fs";
5975
+ import os3 from "os";
5976
+ import path7 from "path";
4312
5977
  import { fileURLToPath } from "url";
4313
5978
  var AUTH_METHOD_DEFINITIONS = [
4314
5979
  {
@@ -4348,7 +6013,7 @@ var AUTH_METHOD_DEFINITIONS = [
4348
6013
  }
4349
6014
  ];
4350
6015
  var AUTH_METHOD_SET = new Set(AUTH_METHOD_DEFINITIONS.map((definition) => definition.id));
4351
- var DEFAULT_CONFIG_DIR = path5.join(os2.homedir(), ".agenr");
6016
+ var DEFAULT_CONFIG_DIR = path7.join(os3.homedir(), ".agenr");
4352
6017
  var DEFAULT_DB_NAME = "knowledge.db";
4353
6018
  function resolveConfigDir() {
4354
6019
  return process.env.AGENR_CONFIG_DIR ?? DEFAULT_CONFIG_DIR;
@@ -4366,18 +6031,18 @@ function resolveConfigPath(options = {}) {
4366
6031
  if (adjacentConfigPath) {
4367
6032
  return adjacentConfigPath;
4368
6033
  }
4369
- return path5.join(resolveConfigDir(), "config.json");
6034
+ return path7.join(resolveConfigDir(), "config.json");
4370
6035
  }
4371
6036
  function resolveDbPath(config) {
4372
- return process.env.AGENR_DB_PATH ?? config?.dbPath ?? path5.join(resolveConfigDir(), DEFAULT_DB_NAME);
6037
+ return process.env.AGENR_DB_PATH ?? config?.dbPath ?? path7.join(resolveConfigDir(), DEFAULT_DB_NAME);
4373
6038
  }
4374
6039
  function readConfig(options = {}) {
4375
6040
  const configPath = resolveFilesystemPath(resolveConfigPath(options));
4376
- if (!fs6.existsSync(configPath)) {
6041
+ if (!fs7.existsSync(configPath)) {
4377
6042
  return {};
4378
6043
  }
4379
6044
  try {
4380
- const raw = fs6.readFileSync(configPath, "utf-8");
6045
+ const raw = fs7.readFileSync(configPath, "utf-8");
4381
6046
  return JSON.parse(raw);
4382
6047
  } catch {
4383
6048
  return {};
@@ -4390,12 +6055,12 @@ function resolveAdjacentConfigPath(dbPath) {
4390
6055
  }
4391
6056
  if (normalizedDbPath.startsWith("file:")) {
4392
6057
  try {
4393
- return path5.join(path5.dirname(fileURLToPath(normalizedDbPath)), "config.json");
6058
+ return path7.join(path7.dirname(fileURLToPath(normalizedDbPath)), "config.json");
4394
6059
  } catch {
4395
6060
  return void 0;
4396
6061
  }
4397
6062
  }
4398
- return path5.join(path5.dirname(normalizedDbPath), "config.json");
6063
+ return path7.join(path7.dirname(normalizedDbPath), "config.json");
4399
6064
  }
4400
6065
  function normalizeOptionalString2(value) {
4401
6066
  const normalized = value?.trim();
@@ -4443,6 +6108,8 @@ async function insertEntry(executor, entry, embedding, contentHash) {
4443
6108
  last_recalled_at,
4444
6109
  superseded_by,
4445
6110
  cluster_id,
6111
+ user_id,
6112
+ project,
4446
6113
  retired,
4447
6114
  retired_at,
4448
6115
  retired_reason,
@@ -4452,7 +6119,7 @@ async function insertEntry(executor, entry, embedding, contentHash) {
4452
6119
  VALUES (
4453
6120
  ?, ?, ?, ?, ?, ?, ?, ?, ?,
4454
6121
  CASE WHEN ? IS NULL THEN NULL ELSE vector32(?) END,
4455
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
6122
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
4456
6123
  )
4457
6124
  `,
4458
6125
  args: [
@@ -4475,6 +6142,8 @@ async function insertEntry(executor, entry, embedding, contentHash) {
4475
6142
  normalizeOptionalString3(entry.last_recalled_at),
4476
6143
  normalizeOptionalString3(entry.superseded_by),
4477
6144
  normalizeOptionalString3(entry.cluster_id),
6145
+ normalizeOptionalString3(entry.user_id),
6146
+ normalizeOptionalString3(entry.project),
4478
6147
  entry.retired ? 1 : 0,
4479
6148
  normalizeOptionalString3(entry.retired_at),
4480
6149
  normalizeOptionalString3(entry.retired_reason),
@@ -4512,6 +6181,8 @@ async function getEntries(executor, ids) {
4512
6181
  last_recalled_at,
4513
6182
  superseded_by,
4514
6183
  cluster_id,
6184
+ user_id,
6185
+ project,
4515
6186
  retired,
4516
6187
  retired_at,
4517
6188
  retired_reason,
@@ -4738,6 +6409,8 @@ var ENTRY_SELECT_COLUMNS2 = `
4738
6409
  e.last_recalled_at,
4739
6410
  e.superseded_by,
4740
6411
  e.cluster_id,
6412
+ e.user_id,
6413
+ e.project,
4741
6414
  e.retired,
4742
6415
  e.retired_at,
4743
6416
  e.retired_reason,
@@ -4966,9 +6639,470 @@ function wrapVectorError(error) {
4966
6639
  }
4967
6640
 
4968
6641
  // ../../src/adapters/db/client.ts
4969
- import fs7 from "fs/promises";
4970
- import path6 from "path";
6642
+ import fs8 from "fs/promises";
6643
+ import path8 from "path";
4971
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
4972
7106
  var DEFAULT_BUSY_TIMEOUT_MS = 3e3;
4973
7107
  async function createDatabase(dbPath) {
4974
7108
  const client = await openClient(dbPath);
@@ -5006,6 +7140,34 @@ var LibsqlDatabase = class _LibsqlDatabase {
5006
7140
  async findExistingHashes(hashes) {
5007
7141
  return findExistingHashes(this.executor, hashes);
5008
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
+ }
5009
7171
  /** Finds which normalized content hashes already exist in storage. */
5010
7172
  async findExistingNormHashes(hashes) {
5011
7173
  return findExistingNormHashes(this.executor, hashes);
@@ -5066,8 +7228,8 @@ async function openClient(dbPath) {
5066
7228
  throw new Error("Database path must not be empty.");
5067
7229
  }
5068
7230
  if (trimmedPath !== ":memory:" && !trimmedPath.startsWith("file:")) {
5069
- const resolvedPath = path6.resolve(trimmedPath);
5070
- await fs7.mkdir(path6.dirname(resolvedPath), { recursive: true });
7231
+ const resolvedPath = path8.resolve(trimmedPath);
7232
+ await fs8.mkdir(path8.dirname(resolvedPath), { recursive: true });
5071
7233
  }
5072
7234
  const client = createClient({ url: resolveClientUrl(trimmedPath) });
5073
7235
  await client.execute("PRAGMA foreign_keys = ON");
@@ -5084,7 +7246,7 @@ function resolveClientUrl(dbPath) {
5084
7246
  if (dbPath.startsWith("file:")) {
5085
7247
  return dbPath;
5086
7248
  }
5087
- return `file:${path6.resolve(dbPath)}`;
7249
+ return `file:${path8.resolve(dbPath)}`;
5088
7250
  }
5089
7251
  async function rollbackTransaction(transaction) {
5090
7252
  if (transaction.closed) {
@@ -5197,7 +7359,6 @@ function requireApiKey(status) {
5197
7359
  function createSessionStartTracker() {
5198
7360
  const seenSessionIds = /* @__PURE__ */ new Set();
5199
7361
  const seenSessionKeys = /* @__PURE__ */ new Set();
5200
- const latestResetBySessionKey = /* @__PURE__ */ new Map();
5201
7362
  const resumedFromBySessionId = /* @__PURE__ */ new Map();
5202
7363
  const countActiveSessions = () => seenSessionIds.size + seenSessionKeys.size;
5203
7364
  return {
@@ -5235,18 +7396,6 @@ function createSessionStartTracker() {
5235
7396
  activeCount: countActiveSessions()
5236
7397
  };
5237
7398
  },
5238
- rememberReset(sessionKey, record) {
5239
- const normalizedSessionKey = sessionKey?.trim();
5240
- const normalizedSessionFile = record.sessionFile.trim();
5241
- if (!normalizedSessionKey || normalizedSessionFile.length === 0) {
5242
- return;
5243
- }
5244
- latestResetBySessionKey.set(normalizedSessionKey, {
5245
- sessionFile: normalizedSessionFile,
5246
- recordedAt: record.recordedAt,
5247
- ...record.sessionId?.trim() ? { sessionId: record.sessionId.trim() } : {}
5248
- });
5249
- },
5250
7399
  rememberSessionStart(sessionId, _sessionKey, resumedFrom) {
5251
7400
  const normalizedSessionId = sessionId?.trim();
5252
7401
  const normalizedResumedFrom = resumedFrom?.trim();
@@ -5255,10 +7404,6 @@ function createSessionStartTracker() {
5255
7404
  }
5256
7405
  resumedFromBySessionId.set(normalizedSessionId, normalizedResumedFrom);
5257
7406
  },
5258
- getLatestReset(sessionKey) {
5259
- const normalizedSessionKey = sessionKey?.trim();
5260
- return normalizedSessionKey ? latestResetBySessionKey.get(normalizedSessionKey) : void 0;
5261
- },
5262
7407
  getResumedFrom(sessionId) {
5263
7408
  const normalizedSessionId = sessionId?.trim();
5264
7409
  return normalizedSessionId ? resumedFromBySessionId.get(normalizedSessionId) : void 0;
@@ -5295,14 +7440,6 @@ var openclaw_default = definePluginEntry({
5295
7440
  tracker
5296
7441
  })
5297
7442
  );
5298
- api.on(
5299
- "before_reset",
5300
- (event, ctx) => handleAgenrBeforeReset(event, ctx, {
5301
- logger: api.logger,
5302
- servicesPromise,
5303
- tracker
5304
- })
5305
- );
5306
7443
  api.on("session_start", (event) => {
5307
7444
  tracker.rememberSessionStart(event.sessionId, event.sessionKey, event.resumedFrom);
5308
7445
  });