@agenr/openclaw-plugin 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js 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 = [
@@ -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,212 +3896,253 @@ var OpenClawTranscriptParser = class {
3197
3896
  * @returns Parsed transcript messages, warnings, and metadata.
3198
3897
  */
3199
3898
  async parseFile(filePath, options) {
3200
- const raw = await 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);
3394
- }
3395
- function renderTranscriptForSummary(messages) {
3396
- return messages.map((message) => `${message.role === "user" ? "User" : "Assistant"}: ${message.text.trim()}`).join("\n");
3397
- }
3398
- function debugLog4(logger, subsystem, message) {
3399
- logger.debug?.(`[agenr] ${subsystem}: ${message}`);
3400
- }
3401
- function normalizeSummary(value) {
3402
- const trimmed = value.trim();
3403
- return trimmed.replace(/^# .+\n+/u, "").trim();
3404
- }
3405
- function resolveSummaryExecution(openClaw, requestedAgentId) {
4145
+ function resolveEpisodeSummaryExecution(openClaw, requestedAgentId) {
3406
4146
  const agentId = requestedAgentId?.trim() || resolveDefaultAgentId(openClaw.config);
3407
4147
  const modelRef = resolveAgentEffectiveModelPrimary(openClaw.config, agentId);
3408
4148
  const parsedModelRef = modelRef ? parseModelRef(modelRef, DEFAULT_PROVIDER) : null;
@@ -3410,224 +4150,965 @@ function resolveSummaryExecution(openClaw, requestedAgentId) {
3410
4150
  agentId,
3411
4151
  agentDir: openClaw.runtime.agent.resolveAgentDir(openClaw.config, agentId),
3412
4152
  workspaceDir: openClaw.runtime.agent.resolveAgentWorkspaceDir(openClaw.config, agentId),
3413
- modelRef,
3414
4153
  provider: parsedModelRef?.provider ?? DEFAULT_PROVIDER,
3415
4154
  model: parsedModelRef?.model ?? DEFAULT_MODEL
3416
4155
  };
3417
4156
  }
3418
- function formatResolvedModel(provider, model) {
4157
+ function formatResolvedEpisodeSummaryModel(provider, model) {
3419
4158
  return `${provider}/${model}`;
3420
4159
  }
3421
- async function createTempSummarySessionFile() {
3422
- const tempDir = await fs5.mkdtemp(path4.join(os.tmpdir(), "agenr-summary-"));
3423
- return path4.join(tempDir, "session.jsonl");
3424
- }
3425
- async function cleanupTempSummarySessionFile(tempSessionFile) {
3426
- if (!tempSessionFile) {
3427
- return;
3428
- }
3429
- try {
3430
- await fs5.rm(path4.dirname(tempSessionFile), {
3431
- recursive: true,
3432
- force: true
3433
- });
3434
- } catch {
4160
+ async function maybeEmbedEpisodeSummary(params) {
4161
+ if (!params.embeddingAvailable) {
4162
+ params.logger.info(
4163
+ `[agenr] session-start predecessor episode embedding skipped for ${params.sessionContext} predecessor=${params.predecessorFile} reason=embedding_unavailable`
4164
+ );
4165
+ return void 0;
3435
4166
  }
3436
- }
3437
- function extractEmbeddedAgentText(result) {
3438
- return result.payloads?.find((payload) => payload.text?.trim())?.text ?? "";
3439
- }
3440
- function formatErrorMessage3(error) {
3441
- return error instanceof Error ? error.message : String(error);
3442
- }
3443
- function capTranscript(transcript, maxChars) {
3444
- if (transcript.length <= maxChars) {
3445
- return transcript;
4167
+ const remainingBudgetMs = params.deadlineMs - Date.now();
4168
+ if (remainingBudgetMs < EPISODE_EMBEDDING_MIN_HEADROOM_MS) {
4169
+ params.logger.info(
4170
+ `[agenr] session-start predecessor episode embedding skipped for ${params.sessionContext} predecessor=${params.predecessorFile} reason=budget_tight remainingMs=${Math.max(
4171
+ 0,
4172
+ remainingBudgetMs
4173
+ )}`
4174
+ );
4175
+ return void 0;
3446
4176
  }
3447
- 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));
4177
+ try {
4178
+ const result = await awaitEmbeddingWithTimeout(params.embedding.embed([params.summary]), remainingBudgetMs);
4179
+ if (result === EPISODE_EMBEDDING_TIMEOUT) {
4180
+ params.logger.info(
4181
+ `[agenr] session-start predecessor episode embedding skipped for ${params.sessionContext} predecessor=${params.predecessorFile} reason=embedding_timeout budgetMs=${remainingBudgetMs}`
4182
+ );
4183
+ return void 0;
4184
+ }
4185
+ const vector = result[0]?.map((value) => Number.isFinite(value) ? value : 0);
4186
+ if (!vector || vector.length === 0) {
4187
+ params.logger.info(
4188
+ `[agenr] session-start predecessor episode embedding skipped for ${params.sessionContext} predecessor=${params.predecessorFile} reason=empty_embedding`
4189
+ );
4190
+ return void 0;
4191
+ }
4192
+ return vector;
4193
+ } catch (error) {
4194
+ params.logger.info(
4195
+ `[agenr] session-start predecessor episode embedding skipped for ${params.sessionContext} predecessor=${params.predecessorFile} reason=${formatErrorMessage2(error)}`
4196
+ );
4197
+ return void 0;
4198
+ }
4199
+ }
4200
+ async function awaitWithTimeout(promise, timeoutMs) {
4201
+ return new Promise((resolve, reject) => {
4202
+ const timeout = setTimeout(() => {
4203
+ resolve(EPISODE_SUMMARY_TIMEOUT);
4204
+ }, timeoutMs);
4205
+ promise.then(
4206
+ (value) => {
4207
+ clearTimeout(timeout);
4208
+ resolve(value);
4209
+ },
4210
+ (error) => {
4211
+ clearTimeout(timeout);
4212
+ reject(error);
4213
+ }
4214
+ );
4215
+ });
4216
+ }
4217
+ async function awaitEmbeddingWithTimeout(promise, timeoutMs) {
4218
+ return new Promise((resolve, reject) => {
4219
+ const timeout = setTimeout(() => {
4220
+ resolve(EPISODE_EMBEDDING_TIMEOUT);
4221
+ }, timeoutMs);
4222
+ promise.then(
4223
+ (value) => {
4224
+ clearTimeout(timeout);
4225
+ resolve(value);
4226
+ },
4227
+ (error) => {
4228
+ clearTimeout(timeout);
4229
+ reject(error);
4230
+ }
4231
+ );
4232
+ });
4233
+ }
4234
+ async function createTempEpisodeSummarySessionFile() {
4235
+ const tempDir = await fs3.mkdtemp(path.join(os.tmpdir(), "agenr-episode-summary-"));
4236
+ return path.join(tempDir, "session.jsonl");
4237
+ }
4238
+ async function cleanupTempEpisodeSummarySessionFile(tempEpisodeSummarySessionFile) {
4239
+ try {
4240
+ await fs3.rm(path.dirname(tempEpisodeSummarySessionFile), {
4241
+ recursive: true,
4242
+ force: true
4243
+ });
4244
+ } catch {
4245
+ }
4246
+ }
4247
+ function extractEmbeddedAgentText(result) {
4248
+ return result.payloads?.find((payload) => payload.text?.trim())?.text ?? "";
4249
+ }
4250
+
4251
+ // ../../src/adapters/openclaw/format/recall-format.ts
4252
+ var MAX_CONTENT_CHARS = 280;
4253
+ function formatAgenrSessionStartRecall(recall2) {
4254
+ const sections = buildSections(recall2);
4255
+ if (sections.length === 0) {
4256
+ return "";
4257
+ }
4258
+ const lines = ["## Agenr Session Recall", "Use this as prior context. Confirm anything important if the current conversation conflicts with it.", ""];
4259
+ for (const section of sections) {
4260
+ lines.push(`### ${section.title}`);
4261
+ for (const item of section.entries) {
4262
+ lines.push(formatEntryHeader(item));
4263
+ lines.push(formatEntryBody(item.entry));
4264
+ }
4265
+ lines.push("");
4266
+ }
4267
+ return lines.join("\n").trim();
4268
+ }
4269
+ function buildSections(recall2) {
4270
+ const sections = [];
4271
+ const coreEntries = recall2.core.map((entry) => ({ entry }));
4272
+ if (coreEntries.length > 0) {
4273
+ sections.push({ title: "Core Memory", entries: coreEntries });
4274
+ }
4275
+ return sections;
4276
+ }
4277
+ function formatEntryHeader(item) {
4278
+ const metadata = [
4279
+ item.entry.id,
4280
+ item.entry.type,
4281
+ item.entry.expiry,
4282
+ `importance ${item.entry.importance}`,
4283
+ item.score !== void 0 ? `score ${item.score.toFixed(2)}` : void 0
4284
+ ].filter((value) => value !== void 0);
4285
+ return `- [${metadata.join(" | ")}] ${item.entry.subject}`;
4286
+ }
4287
+ function formatEntryBody(entry) {
4288
+ const content = truncate2(entry.content.trim(), MAX_CONTENT_CHARS);
4289
+ const extra = [
4290
+ entry.tags.length > 0 ? `tags: ${entry.tags.join(", ")}` : void 0,
4291
+ entry.created_at ? `created: ${entry.created_at.slice(0, 10)}` : void 0
4292
+ ].filter((value) => value !== void 0);
4293
+ if (extra.length === 0) {
4294
+ return ` ${content}`;
4295
+ }
4296
+ return ` ${content} (${extra.join(" | ")})`;
4297
+ }
4298
+ function truncate2(value, maxChars) {
4299
+ if (value.length <= maxChars) {
4300
+ return value;
4301
+ }
4302
+ return `${value.slice(0, maxChars - 3).trimEnd()}...`;
4303
+ }
4304
+
4305
+ // ../../src/adapters/openclaw/session/continuity/continuity-summary-generator.ts
4306
+ import { promises as fs5 } from "fs";
4307
+ import os2 from "os";
4308
+ import path4 from "path";
4309
+ import { DEFAULT_MODEL as DEFAULT_MODEL2, DEFAULT_PROVIDER as DEFAULT_PROVIDER2, parseModelRef as parseModelRef2, resolveAgentEffectiveModelPrimary as resolveAgentEffectiveModelPrimary2, resolveDefaultAgentId as resolveDefaultAgentId2 } from "openclaw/plugin-sdk/agent-runtime";
4310
+
4311
+ // ../../src/adapters/openclaw/session/continuity/continuity-summary-reader.ts
4312
+ import { promises as fs4 } from "fs";
4313
+ import path3 from "path";
4314
+
4315
+ // ../../src/adapters/openclaw/session/session-id.ts
4316
+ import path2 from "path";
4317
+ function deriveOpenClawSessionIdFromFilePath(sessionFile, logger) {
4318
+ const normalizedSessionFile = sessionFile.trim();
4319
+ if (normalizedSessionFile.length === 0) {
4320
+ debugLog(logger, "session-id", "cannot derive session id from empty session file path");
4321
+ return void 0;
4322
+ }
4323
+ const fileName = path2.basename(normalizedSessionFile);
4324
+ const sessionId = fileName.replace(/\.jsonl(?:\..*)?$/i, "").trim();
4325
+ debugLog(logger, "session-id", `derived session id "${sessionId || "<empty>"}" from file=${normalizedSessionFile}`);
4326
+ return sessionId.length > 0 ? sessionId : void 0;
4327
+ }
4328
+ function debugLog(logger, subsystem, message) {
4329
+ logger?.debug?.(`[agenr] ${subsystem}: ${message}`);
4330
+ }
4331
+
4332
+ // ../../src/adapters/openclaw/session/continuity/continuity-summary-reader.ts
4333
+ function resolveOpenClawContinuitySummaryPath(sessionFile, logger) {
4334
+ const normalizedSessionFile = sessionFile.trim();
4335
+ const sessionId = deriveOpenClawSessionIdFromFilePath(normalizedSessionFile, logger);
4336
+ if (!sessionId) {
4337
+ return void 0;
4338
+ }
4339
+ const continuitySummaryPath = path3.join(path3.dirname(normalizedSessionFile), `${sessionId}.continuity-summary.md`);
4340
+ debugLog2(logger, "continuity-summary-reader", `resolved continuity summary path for session=${sessionId}: ${continuitySummaryPath}`);
4341
+ return continuitySummaryPath;
4342
+ }
4343
+ async function readOpenClawContinuitySummaryFile(sessionFile, logger) {
4344
+ const continuitySummaryPath = resolveOpenClawContinuitySummaryPath(sessionFile, logger);
4345
+ const sessionId = deriveOpenClawSessionIdFromFilePath(sessionFile, logger);
4346
+ if (!continuitySummaryPath || !sessionId) {
4347
+ return null;
4348
+ }
4349
+ try {
4350
+ const continuitySummaryContent = (await fs4.readFile(continuitySummaryPath, "utf8")).trim();
4351
+ if (continuitySummaryContent.length === 0) {
4352
+ debugLog2(logger, "continuity-summary-reader", `continuity summary file is empty for session=${sessionId} path=${continuitySummaryPath}`);
4353
+ return null;
4354
+ }
4355
+ debugLog2(
4356
+ logger,
4357
+ "continuity-summary-reader",
4358
+ `loaded continuity summary file for session=${sessionId} path=${continuitySummaryPath} chars=${continuitySummaryContent.length}`
4359
+ );
4360
+ return {
4361
+ sessionId,
4362
+ continuitySummaryPath,
4363
+ content: continuitySummaryContent
4364
+ };
4365
+ } catch (error) {
4366
+ if (isFileNotFound(error)) {
4367
+ debugLog2(logger, "continuity-summary-reader", `continuity summary file missing for session=${sessionId} path=${continuitySummaryPath}`);
4368
+ return null;
4369
+ }
4370
+ throw error;
4371
+ }
4372
+ }
4373
+ function debugLog2(logger, subsystem, message) {
4374
+ logger?.debug?.(`[agenr] ${subsystem}: ${message}`);
4375
+ }
4376
+ function isFileNotFound(error) {
4377
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
4378
+ }
4379
+
4380
+ // ../../src/adapters/openclaw/session/continuity/continuity-summary-generator.ts
4381
+ var MIN_CONTINUITY_SUMMARY_MESSAGES = 4;
4382
+ var MAX_CONTINUITY_TRANSCRIPT_CHARS = 14e3;
4383
+ var CONTINUITY_SUMMARY_TIMEOUT_MS = 15e3;
4384
+ var CONTINUITY_SUMMARY_SYSTEM_PROMPT = [
4385
+ "You write concise narrative continuity summaries that help the next session continue smoothly.",
4386
+ "The transcript can be about any domain. Do not assume technical, project, or coding context unless the transcript shows it.",
4387
+ "Write 200 to 500 words in plain Markdown with no code fences.",
4388
+ "Capture:",
4389
+ "- what topics were discussed",
4390
+ "- what was learned, decided, agreed on, or corrected",
4391
+ "- what remains unfinished, open, or pending",
4392
+ "- user preferences, clarifications, and constraints that matter for continuity",
4393
+ "- the overall direction or intent of the conversation",
4394
+ "Do not replay the transcript turn by turn. Do not invent facts. If something is uncertain, say so briefly."
4395
+ ].join("\n");
4396
+ async function generateAndWriteOpenClawContinuitySummary(params) {
4397
+ const sessionFile = params.sessionFile.trim();
4398
+ const continuitySummaryPath = resolveOpenClawContinuitySummaryPath(sessionFile, params.logger);
4399
+ if (!continuitySummaryPath) {
4400
+ return {
4401
+ status: "skipped",
4402
+ reason: "missing_session_id"
4403
+ };
4404
+ }
4405
+ const parsedTranscript = await openClawTranscriptParser.parseFile(sessionFile);
4406
+ const cleanedMessages = parsedTranscript.messages.filter((message) => message.text.trim().length > 0);
4407
+ const transcript = renderTranscriptForContinuitySummary(cleanedMessages);
4408
+ const normalizedTranscript = capContinuityTranscript(transcript, MAX_CONTINUITY_TRANSCRIPT_CHARS);
4409
+ debugLog3(
4410
+ params.logger,
4411
+ "continuity-summary",
4412
+ `transcript adapter output for file=${sessionFile}: messages=${cleanedMessages.length} chars=${normalizedTranscript.length}`
4413
+ );
4414
+ if (cleanedMessages.length === 0 || normalizedTranscript.length === 0) {
4415
+ return {
4416
+ status: "skipped",
4417
+ reason: "empty",
4418
+ continuitySummaryPath,
4419
+ messageCount: cleanedMessages.length,
4420
+ transcriptChars: normalizedTranscript.length
4421
+ };
4422
+ }
4423
+ if (cleanedMessages.length < MIN_CONTINUITY_SUMMARY_MESSAGES) {
4424
+ return {
4425
+ status: "skipped",
4426
+ reason: "too_short",
4427
+ continuitySummaryPath,
4428
+ messageCount: cleanedMessages.length,
4429
+ transcriptChars: normalizedTranscript.length
4430
+ };
4431
+ }
4432
+ const continuitySummaryExecution = resolveContinuitySummaryExecution(params.openClaw, params.agentId);
4433
+ const continuitySummaryModel = formatResolvedContinuitySummaryModel(continuitySummaryExecution.provider, continuitySummaryExecution.model);
4434
+ const prompt = [
4435
+ "Produce a concise continuity summary for the next session.",
4436
+ "Prefer short paragraphs. Use a short 'Open loops' section only if it adds clarity.",
4437
+ "",
4438
+ "Transcript:",
4439
+ normalizedTranscript
4440
+ ].join("\n");
4441
+ debugLog3(
4442
+ params.logger,
4443
+ "continuity-summary",
4444
+ `sending continuity summary prompt model=${continuitySummaryModel} promptChars=${prompt.length} transcriptChars=${normalizedTranscript.length}`
4445
+ );
4446
+ params.logger.info(
4447
+ `[agenr] continuity-summary: using OpenClaw embedded agent provider=${continuitySummaryExecution.provider} model=${continuitySummaryExecution.model} agent=${continuitySummaryExecution.agentId}`
4448
+ );
4449
+ debugLog3(
4450
+ params.logger,
4451
+ "continuity-summary",
4452
+ `resolved OpenClaw continuity summary model for file=${sessionFile}: agentId=${continuitySummaryExecution.agentId} modelRef=${continuitySummaryExecution.modelRef ?? "default"} provider=${continuitySummaryExecution.provider} model=${continuitySummaryExecution.model}`
4453
+ );
4454
+ const runEmbeddedPiAgent = params.openClaw.runtime.agent.runEmbeddedPiAgent;
4455
+ if (typeof runEmbeddedPiAgent !== "function") {
4456
+ params.logger.warn?.(`[agenr] continuity-summary: OpenClaw embedded agent runner unavailable for file=${sessionFile}`);
4457
+ return {
4458
+ status: "skipped",
4459
+ reason: "embedded_agent_unavailable",
4460
+ continuitySummaryPath,
4461
+ messageCount: cleanedMessages.length,
4462
+ transcriptChars: normalizedTranscript.length,
4463
+ model: continuitySummaryModel
4464
+ };
4465
+ }
4466
+ const startedAt = Date.now();
4467
+ let tempContinuitySummarySessionFile;
4468
+ try {
4469
+ tempContinuitySummarySessionFile = await createTempContinuitySummarySessionFile();
4470
+ const runId = `agenr-continuity-summary-${Date.now()}`;
4471
+ const response = extractEmbeddedAgentText2(
4472
+ await runEmbeddedPiAgent({
4473
+ sessionId: runId,
4474
+ sessionKey: "temp:agenr-continuity-summary",
4475
+ agentId: continuitySummaryExecution.agentId,
4476
+ sessionFile: tempContinuitySummarySessionFile,
4477
+ workspaceDir: continuitySummaryExecution.workspaceDir,
4478
+ agentDir: continuitySummaryExecution.agentDir,
4479
+ config: params.openClaw.config,
4480
+ prompt,
4481
+ provider: continuitySummaryExecution.provider,
4482
+ model: continuitySummaryExecution.model,
4483
+ timeoutMs: CONTINUITY_SUMMARY_TIMEOUT_MS,
4484
+ runId,
4485
+ disableTools: true,
4486
+ extraSystemPrompt: CONTINUITY_SUMMARY_SYSTEM_PROMPT
4487
+ })
4488
+ ).trim();
4489
+ const durationMs = Date.now() - startedAt;
4490
+ const normalizedContinuitySummary = normalizeContinuitySummary(response);
4491
+ debugLog3(
4492
+ params.logger,
4493
+ "continuity-summary",
4494
+ `received continuity summary response model=${continuitySummaryModel} durationMs=${durationMs} chars=${normalizedContinuitySummary.length}`
4495
+ );
4496
+ if (normalizedContinuitySummary.length === 0) {
4497
+ return {
4498
+ status: "failed",
4499
+ reason: "empty_response",
4500
+ continuitySummaryPath,
4501
+ messageCount: cleanedMessages.length,
4502
+ transcriptChars: normalizedTranscript.length,
4503
+ model: continuitySummaryModel,
4504
+ durationMs
4505
+ };
4506
+ }
4507
+ const existingContinuitySummary = await readOpenClawContinuitySummaryFile(sessionFile, params.logger);
4508
+ if (existingContinuitySummary?.continuitySummaryPath === continuitySummaryPath) {
4509
+ debugLog3(
4510
+ params.logger,
4511
+ "continuity-summary",
4512
+ `continuity summary file already exists at write time path=${continuitySummaryPath} chars=${existingContinuitySummary.content.length}`
4513
+ );
4514
+ return {
4515
+ status: "skipped",
4516
+ reason: "already_exists",
4517
+ continuitySummaryPath,
4518
+ content: existingContinuitySummary.content,
4519
+ messageCount: cleanedMessages.length,
4520
+ transcriptChars: normalizedTranscript.length,
4521
+ model: continuitySummaryModel,
4522
+ durationMs
4523
+ };
4524
+ }
4525
+ const continuitySummaryBytes = Buffer.byteLength(`${normalizedContinuitySummary}
4526
+ `, "utf8");
4527
+ await fs5.writeFile(continuitySummaryPath, `${normalizedContinuitySummary}
4528
+ `, "utf8");
4529
+ debugLog3(
4530
+ params.logger,
4531
+ "continuity-summary",
4532
+ `wrote continuity summary file path=${continuitySummaryPath} chars=${normalizedContinuitySummary.length} bytes=${continuitySummaryBytes}`
4533
+ );
4534
+ return {
4535
+ status: "written",
4536
+ continuitySummaryPath,
4537
+ content: normalizedContinuitySummary,
4538
+ messageCount: cleanedMessages.length,
4539
+ transcriptChars: normalizedTranscript.length,
4540
+ model: continuitySummaryModel,
4541
+ durationMs,
4542
+ bytesWritten: continuitySummaryBytes
4543
+ };
4544
+ } catch (error) {
4545
+ const durationMs = Date.now() - startedAt;
4546
+ debugLog3(params.logger, "continuity-summary", `continuity summary generation error for file=${sessionFile}: ${formatErrorMessage3(error)}`);
4547
+ return {
4548
+ status: "failed",
4549
+ reason: formatErrorMessage3(error),
4550
+ continuitySummaryPath,
4551
+ messageCount: cleanedMessages.length,
4552
+ transcriptChars: normalizedTranscript.length,
4553
+ model: continuitySummaryModel,
4554
+ durationMs
4555
+ };
4556
+ } finally {
4557
+ await cleanupTempContinuitySummarySessionFile(tempContinuitySummarySessionFile);
4558
+ }
4559
+ }
4560
+ function renderTranscriptForContinuitySummary(messages) {
4561
+ return messages.map((message) => `${message.role === "user" ? "User" : "Assistant"}: ${message.text.trim()}`).join("\n");
4562
+ }
4563
+ function debugLog3(logger, subsystem, message) {
4564
+ logger.debug?.(`[agenr] ${subsystem}: ${message}`);
4565
+ }
4566
+ function normalizeContinuitySummary(value) {
4567
+ const trimmed = value.trim();
4568
+ return trimmed.replace(/^# .+\n+/u, "").trim();
4569
+ }
4570
+ function resolveContinuitySummaryExecution(openClaw, requestedAgentId) {
4571
+ const agentId = requestedAgentId?.trim() || resolveDefaultAgentId2(openClaw.config);
4572
+ const modelRef = resolveAgentEffectiveModelPrimary2(openClaw.config, agentId);
4573
+ const parsedModelRef = modelRef ? parseModelRef2(modelRef, DEFAULT_PROVIDER2) : null;
4574
+ return {
4575
+ agentId,
4576
+ agentDir: openClaw.runtime.agent.resolveAgentDir(openClaw.config, agentId),
4577
+ workspaceDir: openClaw.runtime.agent.resolveAgentWorkspaceDir(openClaw.config, agentId),
4578
+ modelRef,
4579
+ provider: parsedModelRef?.provider ?? DEFAULT_PROVIDER2,
4580
+ model: parsedModelRef?.model ?? DEFAULT_MODEL2
4581
+ };
4582
+ }
4583
+ function formatResolvedContinuitySummaryModel(provider, model) {
4584
+ return `${provider}/${model}`;
4585
+ }
4586
+ async function createTempContinuitySummarySessionFile() {
4587
+ const tempDir = await fs5.mkdtemp(path4.join(os2.tmpdir(), "agenr-continuity-summary-"));
4588
+ return path4.join(tempDir, "session.jsonl");
4589
+ }
4590
+ async function cleanupTempContinuitySummarySessionFile(tempContinuitySummarySessionFile) {
4591
+ if (!tempContinuitySummarySessionFile) {
4592
+ return;
4593
+ }
4594
+ try {
4595
+ await fs5.rm(path4.dirname(tempContinuitySummarySessionFile), {
4596
+ recursive: true,
4597
+ force: true
4598
+ });
4599
+ } catch {
4600
+ }
4601
+ }
4602
+ function extractEmbeddedAgentText2(result) {
4603
+ return result.payloads?.find((payload) => payload.text?.trim())?.text ?? "";
4604
+ }
4605
+ function formatErrorMessage3(error) {
4606
+ return error instanceof Error ? error.message : String(error);
4607
+ }
4608
+ function capContinuityTranscript(transcript, maxChars) {
4609
+ if (transcript.length <= maxChars) {
4610
+ return transcript;
4611
+ }
4612
+ const omissionMarker = "\n\n[Earlier middle transcript omitted for brevity]\n\n";
4613
+ const headBudget = Math.max(0, Math.floor((maxChars - omissionMarker.length) * 0.35));
3449
4614
  const tailBudget = Math.max(0, maxChars - omissionMarker.length - headBudget);
3450
- const head = trimToBoundary(transcript.slice(0, headBudget), false);
3451
- const tail = trimToBoundary(transcript.slice(-tailBudget), true);
4615
+ const head = trimToBoundary2(transcript.slice(0, headBudget), false);
4616
+ const tail = trimToBoundary2(transcript.slice(-tailBudget), true);
3452
4617
  return `${head}${omissionMarker}${tail}`.trim();
3453
4618
  }
3454
- function trimToBoundary(value, fromStart) {
3455
- if (value.length === 0) {
3456
- return value;
4619
+ function trimToBoundary2(value, fromStart) {
4620
+ if (value.length === 0) {
4621
+ return value;
4622
+ }
4623
+ if (fromStart) {
4624
+ const boundary = value.search(/\s/);
4625
+ return boundary >= 0 ? value.slice(boundary).trimStart() : value.trim();
4626
+ }
4627
+ const reversedBoundary = value.trimEnd().search(/\s\S*$/u);
4628
+ return reversedBoundary >= 0 ? value.slice(0, reversedBoundary).trimEnd() : value.trim();
4629
+ }
4630
+
4631
+ // ../../src/adapters/openclaw/session/continuity/predecessor-resolver.ts
4632
+ import path6 from "path";
4633
+
4634
+ // ../../src/adapters/openclaw/session/sessions-store-reader.ts
4635
+ import { promises as fs6 } from "fs";
4636
+ import path5 from "path";
4637
+ async function readOpenClawSessionsStore(sessionsDir, logger) {
4638
+ const normalizedSessionsDir = sessionsDir.trim();
4639
+ if (normalizedSessionsDir.length === 0) {
4640
+ debugLog4(logger, "sessions-store-reader", "skipping sessions.json read because sessionsDir is empty");
4641
+ return [];
4642
+ }
4643
+ const resolvedSessionsDir = path5.resolve(normalizedSessionsDir);
4644
+ const sessionsJsonPath = path5.join(resolvedSessionsDir, "sessions.json");
4645
+ try {
4646
+ const raw = await fs6.readFile(sessionsJsonPath, "utf8");
4647
+ const parsed = JSON.parse(raw);
4648
+ if (!isRecord2(parsed)) {
4649
+ debugLog4(logger, "sessions-store-reader", `sessions.json did not contain an object: path=${sessionsJsonPath}`);
4650
+ return [];
4651
+ }
4652
+ const entries = [];
4653
+ for (const [sessionKey, value] of Object.entries(parsed)) {
4654
+ const normalizedSessionKey = sessionKey.trim();
4655
+ if (normalizedSessionKey.length === 0) {
4656
+ debugLog4(logger, "sessions-store-reader", `skipping blank session key in ${sessionsJsonPath}`);
4657
+ continue;
4658
+ }
4659
+ if (!isRecord2(value)) {
4660
+ debugLog4(logger, "sessions-store-reader", `skipping non-object entry for key=${normalizedSessionKey}`);
4661
+ continue;
4662
+ }
4663
+ const sessionId = asTrimmedString(value["sessionId"]);
4664
+ const sessionFile = asTrimmedString(value["sessionFile"]);
4665
+ const origin = isRecord2(value["origin"]) ? value["origin"] : void 0;
4666
+ const surface = asTrimmedString(origin?.["surface"]);
4667
+ const provider = asTrimmedString(origin?.["provider"]);
4668
+ const chatType = asTrimmedString(value["chatType"]);
4669
+ const updatedAt = asFiniteNumber(value["updatedAt"]);
4670
+ entries.push({
4671
+ sessionKey: normalizedSessionKey,
4672
+ ...sessionId ? { sessionId } : {},
4673
+ ...sessionFile ? { sessionFile: resolveSessionStorePath(sessionFile, resolvedSessionsDir) } : {},
4674
+ ...surface ? { surface } : {},
4675
+ ...provider ? { provider } : {},
4676
+ ...chatType ? { chatType } : {},
4677
+ ...updatedAt !== void 0 ? { updatedAt } : {}
4678
+ });
4679
+ }
4680
+ debugLog4(logger, "sessions-store-reader", `loaded sessions.json entries=${entries.length} path=${sessionsJsonPath}`);
4681
+ return entries;
4682
+ } catch (error) {
4683
+ if (isFileNotFound2(error)) {
4684
+ debugLog4(logger, "sessions-store-reader", `sessions.json missing at ${sessionsJsonPath}`);
4685
+ return [];
4686
+ }
4687
+ if (error instanceof SyntaxError) {
4688
+ debugLog4(logger, "sessions-store-reader", `sessions.json parse failed at ${sessionsJsonPath}: ${error.message}`);
4689
+ return [];
4690
+ }
4691
+ debugLog4(logger, "sessions-store-reader", `sessions.json read failed at ${sessionsJsonPath}: ${formatErrorMessage4(error)}`);
4692
+ return [];
4693
+ }
4694
+ }
4695
+ function resolveSessionStorePath(candidatePath, sessionsDir) {
4696
+ return path5.isAbsolute(candidatePath) ? path5.resolve(candidatePath) : path5.resolve(sessionsDir, candidatePath);
4697
+ }
4698
+ function isRecord2(value) {
4699
+ return typeof value === "object" && value !== null;
4700
+ }
4701
+ function asTrimmedString(value) {
4702
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
4703
+ }
4704
+ function asFiniteNumber(value) {
4705
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
4706
+ }
4707
+ function debugLog4(logger, subsystem, message) {
4708
+ logger?.debug?.(`[agenr] ${subsystem}: ${message}`);
4709
+ }
4710
+ function isFileNotFound2(error) {
4711
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
4712
+ }
4713
+ function formatErrorMessage4(error) {
4714
+ if (error instanceof Error) {
4715
+ return error.message;
4716
+ }
4717
+ return String(error);
4718
+ }
4719
+
4720
+ // ../../src/adapters/openclaw/session/tui-lane.ts
4721
+ var TUI_SESSION_KEY_PATTERN = /^agent:([^:]+):([^:]+)$/i;
4722
+ var TUI_UUID_LANE_PATTERN = /^tui[a-z0-9]*-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4723
+ var TUI_UUID_SUFFIX_PATTERN = /-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4724
+ function parseTuiSessionKey(sessionKey) {
4725
+ const normalizedSessionKey = sessionKey.trim();
4726
+ if (normalizedSessionKey.length === 0) {
4727
+ return null;
4728
+ }
4729
+ const match = TUI_SESSION_KEY_PATTERN.exec(normalizedSessionKey);
4730
+ if (!match) {
4731
+ return null;
4732
+ }
4733
+ const [, agentId, instanceLane] = match;
4734
+ const normalizedAgentId = agentId?.trim();
4735
+ const normalizedInstanceLane = instanceLane?.trim();
4736
+ if (!normalizedAgentId || !normalizedInstanceLane || !normalizedInstanceLane.toLowerCase().startsWith("tui")) {
4737
+ return null;
4738
+ }
4739
+ const stableLane = TUI_UUID_LANE_PATTERN.test(normalizedInstanceLane) ? normalizedInstanceLane.replace(TUI_UUID_SUFFIX_PATTERN, "") : normalizedInstanceLane;
4740
+ return {
4741
+ agentId: normalizedAgentId,
4742
+ stableLane,
4743
+ instanceLane: normalizedInstanceLane
4744
+ };
4745
+ }
4746
+
4747
+ // ../../src/adapters/openclaw/session/continuity/predecessor-resolver.ts
4748
+ async function resolveOpenClawSessionPredecessor(ctx, tracker, params) {
4749
+ const sessionContext = formatSessionContext2(ctx.sessionId, ctx.sessionKey);
4750
+ debugLog5(params.logger, "predecessor", `resolving predecessor for ${sessionContext}`);
4751
+ const tuiIdentity = parseTuiSessionKey(ctx.sessionKey ?? "");
4752
+ if (!tuiIdentity) {
4753
+ debugLog5(params.logger, "predecessor", `skipping TUI predecessor resolution for ${sessionContext}: current session key is not TUI`);
4754
+ return void 0;
4755
+ }
4756
+ const resumedFrom = tracker.getResumedFrom(ctx.sessionId);
4757
+ if (resumedFrom) {
4758
+ debugLog5(params.logger, "predecessor", `session_start resumedFrom for ${sessionContext}: ${resumedFrom}`);
4759
+ } else {
4760
+ debugLog5(params.logger, "predecessor", `session_start resumedFrom unavailable for ${sessionContext}`);
4761
+ }
4762
+ const sessionsDir = resolveOpenClawSessionsDirectory(ctx, tuiIdentity.agentId, params.resolveStateDir);
4763
+ if (!sessionsDir) {
4764
+ params.logger?.info?.(`[agenr] predecessor: TUI no predecessor found for ${sessionContext} reason=no_sessions_dir`);
4765
+ return void 0;
4766
+ }
4767
+ params.logger?.info?.(
4768
+ `[agenr] predecessor: TUI predecessor resolution for ${sessionContext} sessionKey=${ctx.sessionKey?.trim() ?? "unknown"} stableLane=${tuiIdentity.stableLane}`
4769
+ );
4770
+ debugLog5(
4771
+ params.logger,
4772
+ "predecessor",
4773
+ `TUI stable lane for ${sessionContext}: agentId=${tuiIdentity.agentId} instanceLane=${tuiIdentity.instanceLane} stableLane=${tuiIdentity.stableLane} sessionsDir=${sessionsDir}`
4774
+ );
4775
+ const predecessorResolution = await findTuiPredecessor(ctx.sessionKey ?? "", sessionsDir, resumedFrom, params.logger);
4776
+ if (!predecessorResolution.predecessor) {
4777
+ params.logger?.info?.(`[agenr] predecessor: TUI no predecessor found for ${sessionContext} reason=${predecessorResolution.reason}`);
4778
+ return void 0;
4779
+ }
4780
+ params.logger?.info?.(
4781
+ `[agenr] predecessor: TUI predecessor found for ${sessionContext} predecessorKey=${predecessorResolution.predecessor.sessionKey} predecessor=${predecessorResolution.predecessor.sessionFile}`
4782
+ );
4783
+ return {
4784
+ sessionFile: predecessorResolution.predecessor.sessionFile,
4785
+ sessionId: predecessorResolution.predecessor.sessionId
4786
+ };
4787
+ }
4788
+ async function findTuiPredecessor(currentSessionKey, sessionsDir, resumedFrom, logger) {
4789
+ const currentIdentity = parseTuiSessionKey(currentSessionKey);
4790
+ if (!currentIdentity) {
4791
+ return { reason: "not_tui_session_key" };
4792
+ }
4793
+ const entries = await readOpenClawSessionsStore(sessionsDir, logger);
4794
+ debugLog5(logger, "predecessor", `TUI sessions.json read result for sessionKey=${currentSessionKey}: entries=${entries.length}`);
4795
+ const sameAgentEntries = entries.filter((entry) => {
4796
+ const parsedCandidate = parseSingleLaneSessionKey(entry.sessionKey);
4797
+ if (!parsedCandidate) {
4798
+ debugLog5(logger, "predecessor", `TUI excluded candidate=${entry.sessionKey} reason=unsupported_session_key_shape`);
4799
+ return false;
4800
+ }
4801
+ if (parsedCandidate.agentId !== currentIdentity.agentId) {
4802
+ debugLog5(
4803
+ logger,
4804
+ "predecessor",
4805
+ `TUI excluded candidate=${entry.sessionKey} reason=agent_mismatch expected=${currentIdentity.agentId} actual=${parsedCandidate.agentId}`
4806
+ );
4807
+ return false;
4808
+ }
4809
+ return true;
4810
+ });
4811
+ debugLog5(logger, "predecessor", `TUI candidate filtering for sessionKey=${currentSessionKey}: sameAgentCount=${sameAgentEntries.length}`);
4812
+ if (resumedFrom) {
4813
+ const resumedFromMatch = sameAgentEntries.find((entry) => entry.sessionId === resumedFrom);
4814
+ if (resumedFromMatch) {
4815
+ if (!resumedFromMatch.sessionFile?.trim()) {
4816
+ debugLog5(
4817
+ logger,
4818
+ "predecessor",
4819
+ `TUI ignored session_start resumedFrom match for sessionKey=${currentSessionKey}: resumedFrom=${resumedFrom} reason=missing_session_file`
4820
+ );
4821
+ } else {
4822
+ const resolvedPredecessor = toResolvedTuiPredecessor(resumedFromMatch, logger);
4823
+ if (!resolvedPredecessor) {
4824
+ debugLog5(
4825
+ logger,
4826
+ "predecessor",
4827
+ `TUI ignored session_start resumedFrom match for sessionKey=${currentSessionKey}: resumedFrom=${resumedFrom} reason=missing_session_id`
4828
+ );
4829
+ } else {
4830
+ debugLog5(
4831
+ logger,
4832
+ "predecessor",
4833
+ `TUI matched session_start resumedFrom for sessionKey=${currentSessionKey}: resumedFrom=${resumedFrom} predecessorKey=${resumedFromMatch.sessionKey}`
4834
+ );
4835
+ return {
4836
+ reason: "resolved",
4837
+ predecessor: resolvedPredecessor
4838
+ };
4839
+ }
4840
+ }
4841
+ } else {
4842
+ debugLog5(logger, "predecessor", `TUI found no session_start resumedFrom match for sessionKey=${currentSessionKey}: resumedFrom=${resumedFrom}`);
4843
+ }
4844
+ }
4845
+ const laneMatches = sameAgentEntries.filter((entry) => {
4846
+ const normalizedCandidateKey = entry.sessionKey.trim();
4847
+ if (normalizedCandidateKey === currentSessionKey.trim()) {
4848
+ debugLog5(logger, "predecessor", `TUI excluded candidate=${entry.sessionKey} reason=current_session`);
4849
+ return false;
4850
+ }
4851
+ const candidateKey = parseSingleLaneSessionKey(entry.sessionKey);
4852
+ if (!candidateKey) {
4853
+ return false;
4854
+ }
4855
+ if (currentIdentity.stableLane === "tui" && candidateKey.lane === "main") {
4856
+ return true;
4857
+ }
4858
+ const candidateIdentity = parseTuiSessionKey(entry.sessionKey);
4859
+ if (!candidateIdentity) {
4860
+ debugLog5(logger, "predecessor", `TUI excluded candidate=${entry.sessionKey} reason=not_tui_candidate`);
4861
+ return false;
4862
+ }
4863
+ if (!isSameTuiLane(currentIdentity.stableLane, candidateIdentity.stableLane)) {
4864
+ debugLog5(
4865
+ logger,
4866
+ "predecessor",
4867
+ `TUI excluded candidate=${entry.sessionKey} reason=lane_mismatch currentStableLane=${currentIdentity.stableLane} candidateStableLane=${candidateIdentity.stableLane}`
4868
+ );
4869
+ return false;
4870
+ }
4871
+ return true;
4872
+ });
4873
+ debugLog5(logger, "predecessor", `TUI candidate filtering for sessionKey=${currentSessionKey}: laneMatchCount=${laneMatches.length}`);
4874
+ const sortedCandidates = laneMatches.filter((entry) => {
4875
+ if (!entry.sessionFile?.trim()) {
4876
+ debugLog5(logger, "predecessor", `TUI excluded candidate=${entry.sessionKey} reason=missing_session_file`);
4877
+ return false;
4878
+ }
4879
+ if (entry.updatedAt !== void 0) {
4880
+ return true;
4881
+ }
4882
+ debugLog5(logger, "predecessor", `TUI excluded candidate=${entry.sessionKey} reason=missing_updated_at`);
4883
+ return false;
4884
+ }).sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0));
4885
+ if (sortedCandidates.length === 0) {
4886
+ return { reason: "no_matching_sessions" };
4887
+ }
4888
+ for (const predecessor of sortedCandidates) {
4889
+ const resolvedPredecessor = toResolvedTuiPredecessor(predecessor, logger);
4890
+ if (!resolvedPredecessor) {
4891
+ debugLog5(
4892
+ logger,
4893
+ "predecessor",
4894
+ `TUI excluded candidate=${predecessor.sessionKey} reason=missing_session_id predecessor=${predecessor.sessionFile ?? "unknown"}`
4895
+ );
4896
+ continue;
4897
+ }
4898
+ return {
4899
+ reason: "resolved",
4900
+ predecessor: resolvedPredecessor
4901
+ };
4902
+ }
4903
+ return { reason: "missing_session_id" };
4904
+ }
4905
+ function resolvePredecessorSessionId(sessionId, sessionFile, logger) {
4906
+ const normalizedSessionId = sessionId?.trim();
4907
+ if (normalizedSessionId) {
4908
+ return normalizedSessionId;
3457
4909
  }
3458
- if (fromStart) {
3459
- const boundary = value.search(/\s/);
3460
- return boundary >= 0 ? value.slice(boundary).trimStart() : value.trim();
4910
+ if (!sessionFile?.trim()) {
4911
+ return void 0;
3461
4912
  }
3462
- const reversedBoundary = value.trimEnd().search(/\s\S*$/u);
3463
- return reversedBoundary >= 0 ? value.slice(0, reversedBoundary).trimEnd() : value.trim();
4913
+ return deriveOpenClawSessionIdFromFilePath(sessionFile, logger);
3464
4914
  }
3465
-
3466
- // ../../src/adapters/openclaw/hooks/before-prompt-build.ts
3467
- var CORE_ENTRY_LIMIT = 4;
3468
- var RECENT_SESSION_MESSAGE_LIMIT = 6;
3469
- var RECENT_SESSION_MAX_CHARS = 1800;
3470
- var READ_TIME_SUMMARY_TIMEOUT_MS = 2e4;
3471
- var READ_TIME_SUMMARY_TIMEOUT = /* @__PURE__ */ Symbol("read-time-summary-timeout");
3472
- async function handleAgenrBeforePromptBuild(_event, ctx, params) {
3473
- const sessionContext = formatSessionContext2(ctx.sessionId, ctx.sessionKey);
3474
- const trackerState = params.tracker.consume(ctx.sessionId, ctx.sessionKey);
3475
- if (!trackerState.isFirst) {
3476
- debugLog5(params.logger, "before_prompt_build", `session tracker duplicate blocked for ${sessionContext}`);
3477
- debugLog5(params.logger, "before_prompt_build", `session tracker active count=${trackerState.activeCount}`);
3478
- params.logger.info(`[agenr] session-start recall skipped (already ran) for ${sessionContext}`);
4915
+ function toResolvedTuiPredecessor(candidate, logger) {
4916
+ const sessionFile = candidate.sessionFile?.trim();
4917
+ if (!sessionFile) {
3479
4918
  return void 0;
3480
4919
  }
3481
- 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}`);
3484
- try {
3485
- const services = await params.servicesPromise;
3486
- const sessionStartRecall = await runAgenrSessionStartRecall(services);
3487
- const previousSessionContext = await buildPreviousSessionContext(ctx, params.tracker, services, params.logger);
3488
- const memoryContext = formatAgenrSessionStartRecall(sessionStartRecall);
3489
- const prependContext = [previousSessionContext, memoryContext].filter((value) => value.trim().length > 0).join("\n\n");
3490
- params.logger.info(`[agenr] session-start recall: ${sessionStartRecall.core.length} core entries for ${sessionContext}`);
3491
- debugLog5(params.logger, "before_prompt_build", `session-start core entries for ${sessionContext}: ${formatEntryRefs(sessionStartRecall.core)}`);
3492
- debugLog5(params.logger, "before_prompt_build", `session-start prependContext length for ${sessionContext}: ${prependContext.length} chars`);
3493
- if (prependContext.length === 0) {
3494
- params.logger.info(`[agenr] session-start recall: nothing to inject for ${sessionContext}`);
3495
- return void 0;
3496
- }
3497
- return { prependContext };
3498
- } catch (error) {
3499
- params.logger.warn(`[agenr] session-start recall failed for ${sessionContext}: ${formatErrorMessage4(error)}`);
4920
+ const sessionId = resolvePredecessorSessionId(candidate.sessionId, sessionFile, logger);
4921
+ if (!sessionId) {
3500
4922
  return void 0;
3501
4923
  }
4924
+ return {
4925
+ sessionFile,
4926
+ sessionId,
4927
+ sessionKey: candidate.sessionKey
4928
+ };
3502
4929
  }
3503
- async function runAgenrSessionStartRecall(services) {
3504
- const core = await listOpenClawCoreEntries(services.database, CORE_ENTRY_LIMIT);
4930
+ function resolveOpenClawSessionsDirectory(ctx, parsedAgentId, resolveStateDir) {
4931
+ const agentId = ctx.agentId?.trim() || parsedAgentId.trim();
4932
+ if (!agentId) {
4933
+ return void 0;
4934
+ }
4935
+ return path6.join(resolveStateDir(process.env), "agents", agentId, "sessions");
4936
+ }
4937
+ function parseSingleLaneSessionKey(sessionKey) {
4938
+ const match = /^agent:([^:]+):([^:]+)$/i.exec(sessionKey.trim());
4939
+ if (!match) {
4940
+ return null;
4941
+ }
4942
+ const [, agentId, lane] = match;
4943
+ const normalizedAgentId = agentId?.trim();
4944
+ const normalizedLane = lane?.trim();
4945
+ if (!normalizedAgentId || !normalizedLane) {
4946
+ return null;
4947
+ }
3505
4948
  return {
3506
- core
4949
+ agentId: normalizedAgentId,
4950
+ lane: normalizedLane
3507
4951
  };
3508
4952
  }
3509
- async function buildPreviousSessionContext(ctx, tracker, services, logger) {
3510
- const sessionContext = formatSessionContext2(ctx.sessionId, ctx.sessionKey);
4953
+ function isSameTuiLane(currentStableLane, candidateStableLane) {
4954
+ if (currentStableLane === "tui") {
4955
+ return candidateStableLane.toLowerCase().startsWith("tui");
4956
+ }
4957
+ return currentStableLane === candidateStableLane;
4958
+ }
4959
+ function debugLog5(logger, subsystem, message) {
4960
+ logger?.debug?.(`[agenr] ${subsystem}: ${message}`);
4961
+ }
4962
+ function formatSessionContext2(sessionId, sessionKey) {
4963
+ const normalizedSessionId = sessionId?.trim();
4964
+ const normalizedSessionKey = sessionKey?.trim();
4965
+ if (normalizedSessionId && normalizedSessionKey) {
4966
+ return `session=${normalizedSessionId} key=${normalizedSessionKey}`;
4967
+ }
4968
+ if (normalizedSessionId) {
4969
+ return `session=${normalizedSessionId}`;
4970
+ }
4971
+ if (normalizedSessionKey) {
4972
+ return `key=${normalizedSessionKey}`;
4973
+ }
4974
+ return "session=unknown";
4975
+ }
4976
+
4977
+ // ../../src/adapters/openclaw/session/continuity/recent-session.ts
4978
+ var RECENT_SESSION_MESSAGE_LIMIT = 6;
4979
+ var RECENT_SESSION_MAX_CHARS = 1800;
4980
+ async function renderRecentSessionSection(sessionFile, logger) {
4981
+ try {
4982
+ const transcript = await openClawTranscriptParser.parseFile(sessionFile);
4983
+ const tail = transcript.messages.slice(-RECENT_SESSION_MESSAGE_LIMIT);
4984
+ const body = capRecentSession(tail.map((message) => `${message.role === "user" ? "U" : "A"}: ${message.text}`).join("\n"), RECENT_SESSION_MAX_CHARS);
4985
+ logger.debug?.(`[agenr] before_prompt_build: recent session tail for file=${sessionFile}: messages=${tail.length} chars=${body.length}`);
4986
+ return body;
4987
+ } catch (error) {
4988
+ logger.debug?.(`[agenr] before_prompt_build: failed to build recent session tail for file=${sessionFile}: ${formatErrorMessage5(error)}`);
4989
+ return "";
4990
+ }
4991
+ }
4992
+ function formatErrorMessage5(error) {
4993
+ return error instanceof Error ? error.message : String(error);
4994
+ }
4995
+ function capRecentSession(value, maxChars) {
4996
+ if (value.length <= maxChars) {
4997
+ return value;
4998
+ }
4999
+ const marker = "[...truncated earlier recent session...]\n";
5000
+ return `${marker}${value.slice(-(maxChars - marker.length)).trimStart()}`;
5001
+ }
5002
+
5003
+ // ../../src/adapters/openclaw/session/continuity/index.ts
5004
+ var READ_TIME_CONTINUITY_SUMMARY_TIMEOUT_MS = 2e4;
5005
+ var READ_TIME_CONTINUITY_SUMMARY_TIMEOUT = /* @__PURE__ */ Symbol("read-time-continuity-summary-timeout");
5006
+ async function resolvePredecessorContinuity(ctx, tracker, services, logger) {
5007
+ const sessionContext = formatSessionContext(ctx.sessionId, ctx.sessionKey);
3511
5008
  const predecessor = await resolveOpenClawSessionPredecessor(ctx, tracker, {
3512
5009
  logger,
3513
5010
  resolveStateDir: services.openClaw.runtime.state.resolveStateDir
3514
5011
  });
3515
5012
  if (!predecessor) {
3516
- logger.info(`[agenr] session-start predecessor summary not found for ${sessionContext} reason=no_predecessor`);
3517
- return "";
3518
- }
3519
- 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
- );
3735
- } catch (error) {
3736
- params.logger.info(`[agenr] before_reset: summary generation failed for ${sessionContext} reason=${formatErrorMessage5(error)} model=unknown`);
3737
- debugLog6(params.logger, "before_reset", `unexpected failure for ${sessionContext}: ${formatErrorMessage5(error)}`);
3738
- }
3739
- }
3740
- function debugLog6(logger, subsystem, message) {
3741
- logger.debug?.(`[agenr] ${subsystem}: ${message}`);
3742
- }
3743
- function formatErrorMessage5(error) {
3744
- return error instanceof Error ? error.message : String(error);
3745
- }
3746
- function formatSessionContext3(sessionId, sessionKey) {
3747
- const normalizedSessionId = sessionId?.trim();
3748
- const normalizedSessionKey = sessionKey?.trim();
3749
- if (normalizedSessionId && normalizedSessionKey) {
3750
- return `session=${normalizedSessionId} key=${normalizedSessionKey}`;
3751
- }
3752
- if (normalizedSessionId) {
3753
- return `session=${normalizedSessionId}`;
3754
- }
3755
- if (normalizedSessionKey) {
3756
- return `key=${normalizedSessionKey}`;
5171
+ return { prependContext };
5172
+ } catch (error) {
5173
+ params.logger.warn(`[agenr] session-start recall failed for ${sessionContext}: ${formatErrorMessage2(error)}`);
5174
+ return void 0;
3757
5175
  }
3758
- return "session=unknown";
5176
+ }
5177
+ async function runAgenrSessionStartRecall(services) {
5178
+ return { core: await listOpenClawCoreEntries(services.database, CORE_ENTRY_LIMIT) };
5179
+ }
5180
+ function formatEntryRefs(entries) {
5181
+ return entries.length === 0 ? "none" : entries.map((entry) => `${entry.subject} [${entry.id}]`).join(", ");
5182
+ }
5183
+ function isInternalAgenrSession(sessionKey) {
5184
+ return Boolean(sessionKey?.startsWith(INTERNAL_AGENR_SESSION_PREFIX));
3759
5185
  }
3760
5186
 
3761
5187
  // ../../src/adapters/openclaw/memory/flush-plan.ts
@@ -3946,8 +5372,9 @@ async function sleep(durationMs) {
3946
5372
  }
3947
5373
 
3948
5374
  // ../../src/adapters/db/schema.ts
3949
- var SCHEMA_VERSION = "2";
5375
+ var SCHEMA_VERSION = "4";
3950
5376
  var VECTOR_INDEX_NAME = "idx_entries_embedding";
5377
+ var EPISODE_VECTOR_INDEX_NAME = "idx_episodes_embedding";
3951
5378
  var BULK_WRITE_STATE_META_KEY = "bulk_write_state";
3952
5379
  var CREATE_ENTRIES_TABLE_SQL = `
3953
5380
  CREATE TABLE IF NOT EXISTS entries (
@@ -3969,6 +5396,8 @@ var CREATE_ENTRIES_TABLE_SQL = `
3969
5396
  last_recalled_at TEXT,
3970
5397
  superseded_by TEXT REFERENCES entries(id),
3971
5398
  cluster_id TEXT,
5399
+ user_id TEXT,
5400
+ project TEXT,
3972
5401
  retired INTEGER NOT NULL DEFAULT 0,
3973
5402
  retired_at TEXT,
3974
5403
  retired_reason TEXT,
@@ -4017,6 +5446,51 @@ var CREATE_INGEST_LOG_TABLE_SQL = `
4017
5446
  entry_count INTEGER DEFAULT 0
4018
5447
  )
4019
5448
  `;
5449
+ var CREATE_EPISODES_TABLE_SQL = `
5450
+ CREATE TABLE IF NOT EXISTS episodes (
5451
+ id TEXT PRIMARY KEY,
5452
+ source TEXT NOT NULL,
5453
+ source_id TEXT,
5454
+ source_ref TEXT,
5455
+ transcript_hash TEXT,
5456
+ summary_hash TEXT,
5457
+ agent_id TEXT,
5458
+ surface TEXT,
5459
+ started_at TEXT NOT NULL,
5460
+ ended_at TEXT,
5461
+ summary TEXT NOT NULL,
5462
+ tags TEXT,
5463
+ activity_level TEXT,
5464
+ user_id TEXT,
5465
+ project TEXT,
5466
+ gen_model TEXT,
5467
+ gen_version TEXT,
5468
+ message_count INTEGER,
5469
+ embedding F32_BLOB(1024),
5470
+ retired INTEGER NOT NULL DEFAULT 0,
5471
+ retired_at TEXT,
5472
+ retired_reason TEXT,
5473
+ superseded_by TEXT REFERENCES episodes(id),
5474
+ created_at TEXT NOT NULL,
5475
+ updated_at TEXT NOT NULL
5476
+ )
5477
+ `;
5478
+ var CREATE_TASKS_TABLE_SQL = `
5479
+ CREATE TABLE IF NOT EXISTS tasks (
5480
+ id TEXT PRIMARY KEY,
5481
+ subject TEXT NOT NULL,
5482
+ content TEXT NOT NULL,
5483
+ status TEXT NOT NULL DEFAULT 'open',
5484
+ priority INTEGER NOT NULL DEFAULT 5,
5485
+ tags TEXT,
5486
+ source_context TEXT,
5487
+ project TEXT,
5488
+ created_at TEXT NOT NULL,
5489
+ updated_at TEXT NOT NULL,
5490
+ completed_at TEXT,
5491
+ due_at TEXT
5492
+ )
5493
+ `;
4020
5494
  var CREATE_RECALL_EVENTS_TABLE_SQL = `
4021
5495
  CREATE TABLE IF NOT EXISTS recall_events (
4022
5496
  id TEXT PRIMARY KEY,
@@ -4102,6 +5576,43 @@ var CREATE_ENTRIES_CREATED_AT_INDEX_SQL = `
4102
5576
  CREATE INDEX IF NOT EXISTS idx_entries_created_at
4103
5577
  ON entries(created_at)
4104
5578
  `;
5579
+ var CREATE_EPISODES_STARTED_AT_INDEX_SQL = `
5580
+ CREATE INDEX IF NOT EXISTS idx_episodes_started_at
5581
+ ON episodes(started_at)
5582
+ `;
5583
+ var CREATE_EPISODES_ENDED_AT_INDEX_SQL = `
5584
+ CREATE INDEX IF NOT EXISTS idx_episodes_ended_at
5585
+ ON episodes(ended_at)
5586
+ `;
5587
+ var CREATE_EPISODES_SOURCE_INDEX_SQL = `
5588
+ CREATE INDEX IF NOT EXISTS idx_episodes_source
5589
+ ON episodes(source)
5590
+ `;
5591
+ var CREATE_EPISODES_SOURCE_ID_INDEX_SQL = `
5592
+ CREATE INDEX IF NOT EXISTS idx_episodes_source_id
5593
+ ON episodes(source_id)
5594
+ `;
5595
+ var CREATE_EPISODES_RETIRED_INDEX_SQL = `
5596
+ CREATE INDEX IF NOT EXISTS idx_episodes_retired
5597
+ ON episodes(retired)
5598
+ `;
5599
+ var CREATE_EPISODES_SOURCE_SOURCE_ID_UNIQUE_INDEX_SQL = `
5600
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_episodes_source_source_id
5601
+ ON episodes(source, source_id)
5602
+ WHERE source_id IS NOT NULL
5603
+ `;
5604
+ var CREATE_TASKS_STATUS_INDEX_SQL = `
5605
+ CREATE INDEX IF NOT EXISTS idx_tasks_status
5606
+ ON tasks(status)
5607
+ `;
5608
+ var CREATE_TASKS_CREATED_AT_INDEX_SQL = `
5609
+ CREATE INDEX IF NOT EXISTS idx_tasks_created_at
5610
+ ON tasks(created_at)
5611
+ `;
5612
+ var CREATE_TASKS_PROJECT_INDEX_SQL = `
5613
+ CREATE INDEX IF NOT EXISTS idx_tasks_project
5614
+ ON tasks(project)
5615
+ `;
4105
5616
  var CREATE_RECALL_EVENTS_ENTRY_ID_INDEX_SQL = `
4106
5617
  CREATE INDEX IF NOT EXISTS idx_recall_events_entry_id
4107
5618
  ON recall_events(entry_id)
@@ -4123,6 +5634,19 @@ var CREATE_ENTRIES_EMBEDDING_INDEX_SQL = `
4123
5634
  AND retired = 0
4124
5635
  AND superseded_by IS NULL
4125
5636
  `;
5637
+ var CREATE_EPISODES_EMBEDDING_INDEX_SQL = `
5638
+ CREATE INDEX IF NOT EXISTS idx_episodes_embedding ON episodes (
5639
+ libsql_vector_idx(
5640
+ embedding,
5641
+ 'metric=cosine',
5642
+ 'compress_neighbors=float8',
5643
+ 'max_neighbors=50'
5644
+ )
5645
+ )
5646
+ WHERE embedding IS NOT NULL
5647
+ AND retired = 0
5648
+ AND superseded_by IS NULL
5649
+ `;
4126
5650
  var SCHEMA_STATEMENTS = [
4127
5651
  CREATE_ENTRIES_TABLE_SQL,
4128
5652
  CREATE_ENTRIES_FTS_TABLE_SQL,
@@ -4130,6 +5654,8 @@ var SCHEMA_STATEMENTS = [
4130
5654
  CREATE_ENTRIES_FTS_DELETE_TRIGGER_SQL,
4131
5655
  CREATE_ENTRIES_FTS_UPDATE_TRIGGER_SQL,
4132
5656
  CREATE_INGEST_LOG_TABLE_SQL,
5657
+ CREATE_EPISODES_TABLE_SQL,
5658
+ CREATE_TASKS_TABLE_SQL,
4133
5659
  CREATE_RECALL_EVENTS_TABLE_SQL,
4134
5660
  CREATE_SURGEON_RUNS_TABLE_SQL,
4135
5661
  CREATE_META_TABLE_SQL,
@@ -4139,6 +5665,15 @@ var SCHEMA_STATEMENTS = [
4139
5665
  CREATE_ENTRIES_EXPIRY_INDEX_SQL,
4140
5666
  CREATE_ENTRIES_RETIRED_INDEX_SQL,
4141
5667
  CREATE_ENTRIES_CREATED_AT_INDEX_SQL,
5668
+ CREATE_EPISODES_STARTED_AT_INDEX_SQL,
5669
+ CREATE_EPISODES_ENDED_AT_INDEX_SQL,
5670
+ CREATE_EPISODES_SOURCE_INDEX_SQL,
5671
+ CREATE_EPISODES_SOURCE_ID_INDEX_SQL,
5672
+ CREATE_EPISODES_RETIRED_INDEX_SQL,
5673
+ CREATE_EPISODES_SOURCE_SOURCE_ID_UNIQUE_INDEX_SQL,
5674
+ CREATE_TASKS_STATUS_INDEX_SQL,
5675
+ CREATE_TASKS_CREATED_AT_INDEX_SQL,
5676
+ CREATE_TASKS_PROJECT_INDEX_SQL,
4142
5677
  CREATE_RECALL_EVENTS_ENTRY_ID_INDEX_SQL,
4143
5678
  CREATE_RECALL_EVENTS_RECALLED_AT_INDEX_SQL
4144
5679
  ];
@@ -4150,6 +5685,12 @@ async function initSchema(db) {
4150
5685
  await db.execute(statement);
4151
5686
  }
4152
5687
  await ensureSurgeonSchema(db);
5688
+ if (currentVersion === "2") {
5689
+ await migrateSchemaV2ToV3(db);
5690
+ }
5691
+ if (currentVersion === "3") {
5692
+ await migrateSchemaV3ToV4(db);
5693
+ }
4153
5694
  await db.execute({
4154
5695
  sql: `
4155
5696
  INSERT INTO _meta (key, value)
@@ -4165,7 +5706,7 @@ async function initSchema(db) {
4165
5706
  if (currentVersion !== SCHEMA_VERSION || !hadEntriesFts) {
4166
5707
  await rebuildFts(db);
4167
5708
  }
4168
- await ensureVectorIndex(db);
5709
+ await ensureVectorIndexes(db);
4169
5710
  }
4170
5711
  async function ensureSurgeonSchema(db) {
4171
5712
  const columns = await db.execute("PRAGMA table_info('surgeon_runs')");
@@ -4206,6 +5747,44 @@ async function ensureSurgeonSchema(db) {
4206
5747
  await db.execute(CREATE_SURGEON_RUN_ACTIONS_ENTRY_ID_INDEX_SQL);
4207
5748
  await db.execute(CREATE_SURGEON_RUN_ACTIONS_CREATED_AT_INDEX_SQL);
4208
5749
  }
5750
+ async function migrateSchemaV2ToV3(db) {
5751
+ await runImmediateTransaction(db, async () => {
5752
+ await db.execute("DROP TRIGGER IF EXISTS entries_ai");
5753
+ await db.execute("DROP TRIGGER IF EXISTS entries_ad");
5754
+ await db.execute("DROP TRIGGER IF EXISTS entries_au");
5755
+ await db.execute("UPDATE entries SET type = 'milestone' WHERE type = 'event'");
5756
+ await db.execute(`
5757
+ INSERT INTO tasks (id, subject, content, status, priority, tags, source_context, created_at, updated_at)
5758
+ SELECT id, subject, content, 'open', importance, tags, source_context, created_at, updated_at
5759
+ FROM entries
5760
+ WHERE type = 'todo' AND retired = 0
5761
+ `);
5762
+ await db.execute("DELETE FROM entries WHERE type = 'todo'");
5763
+ await db.execute("DELETE FROM entries WHERE type = 'reflection'");
5764
+ if (!await columnExists(db, "entries", "user_id")) {
5765
+ await db.execute("ALTER TABLE entries ADD COLUMN user_id TEXT");
5766
+ }
5767
+ if (!await columnExists(db, "entries", "project")) {
5768
+ await db.execute("ALTER TABLE entries ADD COLUMN project TEXT");
5769
+ }
5770
+ await db.execute(CREATE_ENTRIES_FTS_INSERT_TRIGGER_SQL);
5771
+ await db.execute(CREATE_ENTRIES_FTS_DELETE_TRIGGER_SQL);
5772
+ await db.execute(CREATE_ENTRIES_FTS_UPDATE_TRIGGER_SQL);
5773
+ });
5774
+ }
5775
+ async function migrateSchemaV3ToV4(db) {
5776
+ for (const statement of [
5777
+ CREATE_EPISODES_TABLE_SQL,
5778
+ CREATE_EPISODES_STARTED_AT_INDEX_SQL,
5779
+ CREATE_EPISODES_ENDED_AT_INDEX_SQL,
5780
+ CREATE_EPISODES_SOURCE_INDEX_SQL,
5781
+ CREATE_EPISODES_SOURCE_ID_INDEX_SQL,
5782
+ CREATE_EPISODES_RETIRED_INDEX_SQL,
5783
+ CREATE_EPISODES_SOURCE_SOURCE_ID_UNIQUE_INDEX_SQL
5784
+ ]) {
5785
+ await db.execute(statement);
5786
+ }
5787
+ }
4209
5788
  async function rebuildFts(db) {
4210
5789
  await db.execute("INSERT INTO entries_fts(entries_fts) VALUES ('rebuild')");
4211
5790
  }
@@ -4222,7 +5801,7 @@ async function prepareBulkWrites(db) {
4222
5801
  await db.execute("DROP TRIGGER IF EXISTS entries_ai");
4223
5802
  await db.execute("DROP TRIGGER IF EXISTS entries_ad");
4224
5803
  await db.execute("DROP TRIGGER IF EXISTS entries_au");
4225
- await dropVectorIndex(db);
5804
+ await dropVectorIndexes(db);
4226
5805
  });
4227
5806
  }
4228
5807
  async function finalizeBulkWrites(db) {
@@ -4231,7 +5810,7 @@ async function finalizeBulkWrites(db) {
4231
5810
  await db.execute(CREATE_ENTRIES_FTS_DELETE_TRIGGER_SQL);
4232
5811
  await db.execute(CREATE_ENTRIES_FTS_UPDATE_TRIGGER_SQL);
4233
5812
  await rebuildFts(db);
4234
- await ensureVectorIndex(db);
5813
+ await ensureVectorIndexes(db);
4235
5814
  await db.execute({
4236
5815
  sql: "DELETE FROM _meta WHERE key = ?",
4237
5816
  args: [BULK_WRITE_STATE_META_KEY]
@@ -4264,6 +5843,13 @@ async function tableExists(db, tableName) {
4264
5843
  });
4265
5844
  return result.rows.length > 0;
4266
5845
  }
5846
+ async function columnExists(db, tableName, columnName) {
5847
+ const result = await db.execute(`PRAGMA table_info('${tableName.replaceAll("'", "''")}')`);
5848
+ return result.rows.some((row) => {
5849
+ const name = row.name;
5850
+ return name === columnName;
5851
+ });
5852
+ }
4267
5853
  async function hasActiveBulkWriteState(db) {
4268
5854
  try {
4269
5855
  const result = await db.execute({
@@ -4276,18 +5862,20 @@ async function hasActiveBulkWriteState(db) {
4276
5862
  return false;
4277
5863
  }
4278
5864
  }
4279
- async function ensureVectorIndex(db) {
5865
+ async function ensureVectorIndexes(db) {
4280
5866
  try {
4281
5867
  await db.execute(CREATE_ENTRIES_EMBEDDING_INDEX_SQL);
5868
+ await db.execute(CREATE_EPISODES_EMBEDDING_INDEX_SQL);
4282
5869
  } catch (error) {
4283
5870
  if (!isVectorUnavailableError(error)) {
4284
5871
  throw error;
4285
5872
  }
4286
5873
  }
4287
5874
  }
4288
- async function dropVectorIndex(db) {
5875
+ async function dropVectorIndexes(db) {
4289
5876
  try {
4290
5877
  await db.execute(`DROP INDEX IF EXISTS ${VECTOR_INDEX_NAME}`);
5878
+ await db.execute(`DROP INDEX IF EXISTS ${EPISODE_VECTOR_INDEX_NAME}`);
4291
5879
  } catch (error) {
4292
5880
  if (!isVectorUnavailableError(error)) {
4293
5881
  throw error;
@@ -4383,9 +5971,9 @@ async function probeVectorAvailability(services) {
4383
5971
  }
4384
5972
 
4385
5973
  // ../../src/config.ts
4386
- import fs6 from "fs";
4387
- import os2 from "os";
4388
- import path5 from "path";
5974
+ import fs7 from "fs";
5975
+ import os3 from "os";
5976
+ import path7 from "path";
4389
5977
  import { fileURLToPath } from "url";
4390
5978
  var AUTH_METHOD_DEFINITIONS = [
4391
5979
  {
@@ -4425,7 +6013,7 @@ var AUTH_METHOD_DEFINITIONS = [
4425
6013
  }
4426
6014
  ];
4427
6015
  var AUTH_METHOD_SET = new Set(AUTH_METHOD_DEFINITIONS.map((definition) => definition.id));
4428
- var DEFAULT_CONFIG_DIR = path5.join(os2.homedir(), ".agenr");
6016
+ var DEFAULT_CONFIG_DIR = path7.join(os3.homedir(), ".agenr");
4429
6017
  var DEFAULT_DB_NAME = "knowledge.db";
4430
6018
  function resolveConfigDir() {
4431
6019
  return process.env.AGENR_CONFIG_DIR ?? DEFAULT_CONFIG_DIR;
@@ -4443,18 +6031,18 @@ function resolveConfigPath(options = {}) {
4443
6031
  if (adjacentConfigPath) {
4444
6032
  return adjacentConfigPath;
4445
6033
  }
4446
- return path5.join(resolveConfigDir(), "config.json");
6034
+ return path7.join(resolveConfigDir(), "config.json");
4447
6035
  }
4448
6036
  function resolveDbPath(config) {
4449
- 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);
4450
6038
  }
4451
6039
  function readConfig(options = {}) {
4452
6040
  const configPath = resolveFilesystemPath(resolveConfigPath(options));
4453
- if (!fs6.existsSync(configPath)) {
6041
+ if (!fs7.existsSync(configPath)) {
4454
6042
  return {};
4455
6043
  }
4456
6044
  try {
4457
- const raw = fs6.readFileSync(configPath, "utf-8");
6045
+ const raw = fs7.readFileSync(configPath, "utf-8");
4458
6046
  return JSON.parse(raw);
4459
6047
  } catch {
4460
6048
  return {};
@@ -4467,12 +6055,12 @@ function resolveAdjacentConfigPath(dbPath) {
4467
6055
  }
4468
6056
  if (normalizedDbPath.startsWith("file:")) {
4469
6057
  try {
4470
- return path5.join(path5.dirname(fileURLToPath(normalizedDbPath)), "config.json");
6058
+ return path7.join(path7.dirname(fileURLToPath(normalizedDbPath)), "config.json");
4471
6059
  } catch {
4472
6060
  return void 0;
4473
6061
  }
4474
6062
  }
4475
- return path5.join(path5.dirname(normalizedDbPath), "config.json");
6063
+ return path7.join(path7.dirname(normalizedDbPath), "config.json");
4476
6064
  }
4477
6065
  function normalizeOptionalString2(value) {
4478
6066
  const normalized = value?.trim();
@@ -4520,6 +6108,8 @@ async function insertEntry(executor, entry, embedding, contentHash) {
4520
6108
  last_recalled_at,
4521
6109
  superseded_by,
4522
6110
  cluster_id,
6111
+ user_id,
6112
+ project,
4523
6113
  retired,
4524
6114
  retired_at,
4525
6115
  retired_reason,
@@ -4529,7 +6119,7 @@ async function insertEntry(executor, entry, embedding, contentHash) {
4529
6119
  VALUES (
4530
6120
  ?, ?, ?, ?, ?, ?, ?, ?, ?,
4531
6121
  CASE WHEN ? IS NULL THEN NULL ELSE vector32(?) END,
4532
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
6122
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
4533
6123
  )
4534
6124
  `,
4535
6125
  args: [
@@ -4552,6 +6142,8 @@ async function insertEntry(executor, entry, embedding, contentHash) {
4552
6142
  normalizeOptionalString3(entry.last_recalled_at),
4553
6143
  normalizeOptionalString3(entry.superseded_by),
4554
6144
  normalizeOptionalString3(entry.cluster_id),
6145
+ normalizeOptionalString3(entry.user_id),
6146
+ normalizeOptionalString3(entry.project),
4555
6147
  entry.retired ? 1 : 0,
4556
6148
  normalizeOptionalString3(entry.retired_at),
4557
6149
  normalizeOptionalString3(entry.retired_reason),
@@ -4589,6 +6181,8 @@ async function getEntries(executor, ids) {
4589
6181
  last_recalled_at,
4590
6182
  superseded_by,
4591
6183
  cluster_id,
6184
+ user_id,
6185
+ project,
4592
6186
  retired,
4593
6187
  retired_at,
4594
6188
  retired_reason,
@@ -4815,6 +6409,8 @@ var ENTRY_SELECT_COLUMNS2 = `
4815
6409
  e.last_recalled_at,
4816
6410
  e.superseded_by,
4817
6411
  e.cluster_id,
6412
+ e.user_id,
6413
+ e.project,
4818
6414
  e.retired,
4819
6415
  e.retired_at,
4820
6416
  e.retired_reason,
@@ -5043,9 +6639,470 @@ function wrapVectorError(error) {
5043
6639
  }
5044
6640
 
5045
6641
  // ../../src/adapters/db/client.ts
5046
- import fs7 from "fs/promises";
5047
- import path6 from "path";
6642
+ import fs8 from "fs/promises";
6643
+ import path8 from "path";
5048
6644
  import { createClient } from "@libsql/client";
6645
+
6646
+ // ../../src/adapters/db/episode-queries.ts
6647
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
6648
+ var EPISODE_SELECT_COLUMNS = `
6649
+ id,
6650
+ source,
6651
+ source_id,
6652
+ source_ref,
6653
+ transcript_hash,
6654
+ summary_hash,
6655
+ agent_id,
6656
+ surface,
6657
+ started_at,
6658
+ ended_at,
6659
+ summary,
6660
+ tags,
6661
+ activity_level,
6662
+ user_id,
6663
+ project,
6664
+ gen_model,
6665
+ gen_version,
6666
+ message_count,
6667
+ embedding,
6668
+ retired,
6669
+ retired_at,
6670
+ retired_reason,
6671
+ superseded_by,
6672
+ created_at,
6673
+ updated_at
6674
+ `;
6675
+ async function getEpisodeBySourceId(executor, source, sourceId) {
6676
+ const normalizedSourceId = normalizeOptionalString4(sourceId);
6677
+ if (!normalizedSourceId) {
6678
+ return null;
6679
+ }
6680
+ const result = await executor.execute({
6681
+ sql: `
6682
+ SELECT ${EPISODE_SELECT_COLUMNS}
6683
+ FROM episodes
6684
+ WHERE source = ?
6685
+ AND source_id = ?
6686
+ LIMIT 1
6687
+ `,
6688
+ args: [source, normalizedSourceId]
6689
+ });
6690
+ const row = result.rows[0];
6691
+ return row ? mapEpisodeRow(row) : null;
6692
+ }
6693
+ async function getEpisodeByTranscriptHash(executor, source, transcriptHash) {
6694
+ const normalizedTranscriptHash = normalizeOptionalString4(transcriptHash);
6695
+ if (!normalizedTranscriptHash) {
6696
+ return null;
6697
+ }
6698
+ const result = await executor.execute({
6699
+ sql: `
6700
+ SELECT ${EPISODE_SELECT_COLUMNS}
6701
+ FROM episodes
6702
+ WHERE source = ?
6703
+ AND transcript_hash = ?
6704
+ LIMIT 1
6705
+ `,
6706
+ args: [source, normalizedTranscriptHash]
6707
+ });
6708
+ const row = result.rows[0];
6709
+ return row ? mapEpisodeRow(row) : null;
6710
+ }
6711
+ async function upsertEpisode(executor, input) {
6712
+ const payload = normalizeEpisodePayload(input);
6713
+ const summaryHash = createEpisodePayloadHash(payload);
6714
+ const existing = payload.sourceId !== void 0 ? await getEpisodeBySourceId(executor, payload.source, payload.sourceId) : await getEpisodeByTranscriptHash(executor, payload.source, readRequiredIdentityHash(payload));
6715
+ if (!existing) {
6716
+ const id = randomUUID3();
6717
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
6718
+ const vectorJson2 = serializeEmbeddingForVector(payload.embedding ?? []);
6719
+ await executor.execute({
6720
+ sql: `
6721
+ INSERT INTO episodes (
6722
+ id,
6723
+ source,
6724
+ source_id,
6725
+ source_ref,
6726
+ transcript_hash,
6727
+ summary_hash,
6728
+ agent_id,
6729
+ surface,
6730
+ started_at,
6731
+ ended_at,
6732
+ summary,
6733
+ tags,
6734
+ activity_level,
6735
+ user_id,
6736
+ project,
6737
+ gen_model,
6738
+ gen_version,
6739
+ message_count,
6740
+ embedding,
6741
+ retired,
6742
+ retired_at,
6743
+ retired_reason,
6744
+ superseded_by,
6745
+ created_at,
6746
+ updated_at
6747
+ )
6748
+ VALUES (
6749
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
6750
+ CASE WHEN ? IS NULL THEN NULL ELSE vector32(?) END,
6751
+ 0, NULL, NULL, NULL, ?, ?
6752
+ )
6753
+ `,
6754
+ args: [
6755
+ id,
6756
+ payload.source,
6757
+ toNullableString(payload.sourceId),
6758
+ toNullableString(payload.sourceRef),
6759
+ toNullableString(payload.transcriptHash),
6760
+ summaryHash,
6761
+ toNullableString(payload.agentId),
6762
+ toNullableString(payload.surface),
6763
+ payload.startedAt,
6764
+ toNullableString(payload.endedAt),
6765
+ payload.summary,
6766
+ serializeTags(payload.tags),
6767
+ toNullableString(payload.activityLevel),
6768
+ toNullableString(payload.userId),
6769
+ toNullableString(payload.project),
6770
+ toNullableString(payload.genModel),
6771
+ toNullableString(payload.genVersion),
6772
+ toNullableInteger(payload.messageCount),
6773
+ vectorJson2,
6774
+ vectorJson2,
6775
+ now2,
6776
+ now2
6777
+ ]
6778
+ });
6779
+ return {
6780
+ episode: await getEpisodeById(executor, id),
6781
+ action: "inserted"
6782
+ };
6783
+ }
6784
+ const existingSummaryHash = existing.summaryHash ?? createEpisodePayloadHash(normalizeEpisodePayload(fromStoredEpisode(existing)));
6785
+ if (existingSummaryHash === summaryHash) {
6786
+ return {
6787
+ episode: existing,
6788
+ action: "unchanged"
6789
+ };
6790
+ }
6791
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6792
+ const vectorJson = serializeEmbeddingForVector(payload.embedding ?? []);
6793
+ await executor.execute({
6794
+ sql: `
6795
+ UPDATE episodes
6796
+ SET source_ref = ?,
6797
+ transcript_hash = ?,
6798
+ summary_hash = ?,
6799
+ agent_id = ?,
6800
+ surface = ?,
6801
+ started_at = ?,
6802
+ ended_at = ?,
6803
+ summary = ?,
6804
+ tags = ?,
6805
+ activity_level = ?,
6806
+ user_id = ?,
6807
+ project = ?,
6808
+ gen_model = ?,
6809
+ gen_version = ?,
6810
+ message_count = ?,
6811
+ embedding = CASE WHEN ? IS NULL THEN NULL ELSE vector32(?) END,
6812
+ updated_at = ?
6813
+ WHERE id = ?
6814
+ `,
6815
+ args: [
6816
+ toNullableString(payload.sourceRef),
6817
+ toNullableString(payload.transcriptHash),
6818
+ summaryHash,
6819
+ toNullableString(payload.agentId),
6820
+ toNullableString(payload.surface),
6821
+ payload.startedAt,
6822
+ toNullableString(payload.endedAt),
6823
+ payload.summary,
6824
+ serializeTags(payload.tags),
6825
+ toNullableString(payload.activityLevel),
6826
+ toNullableString(payload.userId),
6827
+ toNullableString(payload.project),
6828
+ toNullableString(payload.genModel),
6829
+ toNullableString(payload.genVersion),
6830
+ toNullableInteger(payload.messageCount),
6831
+ vectorJson,
6832
+ vectorJson,
6833
+ now,
6834
+ existing.id
6835
+ ]
6836
+ });
6837
+ return {
6838
+ episode: await getEpisodeById(executor, existing.id),
6839
+ action: "updated"
6840
+ };
6841
+ }
6842
+ async function listEpisodesByTimeWindow(executor, window, limit) {
6843
+ const bounds = resolveWindowBounds(window);
6844
+ if (!bounds) {
6845
+ return [];
6846
+ }
6847
+ const whereClauses = [buildActiveEpisodeClause()];
6848
+ const args = [];
6849
+ if (bounds.start && bounds.end) {
6850
+ whereClauses.push("started_at <= ?");
6851
+ whereClauses.push("COALESCE(ended_at, started_at) >= ?");
6852
+ args.push(bounds.end, bounds.start);
6853
+ } else if (bounds.start) {
6854
+ whereClauses.push("COALESCE(ended_at, started_at) >= ?");
6855
+ args.push(bounds.start);
6856
+ } else if (bounds.end) {
6857
+ whereClauses.push("started_at <= ?");
6858
+ args.push(bounds.end);
6859
+ } else {
6860
+ return [];
6861
+ }
6862
+ const normalizedLimit = normalizePositiveInteger(limit);
6863
+ const result = await executor.execute({
6864
+ sql: `
6865
+ SELECT ${EPISODE_SELECT_COLUMNS}
6866
+ FROM episodes
6867
+ WHERE ${whereClauses.join(" AND ")}
6868
+ ORDER BY started_at DESC, id ASC
6869
+ ${normalizedLimit ? "LIMIT ?" : ""}
6870
+ `,
6871
+ args: normalizedLimit ? [...args, normalizedLimit] : args
6872
+ });
6873
+ return result.rows.map((row) => mapEpisodeRow(row));
6874
+ }
6875
+ async function episodeVectorSearch(executor, params) {
6876
+ if (params.limit <= 0 || params.embedding.length === 0) {
6877
+ return [];
6878
+ }
6879
+ const serializedEmbedding = serializeEmbeddingForVector(params.embedding);
6880
+ if (!serializedEmbedding) {
6881
+ return [];
6882
+ }
6883
+ let result;
6884
+ try {
6885
+ result = await executor.execute({
6886
+ sql: `
6887
+ SELECT ${prefixColumns(EPISODE_SELECT_COLUMNS, "e")}
6888
+ FROM vector_top_k('idx_episodes_embedding', vector32(?), ?) AS v
6889
+ JOIN episodes AS e ON e.rowid = v.id
6890
+ WHERE ${buildActiveEpisodeClause("e")}
6891
+ LIMIT ?
6892
+ `,
6893
+ args: [serializedEmbedding, params.limit, params.limit]
6894
+ });
6895
+ } catch (error) {
6896
+ throw wrapEpisodeVectorError(error);
6897
+ }
6898
+ return result.rows.map((row) => {
6899
+ const episode = mapEpisodeRow(row);
6900
+ return {
6901
+ episode,
6902
+ vectorSim: cosineSimilarity(params.embedding, episode.embedding ?? [])
6903
+ };
6904
+ }).filter((candidate) => candidate.vectorSim > 0).sort((left, right) => right.vectorSim - left.vectorSim).slice(0, params.limit);
6905
+ }
6906
+ async function listEpisodesWithoutEmbeddings(executor, limit) {
6907
+ const normalizedLimit = normalizePositiveInteger(limit);
6908
+ const result = await executor.execute({
6909
+ sql: `
6910
+ SELECT ${EPISODE_SELECT_COLUMNS}
6911
+ FROM episodes
6912
+ WHERE ${buildActiveEpisodeClause()}
6913
+ AND embedding IS NULL
6914
+ ORDER BY started_at DESC, id ASC
6915
+ ${normalizedLimit ? "LIMIT ?" : ""}
6916
+ `,
6917
+ args: normalizedLimit ? [normalizedLimit] : []
6918
+ });
6919
+ return result.rows.map((row) => mapEpisodeRow(row));
6920
+ }
6921
+ async function updateEpisodeEmbedding(executor, id, embedding) {
6922
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6923
+ const vectorJson = serializeEmbeddingForVector(embedding);
6924
+ await executor.execute({
6925
+ sql: `
6926
+ UPDATE episodes
6927
+ SET embedding = CASE WHEN ? IS NULL THEN NULL ELSE vector32(?) END,
6928
+ updated_at = ?
6929
+ WHERE id = ?
6930
+ `,
6931
+ args: [vectorJson, vectorJson, now, id]
6932
+ });
6933
+ }
6934
+ function createEpisodePayloadHash(payload) {
6935
+ return createHash3("sha256").update(JSON.stringify(payload)).digest("hex");
6936
+ }
6937
+ function fromStoredEpisode(episode) {
6938
+ return {
6939
+ source: episode.source,
6940
+ sourceId: episode.sourceId,
6941
+ sourceRef: episode.sourceRef,
6942
+ transcriptHash: episode.transcriptHash,
6943
+ agentId: episode.agentId,
6944
+ surface: episode.surface,
6945
+ startedAt: episode.startedAt,
6946
+ endedAt: episode.endedAt,
6947
+ summary: episode.summary,
6948
+ tags: episode.tags,
6949
+ activityLevel: episode.activityLevel,
6950
+ userId: episode.userId,
6951
+ project: episode.project,
6952
+ genModel: episode.genModel,
6953
+ genVersion: episode.genVersion,
6954
+ messageCount: episode.messageCount,
6955
+ embedding: episode.embedding
6956
+ };
6957
+ }
6958
+ async function getEpisodeById(executor, id) {
6959
+ const result = await executor.execute({
6960
+ sql: `
6961
+ SELECT ${EPISODE_SELECT_COLUMNS}
6962
+ FROM episodes
6963
+ WHERE id = ?
6964
+ LIMIT 1
6965
+ `,
6966
+ args: [id]
6967
+ });
6968
+ const row = result.rows[0];
6969
+ if (!row) {
6970
+ throw new Error(`Episode ${id} was not found after persistence.`);
6971
+ }
6972
+ return mapEpisodeRow(row);
6973
+ }
6974
+ function normalizeEpisodePayload(input) {
6975
+ const sourceId = normalizeOptionalString4(input.sourceId);
6976
+ const transcriptHash = normalizeOptionalString4(input.transcriptHash);
6977
+ if (!sourceId && !transcriptHash) {
6978
+ throw new Error("Episode writes require either sourceId or transcriptHash.");
6979
+ }
6980
+ const startedAt = normalizeRequiredString(input.startedAt, "startedAt");
6981
+ const endedAt = normalizeOptionalString4(input.endedAt);
6982
+ const summary = normalizeSummary2(input.summary);
6983
+ return {
6984
+ source: input.source,
6985
+ ...sourceId ? { sourceId } : {},
6986
+ ...normalizeOptionalString4(input.sourceRef) ? { sourceRef: normalizeOptionalString4(input.sourceRef) ?? void 0 } : {},
6987
+ ...transcriptHash ? { transcriptHash } : {},
6988
+ ...normalizeOptionalString4(input.agentId) ? { agentId: normalizeOptionalString4(input.agentId) ?? void 0 } : {},
6989
+ ...normalizeOptionalString4(input.surface) ? { surface: normalizeOptionalString4(input.surface) ?? void 0 } : {},
6990
+ startedAt,
6991
+ ...endedAt ? { endedAt } : {},
6992
+ summary,
6993
+ tags: normalizeTags3(input.tags),
6994
+ ...normalizeActivityLevel2(input.activityLevel) ? { activityLevel: normalizeActivityLevel2(input.activityLevel) } : {},
6995
+ ...normalizeOptionalString4(input.userId) ? { userId: normalizeOptionalString4(input.userId) ?? void 0 } : {},
6996
+ ...normalizeOptionalText(input.project) ? { project: normalizeOptionalText(input.project) ?? void 0 } : {},
6997
+ ...normalizeOptionalString4(input.genModel) ? { genModel: normalizeOptionalString4(input.genModel) ?? void 0 } : {},
6998
+ ...normalizeOptionalString4(input.genVersion) ? { genVersion: normalizeOptionalString4(input.genVersion) ?? void 0 } : {},
6999
+ ...normalizeOptionalInteger(input.messageCount) !== void 0 ? { messageCount: normalizeOptionalInteger(input.messageCount) } : {},
7000
+ ...normalizeEmbedding2(input.embedding) ? { embedding: normalizeEmbedding2(input.embedding) } : {}
7001
+ };
7002
+ }
7003
+ function resolveWindowBounds(window) {
7004
+ switch (window.kind) {
7005
+ case "interval": {
7006
+ if (!window.start || !window.end) {
7007
+ return null;
7008
+ }
7009
+ return { start: window.start.toISOString(), end: window.end.toISOString() };
7010
+ }
7011
+ case "anchor": {
7012
+ if (!window.anchor || window.radiusDays === void 0 || window.radiusDays < 0) {
7013
+ return null;
7014
+ }
7015
+ const radiusMs = Math.trunc(window.radiusDays) * 24 * 60 * 60 * 1e3;
7016
+ return {
7017
+ start: new Date(window.anchor.getTime() - radiusMs).toISOString(),
7018
+ end: new Date(window.anchor.getTime() + radiusMs).toISOString()
7019
+ };
7020
+ }
7021
+ case "open_end":
7022
+ return window.start ? { start: window.start.toISOString() } : null;
7023
+ case "open_start":
7024
+ return window.end ? { end: window.end.toISOString() } : null;
7025
+ default:
7026
+ return null;
7027
+ }
7028
+ }
7029
+ function readRequiredIdentityHash(payload) {
7030
+ if (!payload.transcriptHash) {
7031
+ throw new Error("Episode writes without sourceId require transcriptHash.");
7032
+ }
7033
+ return payload.transcriptHash;
7034
+ }
7035
+ function normalizeActivityLevel2(value) {
7036
+ if (!value) {
7037
+ return void 0;
7038
+ }
7039
+ return value;
7040
+ }
7041
+ function normalizeOptionalString4(value) {
7042
+ const trimmed = value?.trim();
7043
+ return trimmed ? trimmed : void 0;
7044
+ }
7045
+ function normalizeOptionalText(value) {
7046
+ const normalized = value?.replace(/\s+/gu, " ").trim();
7047
+ return normalized ? normalized : void 0;
7048
+ }
7049
+ function normalizeSummary2(value) {
7050
+ const normalized = value.replace(/\s+/gu, " ").trim();
7051
+ if (!normalized) {
7052
+ throw new Error("Episode summary must not be empty.");
7053
+ }
7054
+ return normalized;
7055
+ }
7056
+ function normalizeTags3(tags) {
7057
+ if (!tags || tags.length === 0) {
7058
+ return [];
7059
+ }
7060
+ return Array.from(new Set(tags.map((tag) => tag.trim().toLowerCase()).filter((tag) => tag.length > 0))).sort((left, right) => left.localeCompare(right)).slice(0, 8);
7061
+ }
7062
+ function normalizeOptionalInteger(value) {
7063
+ if (value === void 0 || !Number.isFinite(value)) {
7064
+ return void 0;
7065
+ }
7066
+ return Math.trunc(value);
7067
+ }
7068
+ function normalizeEmbedding2(embedding) {
7069
+ if (!embedding || embedding.length === 0) {
7070
+ return void 0;
7071
+ }
7072
+ return embedding.map((value) => Number.isFinite(value) ? value : 0);
7073
+ }
7074
+ function normalizeRequiredString(value, fieldName) {
7075
+ const normalized = value.trim();
7076
+ if (!normalized) {
7077
+ throw new Error(`Episode field "${fieldName}" must not be empty.`);
7078
+ }
7079
+ return normalized;
7080
+ }
7081
+ function normalizePositiveInteger(value) {
7082
+ const normalized = normalizeOptionalInteger(value);
7083
+ if (normalized === void 0 || normalized <= 0) {
7084
+ return void 0;
7085
+ }
7086
+ return normalized;
7087
+ }
7088
+ function prefixColumns(columns, alias) {
7089
+ return columns.split(",").map((col) => {
7090
+ const trimmed = col.trim();
7091
+ return trimmed ? `${alias}.${trimmed}` : "";
7092
+ }).filter(Boolean).join(", ");
7093
+ }
7094
+ function wrapEpisodeVectorError(error) {
7095
+ const message = error instanceof Error ? error.message : String(error);
7096
+ return new Error(`Episode vector search is unavailable: ${message}`);
7097
+ }
7098
+ function toNullableString(value) {
7099
+ return value ?? null;
7100
+ }
7101
+ function toNullableInteger(value) {
7102
+ return value ?? null;
7103
+ }
7104
+
7105
+ // ../../src/adapters/db/client.ts
5049
7106
  var DEFAULT_BUSY_TIMEOUT_MS = 3e3;
5050
7107
  async function createDatabase(dbPath) {
5051
7108
  const client = await openClient(dbPath);
@@ -5083,6 +7140,34 @@ var LibsqlDatabase = class _LibsqlDatabase {
5083
7140
  async findExistingHashes(hashes) {
5084
7141
  return findExistingHashes(this.executor, hashes);
5085
7142
  }
7143
+ /** Loads one episode by stable `(source, sourceId)` identity. */
7144
+ async getEpisodeBySourceId(source, sourceId) {
7145
+ return getEpisodeBySourceId(this.executor, source, sourceId);
7146
+ }
7147
+ /** Loads one episode by fallback `(source, transcriptHash)` identity. */
7148
+ async getEpisodeByTranscriptHash(source, transcriptHash) {
7149
+ return getEpisodeByTranscriptHash(this.executor, source, transcriptHash);
7150
+ }
7151
+ /** Inserts or updates an episodic-memory row using normalized change detection. */
7152
+ async upsertEpisode(input) {
7153
+ return upsertEpisode(this.executor, input);
7154
+ }
7155
+ /** Lists active episodes whose time range overlaps the requested window. */
7156
+ async listEpisodesByTimeWindow(window, limit) {
7157
+ return listEpisodesByTimeWindow(this.executor, window, limit);
7158
+ }
7159
+ /** Finds active episodes by vector similarity. */
7160
+ async episodeVectorSearch(params) {
7161
+ return episodeVectorSearch(this.executor, params);
7162
+ }
7163
+ /** Lists active episodes that are still missing embeddings. */
7164
+ async listEpisodesWithoutEmbeddings(limit) {
7165
+ return listEpisodesWithoutEmbeddings(this.executor, limit);
7166
+ }
7167
+ /** Updates only the embedding payload for one episode row. */
7168
+ async updateEpisodeEmbedding(id, embedding) {
7169
+ await updateEpisodeEmbedding(this.executor, id, embedding);
7170
+ }
5086
7171
  /** Finds which normalized content hashes already exist in storage. */
5087
7172
  async findExistingNormHashes(hashes) {
5088
7173
  return findExistingNormHashes(this.executor, hashes);
@@ -5143,8 +7228,8 @@ async function openClient(dbPath) {
5143
7228
  throw new Error("Database path must not be empty.");
5144
7229
  }
5145
7230
  if (trimmedPath !== ":memory:" && !trimmedPath.startsWith("file:")) {
5146
- const resolvedPath = path6.resolve(trimmedPath);
5147
- await fs7.mkdir(path6.dirname(resolvedPath), { recursive: true });
7231
+ const resolvedPath = path8.resolve(trimmedPath);
7232
+ await fs8.mkdir(path8.dirname(resolvedPath), { recursive: true });
5148
7233
  }
5149
7234
  const client = createClient({ url: resolveClientUrl(trimmedPath) });
5150
7235
  await client.execute("PRAGMA foreign_keys = ON");
@@ -5161,7 +7246,7 @@ function resolveClientUrl(dbPath) {
5161
7246
  if (dbPath.startsWith("file:")) {
5162
7247
  return dbPath;
5163
7248
  }
5164
- return `file:${path6.resolve(dbPath)}`;
7249
+ return `file:${path8.resolve(dbPath)}`;
5165
7250
  }
5166
7251
  async function rollbackTransaction(transaction) {
5167
7252
  if (transaction.closed) {
@@ -5274,7 +7359,6 @@ function requireApiKey(status) {
5274
7359
  function createSessionStartTracker() {
5275
7360
  const seenSessionIds = /* @__PURE__ */ new Set();
5276
7361
  const seenSessionKeys = /* @__PURE__ */ new Set();
5277
- const latestResetBySessionKey = /* @__PURE__ */ new Map();
5278
7362
  const resumedFromBySessionId = /* @__PURE__ */ new Map();
5279
7363
  const countActiveSessions = () => seenSessionIds.size + seenSessionKeys.size;
5280
7364
  return {
@@ -5312,18 +7396,6 @@ function createSessionStartTracker() {
5312
7396
  activeCount: countActiveSessions()
5313
7397
  };
5314
7398
  },
5315
- rememberReset(sessionKey, record) {
5316
- const normalizedSessionKey = sessionKey?.trim();
5317
- const normalizedSessionFile = record.sessionFile.trim();
5318
- if (!normalizedSessionKey || normalizedSessionFile.length === 0) {
5319
- return;
5320
- }
5321
- latestResetBySessionKey.set(normalizedSessionKey, {
5322
- sessionFile: normalizedSessionFile,
5323
- recordedAt: record.recordedAt,
5324
- ...record.sessionId?.trim() ? { sessionId: record.sessionId.trim() } : {}
5325
- });
5326
- },
5327
7399
  rememberSessionStart(sessionId, _sessionKey, resumedFrom) {
5328
7400
  const normalizedSessionId = sessionId?.trim();
5329
7401
  const normalizedResumedFrom = resumedFrom?.trim();
@@ -5332,10 +7404,6 @@ function createSessionStartTracker() {
5332
7404
  }
5333
7405
  resumedFromBySessionId.set(normalizedSessionId, normalizedResumedFrom);
5334
7406
  },
5335
- getLatestReset(sessionKey) {
5336
- const normalizedSessionKey = sessionKey?.trim();
5337
- return normalizedSessionKey ? latestResetBySessionKey.get(normalizedSessionKey) : void 0;
5338
- },
5339
7407
  getResumedFrom(sessionId) {
5340
7408
  const normalizedSessionId = sessionId?.trim();
5341
7409
  return normalizedSessionId ? resumedFromBySessionId.get(normalizedSessionId) : void 0;
@@ -5372,14 +7440,6 @@ var openclaw_default = definePluginEntry({
5372
7440
  tracker
5373
7441
  })
5374
7442
  );
5375
- api.on(
5376
- "before_reset",
5377
- (event, ctx) => handleAgenrBeforeReset(event, ctx, {
5378
- logger: api.logger,
5379
- servicesPromise,
5380
- tracker
5381
- })
5382
- );
5383
7443
  api.on("session_start", (event) => {
5384
7444
  tracker.rememberSessionStart(event.sessionId, event.sessionKey, event.resumedFrom);
5385
7445
  });