agenr 0.9.74 → 0.9.75

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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.75] - 2026-03-07
4
+
5
+ ### Changed
6
+
7
+ - OpenClaw session-start memory injection now runs browse/core recall through a dedicated startup selector that favors continuity utility over raw browse ordering, suppresses overlap with `Recent session`, and applies hard section caps before rendering.
8
+ - Session-start startup memory now treats `fact`, `event`, `lesson`, `relationship`, and `reflection` intentionally instead of sending all residual non-core entries into one broad startup bucket.
9
+ - The session-start composer now reserves space for `Memory Index` before rendering `Recent memory`, so aggressive char trimming keeps recovery breadcrumbs whenever they still fit.
10
+
11
+ ### Fixed
12
+
13
+ - OpenClaw session-start dedupe now records only the entry IDs that actually survive selector filtering and prompt rendering, so startup-trimmed entries remain eligible for later mid-session recall.
14
+ - Session-start selector freshness now treats missing or malformed `updated_at` values conservatively as stale instead of accidentally boosting them as fresh.
15
+ - Tightened session-start exact-state detection to require structured operational state such as active branch/worktree/cwd, numbered PR or issue references, concrete path or file state, or explicit config/env/version state.
16
+ - Added a narrow older-continuity escape hatch so importance-7 lessons, relationships, and exact-state or open-thread facts can survive beyond the freshness window when they still materially affect current work.
17
+ - OpenClaw session-start browse candidate assembly now prefetches a small larger pool, caps `reflection` entries before the final 10-slot cutoff, and backfills remaining slots by existing browse score so durable non-reflection entries are not starved upstream of the selector.
18
+ - Tightened OpenClaw session-start selector admission for historical decisions and preferences so high importance now boosts ranking only after current behavioral, exact-state, or real continuity evidence is present.
19
+ - Made session-start `openThread` detection more precise by treating weak narrative words like `next`, `continue`, and `fix` as continuity signals only when they are very recent, while preserving stronger unfinished-work cues.
20
+ - Removed a redundant post-classification cosmetic-preference filter from session-start selection and added regression coverage for stale high-importance decisions and weak versus strong continuity wording.
21
+
3
22
  ## [0.9.74] - 2026-03-07
4
23
 
5
24
  ### Added
@@ -3,12 +3,12 @@ import {
3
3
  getDb,
4
4
  initDb,
5
5
  normalizeProject
6
- } from "./chunk-PAO647AF.js";
6
+ } from "./chunk-HEE4JOTJ.js";
7
7
  import {
8
8
  toNumber,
9
9
  toRowsAffected,
10
10
  toStringValue
11
- } from "./chunk-Z7MN65K2.js";
11
+ } from "./chunk-UGIJJOPV.js";
12
12
 
13
13
  // src/openclaw-plugin/plugin-db.ts
14
14
  var CREATE_SEEN_SESSIONS_TABLE_SQL = `
@@ -3,7 +3,7 @@ import {
3
3
  parseDaysBetween,
4
4
  toNumber,
5
5
  toStringValue
6
- } from "./chunk-Z7MN65K2.js";
6
+ } from "./chunk-UGIJJOPV.js";
7
7
 
8
8
  // src/utils/errors.ts
9
9
  function toErrorMessage(error) {
@@ -17,14 +17,14 @@ import {
17
17
  runSimpleStream,
18
18
  toErrorMessage,
19
19
  walCheckpoint
20
- } from "./chunk-PAO647AF.js";
20
+ } from "./chunk-HEE4JOTJ.js";
21
21
  import {
22
22
  MILLISECONDS_PER_DAY,
23
23
  getCoRecallNeighbors,
24
24
  parseDaysBetween,
25
25
  toNumber,
26
26
  toStringValue
27
- } from "./chunk-Z7MN65K2.js";
27
+ } from "./chunk-UGIJJOPV.js";
28
28
 
29
29
  // src/utils/parse.ts
30
30
  function parsePositiveInt(value, fallback, label) {
@@ -1,3 +1,9 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
1
7
  // src/utils/entry-utils.ts
2
8
  var MILLISECONDS_PER_DAY = 1e3 * 60 * 60 * 24;
3
9
  function toNumber(value) {
@@ -321,6 +327,7 @@ async function getTopCoRecallEdges(db, limit = 20) {
321
327
  }
322
328
 
323
329
  export {
330
+ __export,
324
331
  MILLISECONDS_PER_DAY,
325
332
  toNumber,
326
333
  toStringValue,
package/dist/cli-main.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  rehabilitateEntry,
10
10
  resolveOpenClawWatcherSessionProjectState,
11
11
  resolveReview
12
- } from "./chunk-KTBSXLVU.js";
12
+ } from "./chunk-AQN6BP74.js";
13
13
  import {
14
14
  ConflictAlreadyResolvedError,
15
15
  DEFAULT_AROUND_RADIUS_DAYS,
@@ -70,7 +70,7 @@ import {
70
70
  toRecord,
71
71
  updateRecallMetadata,
72
72
  warnIfLocked
73
- } from "./chunk-JH355OHS.js";
73
+ } from "./chunk-PUBBITKQ.js";
74
74
  import {
75
75
  APP_VERSION,
76
76
  DEFAULT_DB_PATH,
@@ -131,7 +131,7 @@ import {
131
131
  toErrorMessage,
132
132
  walCheckpoint,
133
133
  writeConfig
134
- } from "./chunk-PAO647AF.js";
134
+ } from "./chunk-HEE4JOTJ.js";
135
135
  import {
136
136
  getCoRecallNeighbors,
137
137
  getTopCoRecallEdges,
@@ -139,7 +139,7 @@ import {
139
139
  toNumber,
140
140
  toRowsAffected,
141
141
  toStringValue
142
- } from "./chunk-Z7MN65K2.js";
142
+ } from "./chunk-UGIJJOPV.js";
143
143
 
144
144
  // src/cli-main.ts
145
145
  import * as clack18 from "@clack/prompts";
@@ -14795,7 +14795,7 @@ async function processTarget(params) {
14795
14795
  try {
14796
14796
  const existingIds = await queue.runExclusive(() => getSourceEntryIds(db, target.file));
14797
14797
  if (existingIds.size >= 2) {
14798
- const { strengthenCoRecallEdges, MAX_USED_ENTRIES } = await import("./co-recall-MOA5R4D6.js");
14798
+ const { strengthenCoRecallEdges, MAX_USED_ENTRIES } = await import("./co-recall-VWYTWS7P.js");
14799
14799
  const idArr = [...existingIds].slice(0, MAX_USED_ENTRIES);
14800
14800
  const expectedEdges = idArr.length * (idArr.length - 1) / 2;
14801
14801
  const edgeCount = await queue.runExclusive(async () => {
@@ -14966,7 +14966,7 @@ async function processTarget(params) {
14966
14966
  try {
14967
14967
  const fileEntryIds = await queue.runExclusive(() => getSourceEntryIds(db, target.file));
14968
14968
  if (fileEntryIds.size >= 2) {
14969
- const { strengthenCoRecallEdges } = await import("./co-recall-MOA5R4D6.js");
14969
+ const { strengthenCoRecallEdges } = await import("./co-recall-VWYTWS7P.js");
14970
14970
  const edgeTimestamp = resolvedDeps.nowFn().toISOString();
14971
14971
  await queue.runExclusive(() => strengthenCoRecallEdges(db, [...fileEntryIds], edgeTimestamp));
14972
14972
  }
@@ -18950,12 +18950,12 @@ function registerMaintainCommand(program) {
18950
18950
  "--only <tasks>",
18951
18951
  "Comma-separated task names: quality,edge-decay,clusters,conflicts,consolidation,retirement,reflection"
18952
18952
  ).option("--prune-edges", "When edge decay runs, delete edges below the configured decay floor").option("--verbose", "Show detailed per-task output").action(async (opts) => {
18953
- const { runMaintainCommand } = await import("./maintain-CKA7EVOW.js");
18953
+ const { runMaintainCommand } = await import("./maintain-QRHVQ45A.js");
18954
18954
  const result = await runMaintainCommand(opts);
18955
18955
  process.exitCode = result.exitCode;
18956
18956
  });
18957
18957
  maintainCommand.command("history").description("Show past maintenance runs").option("--db <path>", "Database path override").option("--limit <n>", "Max runs to show (default: 10)", parseIntOption).option("--json", "Output as JSON").action(async (opts) => {
18958
- const { runMaintainHistoryCommand } = await import("./maintain-CKA7EVOW.js");
18958
+ const { runMaintainHistoryCommand } = await import("./maintain-QRHVQ45A.js");
18959
18959
  const result = await runMaintainHistoryCommand(opts);
18960
18960
  process.exitCode = result.exitCode;
18961
18961
  });
@@ -6,7 +6,7 @@ import {
6
6
  getLastRecallActivity,
7
7
  getTopCoRecallEdges,
8
8
  strengthenCoRecallEdges
9
- } from "./chunk-Z7MN65K2.js";
9
+ } from "./chunk-UGIJJOPV.js";
10
10
  export {
11
11
  MAX_USED_ENTRIES,
12
12
  decayCoRecallEdges,
@@ -27,7 +27,7 @@ import {
27
27
  resolveModelForLlmClient,
28
28
  runConsolidationOrchestrator,
29
29
  toRecord
30
- } from "./chunk-JH355OHS.js";
30
+ } from "./chunk-PUBBITKQ.js";
31
31
  import {
32
32
  DEFAULT_DB_PATH,
33
33
  DEFAULT_TASK_MODEL,
@@ -43,14 +43,14 @@ import {
43
43
  resolveEmbeddingApiKey,
44
44
  runSimpleStream,
45
45
  walCheckpoint
46
- } from "./chunk-PAO647AF.js";
46
+ } from "./chunk-HEE4JOTJ.js";
47
47
  import {
48
48
  decayCoRecallEdges,
49
49
  getLastRecallActivity,
50
50
  parseDaysBetween,
51
51
  toNumber,
52
52
  toStringValue
53
- } from "./chunk-Z7MN65K2.js";
53
+ } from "./chunk-UGIJJOPV.js";
54
54
 
55
55
  // src/llm/maintain-llm.ts
56
56
  var MaintainLlmError = class extends Error {
@@ -7,7 +7,7 @@ import {
7
7
  getSessionProjectState,
8
8
  resolveProjectWorthySessionProject,
9
9
  setSessionProject
10
- } from "../chunk-KTBSXLVU.js";
10
+ } from "../chunk-AQN6BP74.js";
11
11
  import {
12
12
  KNOWLEDGE_TYPES,
13
13
  SCOPE_LEVELS,
@@ -20,13 +20,14 @@ import {
20
20
  resolveProjectScopeFromSlug,
21
21
  runSimpleStream,
22
22
  toErrorMessage
23
- } from "../chunk-PAO647AF.js";
23
+ } from "../chunk-HEE4JOTJ.js";
24
24
  import {
25
25
  MILLISECONDS_PER_DAY,
26
+ __export,
26
27
  strengthenCoRecallEdges,
27
28
  toNumber,
28
29
  toStringValue
29
- } from "../chunk-Z7MN65K2.js";
30
+ } from "../chunk-UGIJJOPV.js";
30
31
 
31
32
  // src/openclaw-plugin/index.ts
32
33
  import os3 from "os";
@@ -615,6 +616,19 @@ function formatMidSessionRecall(results, options = {}) {
615
616
  }
616
617
 
617
618
  // src/openclaw-plugin/recall.ts
619
+ var recall_exports = {};
620
+ __export(recall_exports, {
621
+ buildSpawnArgs: () => buildSpawnArgs2,
622
+ fetchCoreEntries: () => fetchCoreEntries,
623
+ formatCoreAsMarkdown: () => formatCoreAsMarkdown,
624
+ formatMemoryIndex: () => formatMemoryIndex,
625
+ formatRecallAsMarkdown: () => formatRecallAsMarkdown,
626
+ renderRecallAsMarkdown: () => renderRecallAsMarkdown,
627
+ resolveAgenrPath: () => resolveAgenrPath,
628
+ resolveBudget: () => resolveBudget,
629
+ runMemoryIndex: () => runMemoryIndex,
630
+ runRecall: () => runRecall
631
+ });
618
632
  import { spawn as spawn2 } from "child_process";
619
633
  import path from "path";
620
634
  import { fileURLToPath } from "url";
@@ -891,10 +905,14 @@ function groupValidEntries(result, options = {}) {
891
905
  if (options.excludeCore && isCoreRecallItem(item)) {
892
906
  continue;
893
907
  }
894
- const groupedEntry = normalizeGroupedEntry(entry, options.addCoreMetadata === true);
895
- if (TODO_TYPES.has(entry.type)) {
908
+ const groupedEntry = {
909
+ item,
910
+ entry: normalizeGroupedEntry(entry, options.addCoreMetadata === true)
911
+ };
912
+ const explicitCategory = item.category;
913
+ if (explicitCategory === "active" || TODO_TYPES.has(entry.type)) {
896
914
  grouped.todos.push(groupedEntry);
897
- } else if (PREFERENCE_TYPES.has(entry.type)) {
915
+ } else if (explicitCategory === "preferences" || PREFERENCE_TYPES.has(entry.type)) {
898
916
  grouped.preferences.push(groupedEntry);
899
917
  } else {
900
918
  grouped.facts.push(groupedEntry);
@@ -911,17 +929,21 @@ function collectCoreEntries(result) {
911
929
  if (!isCoreRecallItem(item) || !isValidEntry(item.entry)) {
912
930
  continue;
913
931
  }
914
- entries.push(normalizeGroupedEntry(item.entry, true));
932
+ entries.push({
933
+ item,
934
+ entry: normalizeGroupedEntry(item.entry, true)
935
+ });
915
936
  }
916
937
  return sortCorePriority(entries);
917
938
  }
918
939
  function buildSectionMarkdown(header, entries, maxEntryChars, maxChars) {
919
940
  if (entries.length === 0 || maxChars <= 0) {
920
- return { markdown: "", entryCount: 0 };
941
+ return { markdown: "", entryCount: 0, renderedItems: [] };
921
942
  }
922
943
  const lines = [];
923
944
  let totalChars = 0;
924
945
  let entryCount = 0;
946
+ const renderedItems = [];
925
947
  const tryPushLine = (line) => {
926
948
  const nextLength = totalChars + (lines.length > 0 ? 1 : 0) + line.length;
927
949
  if (nextLength > maxChars) {
@@ -932,20 +954,21 @@ function buildSectionMarkdown(header, entries, maxEntryChars, maxChars) {
932
954
  return true;
933
955
  };
934
956
  if (!tryPushLine(header) || !tryPushLine("")) {
935
- return { markdown: "", entryCount: 0 };
957
+ return { markdown: "", entryCount: 0, renderedItems: [] };
936
958
  }
937
959
  for (const entry of entries) {
938
- const line = `- [${entry.subject}] ${truncateEntryContent(entry.content, maxEntryChars)}`;
960
+ const line = `- [${entry.entry.subject}] ${truncateEntryContent(entry.entry.content, maxEntryChars)}`;
939
961
  if (!tryPushLine(line)) {
940
962
  break;
941
963
  }
942
964
  entryCount += 1;
965
+ renderedItems.push(entry.item);
943
966
  }
944
967
  if (entryCount === 0) {
945
- return { markdown: "", entryCount: 0 };
968
+ return { markdown: "", entryCount: 0, renderedItems: [] };
946
969
  }
947
970
  tryPushLine("");
948
- return { markdown: lines.join("\n").trimEnd(), entryCount };
971
+ return { markdown: lines.join("\n").trimEnd(), entryCount, renderedItems };
949
972
  }
950
973
  function createBlockAccumulator(maxChars) {
951
974
  const blocks = [];
@@ -970,6 +993,9 @@ function createBlockAccumulator(maxChars) {
970
993
  };
971
994
  }
972
995
  function formatRecallAsMarkdown(result, options = {}) {
996
+ return renderRecallAsMarkdown(result, options).markdown;
997
+ }
998
+ function renderRecallAsMarkdown(result, options = {}) {
973
999
  const maxEntryChars = resolveFormatLimit(
974
1000
  options.maxEntryChars,
975
1001
  DEFAULT_FORMAT_MAX_ENTRY_CHARS2
@@ -978,36 +1004,46 @@ function formatRecallAsMarkdown(result, options = {}) {
978
1004
  const coreMaxChars = resolveFormatLimit(options.coreMaxChars, maxChars);
979
1005
  return formatRecallWithLimits(result, maxEntryChars, maxChars, {
980
1006
  coreMaxChars,
981
- includeRootHeader: options.includeRootHeader !== false
1007
+ includeRootHeader: options.includeRootHeader !== false,
1008
+ recentSectionHeader: options.recentSectionHeader ?? "### Facts and Events"
982
1009
  });
983
1010
  }
1011
+ function formatCoreAsMarkdown(result, options = {}) {
1012
+ const maxEntryChars = resolveFormatLimit(
1013
+ options.maxEntryChars,
1014
+ DEFAULT_FORMAT_MAX_ENTRY_CHARS2
1015
+ );
1016
+ const maxChars = resolveFormatLimit(options.maxChars, DEFAULT_FORMAT_MAX_CHARS2);
1017
+ return formatCoreWithLimits(result, maxEntryChars, maxChars);
1018
+ }
984
1019
  function sortCorePriority(entries) {
985
1020
  return [...entries].sort((a, b) => {
986
- const aUniversal = a._isUniversal ?? false;
987
- const bUniversal = b._isUniversal ?? false;
1021
+ const aUniversal = a.entry._isUniversal ?? false;
1022
+ const bUniversal = b.entry._isUniversal ?? false;
988
1023
  if (aUniversal !== bUniversal) {
989
1024
  return aUniversal ? -1 : 1;
990
1025
  }
991
- const aImportance = a._importance ?? 0;
992
- const bImportance = b._importance ?? 0;
1026
+ const aImportance = a.entry._importance ?? 0;
1027
+ const bImportance = b.entry._importance ?? 0;
993
1028
  if (aImportance !== bImportance) {
994
1029
  return bImportance - aImportance;
995
1030
  }
996
- return a.subject.localeCompare(b.subject);
1031
+ return a.entry.subject.localeCompare(b.entry.subject);
997
1032
  });
998
1033
  }
999
1034
  function formatRecallWithLimits(result, maxEntryChars, maxChars, options) {
1000
1035
  if (!result.results || result.results.length === 0) {
1001
- return "";
1036
+ return { markdown: "", renderedItems: [] };
1002
1037
  }
1003
1038
  const coreEntries = collectCoreEntries(result);
1004
1039
  const { todos, preferences, facts } = groupValidEntries(result, { excludeCore: true });
1005
1040
  if (coreEntries.length === 0 && todos.length === 0 && preferences.length === 0 && facts.length === 0) {
1006
- return "";
1041
+ return { markdown: "", renderedItems: [] };
1007
1042
  }
1008
1043
  const blocks = createBlockAccumulator(maxChars);
1044
+ const renderedItems = [];
1009
1045
  if (options.includeRootHeader && !blocks.tryPushBlock("## agenr Memory Context", 0)) {
1010
- return blocks.finalize();
1046
+ return { markdown: blocks.finalize(), renderedItems };
1011
1047
  }
1012
1048
  if (coreEntries.length > 0) {
1013
1049
  const coreBudget = Math.min(blocks.remainingChars(), options.coreMaxChars);
@@ -1018,13 +1054,47 @@ function formatRecallWithLimits(result, maxEntryChars, maxChars, options) {
1018
1054
  coreBudget
1019
1055
  );
1020
1056
  if (!blocks.tryPushBlock(coreSection.markdown, coreSection.entryCount)) {
1021
- return blocks.finalize();
1057
+ return { markdown: blocks.finalize(), renderedItems };
1022
1058
  }
1059
+ renderedItems.push(...coreSection.renderedItems);
1023
1060
  }
1024
1061
  for (const [header, entries] of [
1025
1062
  ["### Active Todos", todos],
1026
1063
  ["### Preferences and Decisions", preferences],
1027
- ["### Facts and Events", facts]
1064
+ [options.recentSectionHeader, facts]
1065
+ ]) {
1066
+ const section = buildSectionMarkdown(
1067
+ header,
1068
+ entries,
1069
+ maxEntryChars,
1070
+ blocks.remainingChars()
1071
+ );
1072
+ if (!blocks.tryPushBlock(section.markdown, section.entryCount)) {
1073
+ return { markdown: blocks.finalize(), renderedItems };
1074
+ }
1075
+ renderedItems.push(...section.renderedItems);
1076
+ }
1077
+ return { markdown: blocks.finalize(), renderedItems };
1078
+ }
1079
+ function formatCoreWithLimits(result, maxEntryChars, maxChars) {
1080
+ if (!result.results || result.results.length === 0) {
1081
+ return "";
1082
+ }
1083
+ let { todos, preferences, facts } = groupValidEntries(result, { addCoreMetadata: true });
1084
+ todos = sortCorePriority(todos);
1085
+ preferences = sortCorePriority(preferences);
1086
+ facts = sortCorePriority(facts);
1087
+ if (todos.length === 0 && preferences.length === 0 && facts.length === 0) {
1088
+ return "";
1089
+ }
1090
+ const blocks = createBlockAccumulator(maxChars);
1091
+ if (!blocks.tryPushBlock("### Core Context", 0)) {
1092
+ return blocks.finalize();
1093
+ }
1094
+ for (const [header, entries] of [
1095
+ ["#### Active Todos", todos],
1096
+ ["#### Preferences and Decisions", preferences],
1097
+ ["#### Facts and Events", facts]
1028
1098
  ]) {
1029
1099
  const section = buildSectionMarkdown(
1030
1100
  header,
@@ -2641,6 +2711,576 @@ async function runHandoffForSession(opts) {
2641
2711
  import os from "os";
2642
2712
  import path4 from "path";
2643
2713
 
2714
+ // src/openclaw-plugin/session-start-selector.ts
2715
+ var DEFAULT_SESSION_START_SECTION_CAPS = {
2716
+ core: 6,
2717
+ active: 4,
2718
+ preferences: 4,
2719
+ recent: 2
2720
+ };
2721
+ var CATEGORY_PRIORITY = {
2722
+ core: 0,
2723
+ active: 1,
2724
+ preferences: 2,
2725
+ recent: 3
2726
+ };
2727
+ var BEHAVIOR_KEYWORDS = [
2728
+ "always",
2729
+ "avoid",
2730
+ "constraint",
2731
+ "default",
2732
+ "do not",
2733
+ "don't",
2734
+ "format",
2735
+ "must",
2736
+ "must not",
2737
+ "never",
2738
+ "policy",
2739
+ "rule",
2740
+ "style",
2741
+ "workflow"
2742
+ ];
2743
+ var STRONG_OPEN_THREAD_KEYWORDS = [
2744
+ "blocker",
2745
+ "blocked",
2746
+ "failing",
2747
+ "follow up",
2748
+ "follow-up",
2749
+ "in progress",
2750
+ "open thread",
2751
+ "pending",
2752
+ "stuck",
2753
+ "todo",
2754
+ "unfinished",
2755
+ "waiting",
2756
+ "wip"
2757
+ ];
2758
+ var WEAK_OPEN_THREAD_KEYWORDS = [
2759
+ "continue",
2760
+ "fix",
2761
+ "next",
2762
+ "remaining",
2763
+ "resume"
2764
+ ];
2765
+ var OPERATIONAL_KEYWORDS = [
2766
+ "build",
2767
+ "check",
2768
+ "command",
2769
+ "debug",
2770
+ "deploy",
2771
+ "install",
2772
+ "rerun",
2773
+ "run",
2774
+ "ship",
2775
+ "test",
2776
+ "verify",
2777
+ "write"
2778
+ ];
2779
+ var RELATIONSHIP_KEYWORDS = [
2780
+ "blocked by",
2781
+ "contact",
2782
+ "depends on",
2783
+ "maintainer",
2784
+ "owner",
2785
+ "paired with",
2786
+ "reviewer"
2787
+ ];
2788
+ var ARCHIVAL_KEYWORDS = [
2789
+ "closed",
2790
+ "completed",
2791
+ "deprecated",
2792
+ "done",
2793
+ "fixed",
2794
+ "historical",
2795
+ "launched",
2796
+ "merged",
2797
+ "released",
2798
+ "resolved",
2799
+ "retired",
2800
+ "shipped",
2801
+ "used to"
2802
+ ];
2803
+ var COSMETIC_PREFERENCE_KEYWORDS = [
2804
+ "dark mode",
2805
+ "font",
2806
+ "screenshot",
2807
+ "theme",
2808
+ "ui"
2809
+ ];
2810
+ var EXACT_STATE_PATH_HINT_PATTERN = /\b(?:cwd|file|path|repo path|worktree)\b/i;
2811
+ var EXACT_STATE_PATH_VALUE_PATTERN = /(?:^|[\s(])(?:~\/|\/[^\s)]+|[A-Za-z0-9._-]+\/[A-Za-z0-9._/-]+|[A-Za-z0-9._-]+\.[A-Za-z0-9]{1,8})(?=$|[\s),])/;
2812
+ var EXACT_STATE_ISSUE_PATTERN = /\b(?:pr|issue)\s*#?\d+\b/i;
2813
+ var EXACT_STATE_COMMIT_PATTERN = /\bcommit\b/i;
2814
+ var EXACT_STATE_COMMIT_HASH_PATTERN = /\b[0-9a-f]{7,40}\b/i;
2815
+ var EXACT_STATE_TAG_PATTERN = /\btag\b/i;
2816
+ var EXACT_STATE_ENV_PATTERN = /\b(?:env|environment)\b/i;
2817
+ var EXACT_STATE_CONFIG_PATTERN = /\bconfig\b/i;
2818
+ var EXACT_STATE_VERSION_PATTERN = /\bversion\b/i;
2819
+ var EXACT_STATE_STATEFUL_PATTERN = /\b(?:active|current|disabled|enabled|pinned|points to|running|set to|using)\b/i;
2820
+ var EXACT_STATE_ENV_ASSIGNMENT_PATTERN = /\b[A-Z][A-Z0-9_]{1,}=[^\s]+\b/;
2821
+ var EXACT_STATE_SEMVER_PATTERN = /\bv?\d+\.\d+(?:\.\d+)?\b/i;
2822
+ var IMPORTANCE_SEVEN_CONTINUITY_MAX_AGE_DAYS = 120;
2823
+ function normalizeText(value) {
2824
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
2825
+ }
2826
+ function tokenize(value) {
2827
+ return normalizeText(value).split(" ").map((token) => token.trim()).filter((token) => token.length >= 4);
2828
+ }
2829
+ function toTimestamp(value) {
2830
+ if (typeof value !== "string") {
2831
+ return 0;
2832
+ }
2833
+ const timestamp = Date.parse(value);
2834
+ return Number.isFinite(timestamp) ? timestamp : 0;
2835
+ }
2836
+ function containsKeyword(text, keywords) {
2837
+ return keywords.some((keyword) => text.includes(keyword));
2838
+ }
2839
+ function hasOpenThreadSignal(text, veryRecent) {
2840
+ if (containsKeyword(text, STRONG_OPEN_THREAD_KEYWORDS)) {
2841
+ return true;
2842
+ }
2843
+ return veryRecent && containsKeyword(text, WEAK_OPEN_THREAD_KEYWORDS);
2844
+ }
2845
+ function overlapRatio(candidateText, referenceTokens) {
2846
+ if (referenceTokens.size === 0) {
2847
+ return 0;
2848
+ }
2849
+ const tokens = new Set(tokenize(candidateText));
2850
+ if (tokens.size === 0) {
2851
+ return 0;
2852
+ }
2853
+ let overlap = 0;
2854
+ for (const token of tokens) {
2855
+ if (referenceTokens.has(token)) {
2856
+ overlap += 1;
2857
+ }
2858
+ }
2859
+ return overlap / tokens.size;
2860
+ }
2861
+ function subjectKey(item) {
2862
+ const subject = typeof item.entry?.subject === "string" ? normalizeText(item.entry.subject) : "";
2863
+ return subject ? `subject:${subject}` : null;
2864
+ }
2865
+ function contentKey(item) {
2866
+ const content = typeof item.entry?.content === "string" ? normalizeText(item.entry.content) : "";
2867
+ return content ? `content:${content.slice(0, 200)}` : null;
2868
+ }
2869
+ function exactKey(item) {
2870
+ const id = typeof item.entry?.id === "string" ? item.entry.id.trim() : "";
2871
+ if (id) {
2872
+ return `id:${id}`;
2873
+ }
2874
+ const type = typeof item.entry?.type === "string" ? item.entry.type.trim() : "";
2875
+ const subject = typeof item.entry?.subject === "string" ? normalizeText(item.entry.subject) : "";
2876
+ const content = typeof item.entry?.content === "string" ? normalizeText(item.entry.content) : "";
2877
+ if (!type || !subject || !content) {
2878
+ return null;
2879
+ }
2880
+ return `entry:${type}\0${subject}\0${content}`;
2881
+ }
2882
+ function buildOverlapKeys(item) {
2883
+ return [exactKey(item), subjectKey(item), contentKey(item)].filter((value) => Boolean(value));
2884
+ }
2885
+ function getImportance(item) {
2886
+ const rawImportance = item.entry?.importance;
2887
+ return typeof rawImportance === "number" && Number.isFinite(rawImportance) ? rawImportance : 0;
2888
+ }
2889
+ function getAgeDays(item, now) {
2890
+ const updatedAt = toTimestamp(item.entry?.updated_at);
2891
+ if (updatedAt <= 0 || now <= 0) {
2892
+ return Number.POSITIVE_INFINITY;
2893
+ }
2894
+ return Math.max(0, (now - updatedAt) / (24 * 60 * 60 * 1e3));
2895
+ }
2896
+ function hasExactStateSignal(subject, content) {
2897
+ const text = normalizeText(`${subject} ${content}`);
2898
+ const rawText = `${subject} ${content}`;
2899
+ if (containsKeyword(text, ["branch", "cwd", "worktree"])) {
2900
+ return true;
2901
+ }
2902
+ if (EXACT_STATE_ISSUE_PATTERN.test(rawText)) {
2903
+ return true;
2904
+ }
2905
+ if (EXACT_STATE_COMMIT_PATTERN.test(text) && EXACT_STATE_COMMIT_HASH_PATTERN.test(rawText)) {
2906
+ return true;
2907
+ }
2908
+ if (EXACT_STATE_TAG_PATTERN.test(text) && EXACT_STATE_SEMVER_PATTERN.test(rawText)) {
2909
+ return true;
2910
+ }
2911
+ if (EXACT_STATE_PATH_HINT_PATTERN.test(text) && EXACT_STATE_PATH_VALUE_PATTERN.test(rawText)) {
2912
+ return true;
2913
+ }
2914
+ if (EXACT_STATE_ENV_PATTERN.test(text)) {
2915
+ return EXACT_STATE_ENV_ASSIGNMENT_PATTERN.test(rawText) || EXACT_STATE_STATEFUL_PATTERN.test(text);
2916
+ }
2917
+ if (EXACT_STATE_CONFIG_PATTERN.test(text) && EXACT_STATE_STATEFUL_PATTERN.test(text)) {
2918
+ return true;
2919
+ }
2920
+ if (EXACT_STATE_VERSION_PATTERN.test(text) && EXACT_STATE_SEMVER_PATTERN.test(rawText) && EXACT_STATE_STATEFUL_PATTERN.test(text)) {
2921
+ return true;
2922
+ }
2923
+ return false;
2924
+ }
2925
+ function buildSignals(item, now) {
2926
+ const subject = typeof item.entry?.subject === "string" ? item.entry.subject : "";
2927
+ const content = typeof item.entry?.content === "string" ? item.entry.content : "";
2928
+ const text = normalizeText(`${subject} ${content}`);
2929
+ const ageDays = getAgeDays(item, now);
2930
+ const exactState = hasExactStateSignal(subject, content);
2931
+ const veryRecent = ageDays <= 3;
2932
+ const openThread = hasOpenThreadSignal(text, veryRecent);
2933
+ const behaviorImpact = openThread || containsKeyword(text, BEHAVIOR_KEYWORDS);
2934
+ const operational = behaviorImpact || exactState || containsKeyword(text, OPERATIONAL_KEYWORDS);
2935
+ const strongRelationship = containsKeyword(text, RELATIONSHIP_KEYWORDS);
2936
+ return {
2937
+ archival: containsKeyword(text, ARCHIVAL_KEYWORDS),
2938
+ behaviorImpact,
2939
+ exactState,
2940
+ fresh: ageDays <= 14,
2941
+ operational,
2942
+ openThread,
2943
+ recent: ageDays <= 30,
2944
+ strongRelationship,
2945
+ veryRecent
2946
+ };
2947
+ }
2948
+ function hasImportanceSevenContinuityEscape(item, signals, now) {
2949
+ const importance = getImportance(item);
2950
+ if (importance < 7 || importance >= 8 || signals.archival) {
2951
+ return false;
2952
+ }
2953
+ const ageDays = getAgeDays(item, now);
2954
+ if (!Number.isFinite(ageDays) || ageDays <= 14 || ageDays > IMPORTANCE_SEVEN_CONTINUITY_MAX_AGE_DAYS) {
2955
+ return false;
2956
+ }
2957
+ switch (item.entry?.type) {
2958
+ case "lesson":
2959
+ return signals.operational;
2960
+ case "relationship":
2961
+ return signals.strongRelationship;
2962
+ case "fact":
2963
+ case "event":
2964
+ return signals.exactState || signals.behaviorImpact && signals.openThread;
2965
+ default:
2966
+ return false;
2967
+ }
2968
+ }
2969
+ function classifyCandidate(item, signals, now) {
2970
+ const expiry = item.category === "core" || item.entry?.expiry === "core";
2971
+ if (expiry) {
2972
+ return "core";
2973
+ }
2974
+ const type = item.entry?.type;
2975
+ const importance = getImportance(item);
2976
+ const ageDays = getAgeDays(item, now);
2977
+ const recentEnough = ageDays <= 14;
2978
+ if (type === "todo") {
2979
+ return "active";
2980
+ }
2981
+ if (type === "decision" || type === "preference") {
2982
+ const text = normalizeText(`${item.entry?.subject ?? ""} ${item.entry?.content ?? ""}`);
2983
+ const hasCurrentEffect = signals.behaviorImpact || signals.exactState || signals.openThread;
2984
+ if (type === "preference" && importance < 8 && !signals.exactState && !signals.openThread && containsKeyword(text, COSMETIC_PREFERENCE_KEYWORDS)) {
2985
+ return null;
2986
+ }
2987
+ if (signals.archival && !hasCurrentEffect) {
2988
+ return null;
2989
+ }
2990
+ if (hasCurrentEffect) {
2991
+ return "preferences";
2992
+ }
2993
+ return null;
2994
+ }
2995
+ if (type === "reflection") {
2996
+ if (signals.behaviorImpact && signals.openThread && signals.veryRecent && importance >= 9) {
2997
+ return "recent";
2998
+ }
2999
+ return null;
3000
+ }
3001
+ if (type === "relationship") {
3002
+ if (!signals.strongRelationship && !signals.behaviorImpact && !signals.exactState) {
3003
+ return null;
3004
+ }
3005
+ if (recentEnough || importance >= 8 || hasImportanceSevenContinuityEscape(item, signals, now)) {
3006
+ return "recent";
3007
+ }
3008
+ return null;
3009
+ }
3010
+ if (type === "lesson") {
3011
+ if (!signals.operational) {
3012
+ return null;
3013
+ }
3014
+ if (recentEnough || importance >= 8 || hasImportanceSevenContinuityEscape(item, signals, now)) {
3015
+ return "recent";
3016
+ }
3017
+ return null;
3018
+ }
3019
+ if (type === "fact" || type === "event") {
3020
+ if (!signals.exactState && !signals.openThread && !signals.behaviorImpact && item.score < 0.8) {
3021
+ return null;
3022
+ }
3023
+ if (signals.archival && !signals.openThread && !signals.exactState) {
3024
+ return null;
3025
+ }
3026
+ if (recentEnough || importance >= 8 || hasImportanceSevenContinuityEscape(item, signals, now)) {
3027
+ return "recent";
3028
+ }
3029
+ return null;
3030
+ }
3031
+ return null;
3032
+ }
3033
+ function computeUtility(item, category, signals, now) {
3034
+ const importance = getImportance(item);
3035
+ const ageDays = getAgeDays(item, now);
3036
+ let utility = item.score * 5 + importance * 0.7;
3037
+ if (category === "core") {
3038
+ utility += 10;
3039
+ } else if (category === "active") {
3040
+ utility += 6;
3041
+ } else if (category === "preferences") {
3042
+ utility += 4;
3043
+ } else {
3044
+ utility += 1;
3045
+ }
3046
+ if (signals.behaviorImpact) {
3047
+ utility += 3;
3048
+ }
3049
+ if (signals.exactState) {
3050
+ utility += 3;
3051
+ }
3052
+ if (signals.openThread) {
3053
+ utility += 4;
3054
+ }
3055
+ if (signals.operational) {
3056
+ utility += 2;
3057
+ }
3058
+ if (signals.veryRecent) {
3059
+ utility += 2;
3060
+ } else if (signals.fresh) {
3061
+ utility += 1;
3062
+ } else if (ageDays > 30) {
3063
+ utility -= 2;
3064
+ }
3065
+ if (signals.archival) {
3066
+ utility -= 3;
3067
+ }
3068
+ if (item.entry?.type === "relationship") {
3069
+ utility -= 1;
3070
+ }
3071
+ return utility;
3072
+ }
3073
+ function isRedundantWithRecentSession(item, recentSessionText, recentSessionTokens) {
3074
+ if (!recentSessionText) {
3075
+ return false;
3076
+ }
3077
+ const rawSubject = typeof item.entry?.subject === "string" ? item.entry.subject : "";
3078
+ const rawContent = typeof item.entry?.content === "string" ? item.entry.content : "";
3079
+ const subject = normalizeText(rawSubject);
3080
+ const content = normalizeText(rawContent);
3081
+ if (subject && recentSessionText.includes(subject)) {
3082
+ return true;
3083
+ }
3084
+ if (content.length >= 24 && recentSessionText.includes(content.slice(0, 160))) {
3085
+ return true;
3086
+ }
3087
+ const overlap = overlapRatio(`${subject} ${content}`, recentSessionTokens);
3088
+ if (subject.includes("session handoff") && overlap >= 0.45) {
3089
+ return true;
3090
+ }
3091
+ if (hasExactStateSignal(rawSubject, rawContent) && overlap >= 0.45) {
3092
+ return true;
3093
+ }
3094
+ return overlap >= 0.55;
3095
+ }
3096
+ function compareCandidates(a, b) {
3097
+ if (b.utility !== a.utility) {
3098
+ return b.utility - a.utility;
3099
+ }
3100
+ if (CATEGORY_PRIORITY[a.category] !== CATEGORY_PRIORITY[b.category]) {
3101
+ return CATEGORY_PRIORITY[a.category] - CATEGORY_PRIORITY[b.category];
3102
+ }
3103
+ return b.updatedAt - a.updatedAt;
3104
+ }
3105
+ function roundRobinOrder(groups, maxNonCoreEntries) {
3106
+ const ordered = [];
3107
+ const categories = ["active", "preferences", "recent"];
3108
+ let index = 0;
3109
+ while (ordered.length < maxNonCoreEntries) {
3110
+ let addedThisRound = false;
3111
+ for (const category of categories) {
3112
+ const candidate = groups[category][index];
3113
+ if (!candidate) {
3114
+ continue;
3115
+ }
3116
+ ordered.push(candidate);
3117
+ addedThisRound = true;
3118
+ if (ordered.length >= maxNonCoreEntries) {
3119
+ return ordered;
3120
+ }
3121
+ }
3122
+ if (!addedThisRound) {
3123
+ break;
3124
+ }
3125
+ index += 1;
3126
+ }
3127
+ return ordered;
3128
+ }
3129
+ function selectSessionStartMemory(coreResult, browseResult, options = {}) {
3130
+ const now = options.now?.getTime() ?? Date.now();
3131
+ const sectionCaps = {
3132
+ ...DEFAULT_SESSION_START_SECTION_CAPS,
3133
+ ...options.sectionCaps
3134
+ };
3135
+ const maxNonCoreEntries = options.maxNonCoreEntries ?? Number.POSITIVE_INFINITY;
3136
+ const recentSessionText = normalizeText(options.recentSessionText ?? "");
3137
+ const recentSessionTokens = new Set(tokenize(recentSessionText));
3138
+ const coreCandidates = [];
3139
+ const nonCoreCandidates = [];
3140
+ for (const item of coreResult?.results ?? []) {
3141
+ const signals = buildSignals(item, now);
3142
+ coreCandidates.push({
3143
+ item: {
3144
+ ...item,
3145
+ category: "core"
3146
+ },
3147
+ category: "core",
3148
+ overlapKeys: buildOverlapKeys(item),
3149
+ utility: computeUtility(item, "core", signals, now),
3150
+ updatedAt: toTimestamp(item.entry?.updated_at)
3151
+ });
3152
+ }
3153
+ for (const item of browseResult?.results ?? []) {
3154
+ const signals = buildSignals(item, now);
3155
+ const category = classifyCandidate(item, signals, now);
3156
+ if (!category || category === "core") {
3157
+ continue;
3158
+ }
3159
+ if (isRedundantWithRecentSession(item, recentSessionText, recentSessionTokens)) {
3160
+ continue;
3161
+ }
3162
+ nonCoreCandidates.push({
3163
+ item: {
3164
+ ...item,
3165
+ category
3166
+ },
3167
+ category,
3168
+ overlapKeys: buildOverlapKeys(item),
3169
+ utility: computeUtility(item, category, signals, now),
3170
+ updatedAt: toTimestamp(item.entry?.updated_at)
3171
+ });
3172
+ }
3173
+ coreCandidates.sort(compareCandidates);
3174
+ nonCoreCandidates.sort(compareCandidates);
3175
+ const seenOverlapKeys = /* @__PURE__ */ new Set();
3176
+ const selectedCore = [];
3177
+ for (const candidate of coreCandidates) {
3178
+ const overlaps = candidate.overlapKeys.some((key) => seenOverlapKeys.has(key));
3179
+ if (overlaps) {
3180
+ continue;
3181
+ }
3182
+ selectedCore.push(candidate);
3183
+ for (const key of candidate.overlapKeys) {
3184
+ seenOverlapKeys.add(key);
3185
+ }
3186
+ if (selectedCore.length >= sectionCaps.core) {
3187
+ break;
3188
+ }
3189
+ }
3190
+ const grouped = {
3191
+ core: selectedCore,
3192
+ active: [],
3193
+ preferences: [],
3194
+ recent: []
3195
+ };
3196
+ for (const candidate of nonCoreCandidates) {
3197
+ if (grouped[candidate.category].length >= sectionCaps[candidate.category]) {
3198
+ continue;
3199
+ }
3200
+ const overlaps = candidate.overlapKeys.some((key) => seenOverlapKeys.has(key));
3201
+ if (overlaps) {
3202
+ continue;
3203
+ }
3204
+ grouped[candidate.category].push(candidate);
3205
+ for (const key of candidate.overlapKeys) {
3206
+ seenOverlapKeys.add(key);
3207
+ }
3208
+ }
3209
+ const selectedNonCore = roundRobinOrder(grouped, maxNonCoreEntries);
3210
+ const results = [
3211
+ ...grouped.core.map((candidate) => candidate.item),
3212
+ ...grouped.active.filter((candidate) => selectedNonCore.includes(candidate)).map((candidate) => candidate.item),
3213
+ ...grouped.preferences.filter((candidate) => selectedNonCore.includes(candidate)).map((candidate) => candidate.item),
3214
+ ...grouped.recent.filter((candidate) => selectedNonCore.includes(candidate)).map((candidate) => candidate.item)
3215
+ ];
3216
+ if (results.length === 0) {
3217
+ return null;
3218
+ }
3219
+ return {
3220
+ query: browseResult?.query ?? coreResult?.query ?? "[session-start]",
3221
+ results
3222
+ };
3223
+ }
3224
+
3225
+ // src/openclaw-plugin/session-start-browse.ts
3226
+ var SESSION_START_BROWSE_CANDIDATE_LIMIT = 10;
3227
+ var SESSION_START_BROWSE_MIN_IMPORTANCE = 7;
3228
+ var SESSION_START_BROWSE_PREFETCH_MULTIPLIER = 3;
3229
+ var SESSION_START_BROWSE_PREFETCH_LIMIT = SESSION_START_BROWSE_CANDIDATE_LIMIT * SESSION_START_BROWSE_PREFETCH_MULTIPLIER;
3230
+ var SESSION_START_BROWSE_TYPE_CAPS = {
3231
+ reflection: 3
3232
+ };
3233
+ function normalizeType(item) {
3234
+ const rawType = item.entry?.type;
3235
+ return typeof rawType === "string" ? rawType.trim().toLowerCase() : "";
3236
+ }
3237
+ function diversifySessionStartBrowseCandidates(browseResult, limit = SESSION_START_BROWSE_CANDIDATE_LIMIT, typeCaps = SESSION_START_BROWSE_TYPE_CAPS) {
3238
+ if (!browseResult) {
3239
+ return null;
3240
+ }
3241
+ const normalizedLimit = Math.max(0, Math.floor(limit));
3242
+ if (normalizedLimit === 0 || browseResult.results.length === 0) {
3243
+ return {
3244
+ ...browseResult,
3245
+ results: []
3246
+ };
3247
+ }
3248
+ const admittedIndexes = /* @__PURE__ */ new Set();
3249
+ const counts = /* @__PURE__ */ new Map();
3250
+ const selected = [];
3251
+ for (const [index, item] of browseResult.results.entries()) {
3252
+ if (selected.length >= normalizedLimit) {
3253
+ break;
3254
+ }
3255
+ const type = normalizeType(item);
3256
+ const cap = typeCaps[type];
3257
+ const count = counts.get(type) ?? 0;
3258
+ if (cap !== void 0 && count >= cap) {
3259
+ continue;
3260
+ }
3261
+ selected.push(item);
3262
+ admittedIndexes.add(index);
3263
+ if (type) {
3264
+ counts.set(type, count + 1);
3265
+ }
3266
+ }
3267
+ if (selected.length < normalizedLimit) {
3268
+ for (const [index, item] of browseResult.results.entries()) {
3269
+ if (selected.length >= normalizedLimit) {
3270
+ break;
3271
+ }
3272
+ if (admittedIndexes.has(index)) {
3273
+ continue;
3274
+ }
3275
+ selected.push(item);
3276
+ }
3277
+ }
3278
+ return {
3279
+ ...browseResult,
3280
+ results: selected
3281
+ };
3282
+ }
3283
+
2644
3284
  // src/openclaw-plugin/heartbeat.ts
2645
3285
  var HEARTBEAT_FILE_PATTERN = /\bHEARTBEAT\.md\b/i;
2646
3286
  function isHeartbeatPoll(message) {
@@ -2764,10 +3404,14 @@ var DEFAULT_MID_SESSION_RECALL_MODE = "hybrid";
2764
3404
  var DEFAULT_STORE_NUDGE_THRESHOLD = 8;
2765
3405
  var DEFAULT_STORE_NUDGE_MAX_PER_SESSION = 5;
2766
3406
  var DEFAULT_SESSION_START_BUDGET_CHARS = 12e3;
2767
- var SESSION_START_BROWSE_LIMIT = 10;
2768
- var SESSION_START_BROWSE_MIN_IMPORTANCE = 7;
2769
3407
  var SESSION_START_CORE_MAX_CHARS = 4e3;
3408
+ var SESSION_START_MEMORY_INDEX_MAX_CHARS = 1200;
3409
+ var SESSION_START_NON_CORE_MAX_ENTRIES = DEFAULT_SESSION_START_SECTION_CAPS.active + DEFAULT_SESSION_START_SECTION_CAPS.preferences + DEFAULT_SESSION_START_SECTION_CAPS.recent;
3410
+ var SESSION_START_RECENT_HEADER = "### State and Continuity";
3411
+ var SESSION_START_RECENT_SESSION_MAX_CHARS = 1200;
3412
+ var SESSION_START_SECTION_TRUNCATION_NOTICE = "\n[Section truncated]";
2770
3413
  var SESSION_START_TRUNCATION_NOTICE = "\n\n[Memory context truncated at budget]";
3414
+ var SESSION_START_MEMORY_BLOCK_HEADER = "## Recent memory\n";
2771
3415
  var SESSION_START_FORMAT_OPTIONS = {
2772
3416
  maxEntryChars: 500,
2773
3417
  maxChars: 5e3
@@ -2838,21 +3482,32 @@ function getRecallEntryDedupKey(item) {
2838
3482
  }
2839
3483
  return `content:${type}\0${subject}\0${content}`;
2840
3484
  }
2841
- function mergeSessionStartMemoryResults(coreResult, browseResult) {
2842
- const results = [
2843
- ...(coreResult?.results ?? []).map((item) => ({
2844
- ...item,
2845
- category: "core"
2846
- })),
2847
- ...browseResult?.results ?? []
2848
- ];
2849
- if (results.length === 0) {
2850
- return null;
3485
+ function capSectionText(text, maxChars, notice = SESSION_START_SECTION_TRUNCATION_NOTICE) {
3486
+ if (!text || maxChars <= 0) {
3487
+ return "";
3488
+ }
3489
+ if (text.length <= maxChars) {
3490
+ return text;
3491
+ }
3492
+ if (maxChars <= notice.length) {
3493
+ return "";
3494
+ }
3495
+ return `${text.slice(0, maxChars - notice.length).trimEnd()}${notice}`;
3496
+ }
3497
+ function pushCappedSection(blocks, block, hardCapChars) {
3498
+ if (!block) {
3499
+ return;
3500
+ }
3501
+ const remaining = hardCapChars - blocks.reduce((sum, existing, index) => {
3502
+ return sum + existing.length + (index > 0 ? 2 : 0);
3503
+ }, 0) - (blocks.length > 0 ? 2 : 0);
3504
+ if (remaining <= 0) {
3505
+ return;
3506
+ }
3507
+ const capped = capSectionText(block, remaining, SESSION_START_TRUNCATION_NOTICE);
3508
+ if (capped) {
3509
+ blocks.push(capped);
2851
3510
  }
2852
- return {
2853
- query: browseResult?.query ?? coreResult?.query ?? "[session-start]",
2854
- results
2855
- };
2856
3511
  }
2857
3512
  function formatProjectScope(projects) {
2858
3513
  const normalizedProjects = Array.from(
@@ -3198,7 +3853,7 @@ async function handleBeforePromptBuild(event, ctx, params) {
3198
3853
  const browseRecallOptions = {
3199
3854
  context: "browse",
3200
3855
  since: "30d",
3201
- limit: SESSION_START_BROWSE_LIMIT,
3856
+ limit: SESSION_START_BROWSE_PREFETCH_LIMIT,
3202
3857
  minImportance: SESSION_START_BROWSE_MIN_IMPORTANCE
3203
3858
  };
3204
3859
  if (!sessionStartBrowseProject) {
@@ -3256,6 +3911,12 @@ async function handleBeforePromptBuild(event, ctx, params) {
3256
3911
  return !key || !coreEntryKeys.has(key);
3257
3912
  });
3258
3913
  }
3914
+ if (browseResult) {
3915
+ browseResult.results = diversifySessionStartBrowseCandidates(
3916
+ browseResult,
3917
+ SESSION_START_BROWSE_CANDIDATE_LIMIT
3918
+ )?.results ?? [];
3919
+ }
3259
3920
  if (browseResult) {
3260
3921
  debugLog(
3261
3922
  params.debug,
@@ -3308,48 +3969,75 @@ async function handleBeforePromptBuild(event, ctx, params) {
3308
3969
  `filtered ${browseResult.results.length - browseResultForContext.results.length} session handoff entries after explicit clear`
3309
3970
  );
3310
3971
  }
3311
- const sessionStartMemory = mergeSessionStartMemoryResults(coreResult, browseResultForContext);
3972
+ const hardCapChars = resolveSessionStartBudgetChars(params.config);
3312
3973
  const sections = [];
3313
3974
  const sessionProjectSection = formatSessionProjectStateSection(sessionProjectResolution);
3314
3975
  if (sessionProjectSection) {
3315
- sections.push(sessionProjectSection);
3976
+ pushCappedSection(sections, sessionProjectSection, hardCapChars);
3316
3977
  }
3317
- if (previousTurns.trim()) {
3318
- sections.push(`## Recent session
3319
- ${previousTurns.trim()}`);
3978
+ const recentSessionBody = capSectionText(
3979
+ previousTurns.trim(),
3980
+ SESSION_START_RECENT_SESSION_MAX_CHARS
3981
+ );
3982
+ if (recentSessionBody) {
3983
+ pushCappedSection(sections, `## Recent session
3984
+ ${recentSessionBody}`, hardCapChars);
3320
3985
  }
3986
+ const memoryIndexSection = capSectionText(
3987
+ memoryIndexMarkdown?.trim() ?? "",
3988
+ SESSION_START_MEMORY_INDEX_MAX_CHARS
3989
+ );
3990
+ const sessionStartMemory = selectSessionStartMemory(
3991
+ coreResult,
3992
+ browseResultForContext,
3993
+ {
3994
+ maxNonCoreEntries: SESSION_START_NON_CORE_MAX_ENTRIES,
3995
+ recentSessionText: recentSessionBody
3996
+ }
3997
+ );
3998
+ const renderedStartupItems = [];
3321
3999
  if (sessionStartMemory) {
3322
- const formatted = formatRecallAsMarkdown(
3323
- sessionStartMemory,
3324
- {
3325
- ...SESSION_START_FORMAT_OPTIONS,
3326
- coreMaxChars: SESSION_START_CORE_MAX_CHARS,
3327
- includeRootHeader: false
3328
- }
4000
+ const reservedForMemoryIndex = memoryIndexSection ? memoryIndexSection.length + 2 : 0;
4001
+ const usedBeforeMemory = sections.reduce((sum, block, index) => {
4002
+ return sum + block.length + (index > 0 ? 2 : 0);
4003
+ }, 0);
4004
+ const availableForMemory = Math.max(
4005
+ 0,
4006
+ hardCapChars - usedBeforeMemory - reservedForMemoryIndex - (sections.length > 0 ? 2 : 0)
4007
+ );
4008
+ const memoryBodyBudget = Math.max(0, availableForMemory - SESSION_START_MEMORY_BLOCK_HEADER.length);
4009
+ const renderOptions = {
4010
+ ...SESSION_START_FORMAT_OPTIONS,
4011
+ maxChars: Math.min(SESSION_START_FORMAT_OPTIONS.maxChars ?? memoryBodyBudget, memoryBodyBudget),
4012
+ coreMaxChars: SESSION_START_CORE_MAX_CHARS,
4013
+ includeRootHeader: false,
4014
+ recentSectionHeader: SESSION_START_RECENT_HEADER
4015
+ };
4016
+ const formatted = formatRecallAsMarkdown(sessionStartMemory, renderOptions).trim();
4017
+ const renderRecallAsMarkdown2 = Reflect.get(
4018
+ recall_exports,
4019
+ "renderRecallAsMarkdown"
3329
4020
  );
3330
- if (formatted.trim()) {
3331
- sections.push(`## Recent memory
3332
- ${formatted.trim()}`);
4021
+ const renderedMemory = typeof renderRecallAsMarkdown2 === "function" ? renderRecallAsMarkdown2(sessionStartMemory, renderOptions) : {
4022
+ markdown: formatted,
4023
+ renderedItems: formatted ? sessionStartMemory.results : []
4024
+ };
4025
+ if (formatted) {
4026
+ pushCappedSection(sections, `${SESSION_START_MEMORY_BLOCK_HEADER}${formatted}`, hardCapChars);
4027
+ renderedStartupItems.push(...renderedMemory.renderedItems);
3333
4028
  }
3334
4029
  }
3335
- if (memoryIndexMarkdown?.trim()) {
3336
- sections.push(memoryIndexMarkdown);
4030
+ if (memoryIndexSection) {
4031
+ pushCappedSection(sections, memoryIndexSection, hardCapChars);
3337
4032
  }
3338
4033
  markdown = sections.length > 0 ? sections.join("\n\n") : void 0;
3339
- const hardCapChars = resolveSessionStartBudgetChars(params.config);
3340
4034
  if (markdown && markdown.length > hardCapChars) {
3341
4035
  const retainedLength = Math.max(0, hardCapChars - SESSION_START_TRUNCATION_NOTICE.length);
3342
4036
  markdown = `${markdown.slice(0, retainedLength)}${SESSION_START_TRUNCATION_NOTICE}`;
3343
4037
  }
3344
4038
  debugLog(params.debug, "session-start", `prependContext chars=${markdown?.length ?? 0}`);
3345
4039
  const recalledIds = /* @__PURE__ */ new Set();
3346
- for (const item of coreResult?.results ?? []) {
3347
- const id = getRecallEntryId(item);
3348
- if (id) {
3349
- recalledIds.add(id);
3350
- }
3351
- }
3352
- for (const item of browseResultForContext?.results ?? []) {
4040
+ for (const item of renderedStartupItems) {
3353
4041
  const id = getRecallEntryId(item);
3354
4042
  if (id) {
3355
4043
  recalledIds.add(id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenr",
3
- "version": "0.9.74",
3
+ "version": "0.9.75",
4
4
  "openclaw": {
5
5
  "extensions": [
6
6
  "dist/openclaw-plugin/index.js"