agenr 0.8.8 → 0.8.10

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,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.10]
4
+
5
+ ### Added
6
+ - feat(plugin): session-start recall now uses the inbound user message as the recall query seed, enabling vector similarity scoring instead of pure recency ranking; entries relevant to the actual conversation topic now surface at session start (issues #177, #178)
7
+ - feat(plugin): before_reset hook captures the last 3 substantive user messages before a /new reset and stashes them in memory; the next session-start recall uses the stash as its query seed when the opening prompt is low-signal (issues #177, #178)
8
+ - feat(plugin): session topic stash eviction sweep runs every 5 minutes; TTL is 1 hour
9
+
10
+ ### Changed
11
+ - chore(plugin): session-start recall timeout increased from 5s to 10s to accommodate the embedding API call now required when a query is present
12
+ - chore(plugin): session topic stash requires a minimum of 40 characters and 5 words to filter out low-signal conversational closers
13
+ - refactor(plugin): session query helpers extracted from index.ts into session-query.ts
14
+
15
+ ### Fixed
16
+ - fix(plugin): session-start recall no longer skips vector similarity scoring when a query is available; previously RecallQuery.text was always undefined at session start (issue #177)
17
+
18
+ ## [0.8.9]
19
+
20
+ ### Added
21
+ - feat(extractor): broadened extraction prompt to capture personal user context (health, diet, family, occupation, location, values) even from casual or passing mentions; added 6-month durability test heuristic to distinguish durable personal facts from transient states (issue #173)
22
+ - feat(extractor): new few-shot examples for RELATIONSHIP, PREFERENCE, FACT, and EVENT types covering personal context scenarios with scoring rationale
23
+
24
+ ### Fixed
25
+ - fix(ingest): suppress redundant whole-file ignored-params warning; now fires once per ingest run via shared ExtractRunOnceFlags object instead of once per file (issue #168)
26
+ - fix(ingest): silence SQLITE_ERROR vector-index-not-found pre-fetch error during bulk ingest when vector index is intentionally absent; all other pre-fetch errors still log (issue #168)
27
+ - fix(ingest): detect .jsonl.reset.TIMESTAMP session files as JSONL adapter by extending suffix-stripping regex to handle both .deleted and .reset suffixes (issue #169)
28
+ - fix(consolidate): added merge system prompt constraint that expiry must be exactly permanent or temporary, never a date or timestamp; complements existing runtime fallback (issue #172)
29
+ - fix(daemon): daemon install plist now uses the runtime CLI path resolved from argv[1] via the injected argvFn, preventing hardcoded npm global paths from breaking pnpm installs (issue #174)
30
+
3
31
  ## [0.8.8]
4
32
 
5
33
  ### Fixed
@@ -170,6 +170,16 @@ interface ExtractChunkCompleteResult {
170
170
  durationMs?: number;
171
171
  warnings: string[];
172
172
  }
173
+ /**
174
+ * Shared mutable state object passed across multiple extractKnowledgeFromChunks
175
+ * calls (e.g., across files in a single ingest run) to suppress duplicate
176
+ * one-time warnings. When undefined, all warnings fire on every call,
177
+ * preserving backward-compatible behavior for callers that do not share state.
178
+ */
179
+ interface ExtractRunOnceFlags {
180
+ /** Set to true after the whole-file ignored-params warning fires once. */
181
+ hasWarnedWholeFileIgnoredParams?: boolean;
182
+ }
173
183
  declare function extractKnowledgeFromChunks(params: {
174
184
  file: string;
175
185
  chunks: TranscriptChunk[];
@@ -192,6 +202,8 @@ declare function extractKnowledgeFromChunks(params: {
192
202
  embeddingApiKey?: string;
193
203
  noPreFetch?: boolean;
194
204
  embedFn?: (texts: string[], apiKey: string) => Promise<number[][]>;
205
+ /** Shared warning state across calls; pass one object per ingest run to dedupe one-time logs. */
206
+ onceFlags?: ExtractRunOnceFlags;
195
207
  }): Promise<ExtractChunksResult>;
196
208
 
197
209
  interface ResolveLlmClientInput {
package/dist/cli-main.js CHANGED
@@ -7088,7 +7088,8 @@ function buildMergeContext(cluster) {
7088
7088
  "Merge the provided related entries into one canonical entry.",
7089
7089
  "Only include information explicitly stated in the source entries. Do not infer or add details not present.",
7090
7090
  "Prefer preserving temporal changes in the merged narrative.",
7091
- "Call merge_entries with your final merged result."
7091
+ "Call merge_entries with your final merged result.",
7092
+ 'expiry must be exactly the string "permanent" or "temporary" -- never a date, timestamp, or other value.'
7092
7093
  ].join("\n");
7093
7094
  let contentLimit;
7094
7095
  let payload = formatClusterEntries(cluster, contentLimit);
@@ -11164,7 +11165,7 @@ var vscodeCopilotAdapter = {
11164
11165
 
11165
11166
  // src/adapters/registry.ts
11166
11167
  async function detectAdapter(filePath) {
11167
- const strippedPath = filePath.replace(/\.deleted\.[^/\\]+$/, "");
11168
+ const strippedPath = filePath.replace(/\.(deleted|reset)\.[^/\\]+$/, "");
11168
11169
  const ext = path23.extname(strippedPath).toLowerCase();
11169
11170
  if (ext === ".jsonl") {
11170
11171
  const firstLine = await readFirstNonEmptyLine(filePath);
@@ -11725,15 +11726,15 @@ var SUBMIT_DEDUPED_KNOWLEDGE_TOOL = {
11725
11726
  // src/extractor.ts
11726
11727
  var SYSTEM_PROMPT = `You are a selective memory extraction engine. Extract only knowledge worth remembering beyond the immediate step.
11727
11728
 
11728
- Default action: SKIP. Most chunks should produce zero entries.
11729
+ Default action: SKIP. Most chunks should produce zero entries. Personal disclosures (health, diet, family structure, pets, occupation, location, values, relationships) are high-signal -- do not skip them just because the mention is brief or incidental. Apply the durability gate below to all entries including personal ones.
11729
11730
 
11730
11731
  ## Types
11731
11732
 
11732
- FACT \u2014 Verifiable information about a system, project, person, or concept.
11733
+ FACT \u2014 Verifiable information about a system, project, person, or concept. Personal facts count as first-class entries: health conditions, family members, pets, where the user lives or works, occupation, hobbies, recurring habits, and lifestyle circumstances.
11733
11734
  DECISION \u2014 A choice that constrains future options. Requires BOTH the choice AND the rationale. If rationale is missing, use fact or event instead.
11734
11735
  PREFERENCE \u2014 A stated or demonstrated preference that should influence future behavior.
11735
11736
  LESSON \u2014 An insight from experience that should change future behavior.
11736
- EVENT \u2014 A significant milestone, launch, or completion. NOT "the assistant ran git status."
11737
+ EVENT \u2014 A significant milestone, launch, completion, or one-time life moment. Includes project launches, deployments, merges; and personal milestones like starting a new job, relocating, major health events, or notable personal experiences. NOT recurring habits or routines (those are FACT or PREFERENCE). NOT "the assistant ran git status." NOT vague references with no anchoring detail. If a personal behavior is described as newly begun, prefer EVENT for the initiation; use FACT if the stable ongoing state is also established in the same context.
11737
11738
  RELATIONSHIP \u2014 A connection between named entities. Content must include both entities and the relation.
11738
11739
  TODO \u2014 A persistent future action not completed in this chunk and not a one-step session instruction.
11739
11740
 
@@ -11756,7 +11757,7 @@ Do NOT emit a completion event for:
11756
11757
  ## Durability Gate
11757
11758
 
11758
11759
  Only extract if useful in future conversations/tasks after the current immediate execution.
11759
- If uncertain whether durable, skip.
11760
+ If uncertain whether durable, apply the 6-month test: would knowing this fact 6 months from now still help an AI assist this person? Health conditions, family structure, location, diet, pets, occupation, values -> YES, extract. Current mood, today's plans, this week's weather -> NO, skip. Personal disclosures are durable by default -- if a user reveals something true about themselves, capture it even if mentioned casually or as an aside.
11760
11761
 
11761
11762
  ## Importance (1-10)
11762
11763
 
@@ -11843,8 +11844,9 @@ If you cannot name a concrete topic, skip the entry.
11843
11844
  5. Code-level implementation details likely to churn (unless architecture-level decision)
11844
11845
  6. One workflow split into multiple near-duplicate entries \u2014 merge into one
11845
11846
  7. Minor rephrases/duplicates of another extracted entry
11846
- 8. Greetings, acknowledgments, small talk
11847
+ 8. Pure pleasantries with no personal content: greetings, acknowledgments, filler phrases that reveal nothing about the user ("how are you", "thanks", "sounds good", "got it"). Do NOT suppress personal disclosures just because they appear inside casual phrasing or as asides. "I follow keto so no carbs -- anyway, back to the project" contains an extractable preference even though the user pivoted away. The pivot does not cancel the fact. Test: does the underlying disclosure reveal something true and durable about the user? If yes, extract it.
11847
11848
  9. Transient implementation status unless it represents a milestone, decision, or lesson
11849
+ 10. Transient personal states: current mood, today's fatigue, this week's busyness, temporary travel plans, passing weather references. These are not personal facts. Extract only if the condition is recurring or structural ("I'm always exhausted" may indicate a chronic health pattern worth capturing; "I'm tired today" does not).
11848
11850
 
11849
11851
  ## Explicit Memory Requests
11850
11852
 
@@ -11949,6 +11951,17 @@ RELATIONSHIP:
11949
11951
  "source_context": "Architecture discussion about memory integration"
11950
11952
  }
11951
11953
 
11954
+ RELATIONSHIP:
11955
+ {
11956
+ "type": "relationship",
11957
+ "subject": "user and sister Sarah",
11958
+ "content": "User's sister Sarah lives nearby and they see each other regularly. She helps with childcare.",
11959
+ "importance": 7,
11960
+ "expiry": "permanent",
11961
+ "tags": ["family", "personal", "relationship"],
11962
+ "source_context": "User mentioned sister while discussing weekend plans"
11963
+ }
11964
+
11952
11965
  TODO:
11953
11966
  {
11954
11967
  "type": "todo",
@@ -12015,6 +12028,39 @@ PREFERENCE:
12015
12028
  "source_context": "User mentioned scheduling preference during calendar discussion -- scored 6 not 8 because no parallel session needs to act on this immediately; it is a low-urgency convenience preference"
12016
12029
  }
12017
12030
 
12031
+ PREFERENCE:
12032
+ {
12033
+ "type": "preference",
12034
+ "subject": "user dietary preference",
12035
+ "content": "User follows a strict ketogenic diet and avoids carbohydrates. Do not suggest high-carb meals, recipes, or foods.",
12036
+ "importance": 7,
12037
+ "expiry": "permanent",
12038
+ "tags": ["diet", "keto", "personal", "health"],
12039
+ "source_context": "User mentioned diet as an aside mid-conversation -- scored 7 because future sessions about food, health, or restaurants need this; the casual phrasing does not reduce its durability"
12040
+ }
12041
+
12042
+ FACT:
12043
+ {
12044
+ "type": "fact",
12045
+ "subject": "user morning routine",
12046
+ "content": "User wakes at 6:15 AM and goes to the gym every weekday morning before work.",
12047
+ "importance": 6,
12048
+ "expiry": "permanent",
12049
+ "tags": ["routine", "health", "personal"],
12050
+ "source_context": "User described morning routine while discussing their schedule -- scored 6 not 8 because this is low-urgency biographical context; no parallel session needs to act on it immediately"
12051
+ }
12052
+
12053
+ EVENT:
12054
+ {
12055
+ "type": "event",
12056
+ "subject": "user job change",
12057
+ "content": "User started a new senior engineering role at a new company. This is a recent career change.",
12058
+ "importance": 7,
12059
+ "expiry": "permanent",
12060
+ "tags": ["career", "work", "personal"],
12061
+ "source_context": "User mentioned new job while discussing their schedule -- scored 7 not 9 because this is a significant life milestone worth preserving but does not require immediate cross-session action"
12062
+ }
12063
+
12018
12064
  // NOTE: These examples are drawn from OpenClaw transcripts (agent role labels, tool-verified claims). They provide soft cross-platform guidance for hedged-claim handling; mechanical enforcement (importance cap + unverified tag) is applied for openclaw, codex, and claude-code via applyConfidenceCap().
12019
12065
  Example: hedged agent claim (correct handling):
12020
12066
  {
@@ -12459,6 +12505,10 @@ var ParseResponseError = class extends Error {
12459
12505
  function normalize3(value) {
12460
12506
  return value.trim().toLowerCase();
12461
12507
  }
12508
+ function isVectorIndexNotFoundMessage(message) {
12509
+ const normalized = message.toLowerCase();
12510
+ return normalized.includes("vector index") && normalized.includes("not found");
12511
+ }
12462
12512
  async function preFetchRelated(chunkText2, db, embeddingApiKey, embedFn = embed, onVerbose) {
12463
12513
  const run = async () => {
12464
12514
  try {
@@ -12490,7 +12540,11 @@ async function preFetchRelated(chunkText2, db, embeddingApiKey, embedFn = embed,
12490
12540
  onVerbose?.(`[pre-fetch] ${above.length} above threshold ${PREFETCH_SIMILARITY_THRESHOLD}`);
12491
12541
  return above.slice(0, MAX_PREFETCH_RESULTS).map((candidate) => candidate.entry);
12492
12542
  } catch (error) {
12493
- onVerbose?.(`[pre-fetch] skipped: ${error instanceof Error ? error.message : String(error)}`);
12543
+ const message = error instanceof Error ? error.message : String(error);
12544
+ if (isVectorIndexNotFoundMessage(message)) {
12545
+ return [];
12546
+ }
12547
+ onVerbose?.(`[pre-fetch] skipped: ${message}`);
12494
12548
  return [];
12495
12549
  }
12496
12550
  };
@@ -13251,10 +13305,15 @@ async function extractKnowledgeFromChunks(params) {
13251
13305
  let effectiveNoPreFetch = wholeFileMode ? true : params.noPreFetch ?? false;
13252
13306
  if (wholeFileMode && params.verbose) {
13253
13307
  const ignoredParamsLine = "[whole-file] interChunkDelayMs and llmConcurrency have no effect in whole-file mode";
13254
- if (params.onVerbose) {
13255
- params.onVerbose(ignoredParamsLine);
13256
- } else {
13257
- console.warn(ignoredParamsLine);
13308
+ if (!params.onceFlags?.hasWarnedWholeFileIgnoredParams) {
13309
+ if (params.onVerbose) {
13310
+ params.onVerbose(ignoredParamsLine);
13311
+ } else {
13312
+ console.warn(ignoredParamsLine);
13313
+ }
13314
+ if (params.onceFlags) {
13315
+ params.onceFlags.hasWarnedWholeFileIgnoredParams = true;
13316
+ }
13258
13317
  }
13259
13318
  }
13260
13319
  if (wholeFileMode) {
@@ -14545,6 +14604,7 @@ async function runIngestCommand(inputPaths, options, deps) {
14545
14604
  let totalChunksFailed = 0;
14546
14605
  let filesWithChunkFailures = 0;
14547
14606
  const chunkStatsByFile = /* @__PURE__ */ new Map();
14607
+ const extractOnceFlags = { hasWarnedWholeFileIgnoredParams: false };
14548
14608
  let firstPassFailedIndexSet = /* @__PURE__ */ new Set();
14549
14609
  let bulkTeardownComplete = false;
14550
14610
  let bulkVectorRebuildDurationSeconds = null;
@@ -14699,6 +14759,7 @@ async function runIngestCommand(inputPaths, options, deps) {
14699
14759
  db: options.noPreFetch ? void 0 : db,
14700
14760
  embeddingApiKey: options.noPreFetch ? void 0 : embeddingApiKey ?? void 0,
14701
14761
  noPreFetch: options.noPreFetch === true,
14762
+ onceFlags: extractOnceFlags,
14702
14763
  onVerbose: verbose ? (line) => {
14703
14764
  clack4.log.info(line, clackOutput);
14704
14765
  } : void 0,
@@ -23,6 +23,12 @@ type BeforePromptBuildResult = {
23
23
  systemPrompt?: string;
24
24
  prependContext?: string;
25
25
  };
26
+ type BeforeResetEvent = {
27
+ sessionFile?: string;
28
+ messages?: unknown[];
29
+ reason?: string;
30
+ [key: string]: unknown;
31
+ };
26
32
  type PluginLogger = {
27
33
  debug?: (message: string) => void;
28
34
  info?: (message: string) => void;
@@ -58,6 +64,7 @@ type PluginApi = {
58
64
  on: {
59
65
  (hook: "before_agent_start", handler: (event: BeforeAgentStartEvent, ctx: PluginHookAgentContext) => Promise<BeforeAgentStartResult | undefined> | BeforeAgentStartResult | undefined): void;
60
66
  (hook: "before_prompt_build", handler: (event: BeforePromptBuildEvent, ctx: PluginHookAgentContext) => Promise<BeforePromptBuildResult | undefined> | BeforePromptBuildResult | undefined): void;
67
+ (hook: "before_reset", handler: (event: BeforeResetEvent, ctx: PluginHookAgentContext) => Promise<void> | void): void;
61
68
  };
62
69
  };
63
70
 
@@ -67,5 +74,9 @@ declare const plugin: {
67
74
  description: string;
68
75
  register(api: PluginApi): void;
69
76
  };
77
+ declare const __testing: {
78
+ SESSION_TOPIC_TTL_MS: number;
79
+ clearState(): void;
80
+ };
70
81
 
71
- export { plugin as default };
82
+ export { __testing, plugin as default };
@@ -13,7 +13,8 @@ import { Type } from "@sinclair/typebox";
13
13
  import { spawn } from "child_process";
14
14
  import path from "path";
15
15
  import { fileURLToPath } from "url";
16
- var RECALL_TIMEOUT_MS = 5e3;
16
+ var RECALL_TIMEOUT_MS = 1e4;
17
+ var RECALL_QUERY_MAX_CHARS = 500;
17
18
  var DEFAULT_BUDGET = 2e3;
18
19
  var MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
19
20
  var PACKAGE_ROOT = path.resolve(MODULE_DIR, "..", "..");
@@ -30,7 +31,7 @@ function buildSpawnArgs(agenrPath) {
30
31
  }
31
32
  return { cmd: agenrPath, args: [] };
32
33
  }
33
- async function runRecall(agenrPath, budget, project) {
34
+ async function runRecall(agenrPath, budget, project, query) {
34
35
  return await new Promise((resolve) => {
35
36
  let stdout = "";
36
37
  let settled = false;
@@ -46,6 +47,11 @@ async function runRecall(agenrPath, budget, project) {
46
47
  if (project) {
47
48
  args.push("--project", project);
48
49
  }
50
+ const trimmedQuery = query?.trim() ?? "";
51
+ const truncatedQuery = trimmedQuery.length > RECALL_QUERY_MAX_CHARS ? trimmedQuery.slice(0, RECALL_QUERY_MAX_CHARS) : trimmedQuery;
52
+ if (truncatedQuery) {
53
+ args.push(truncatedQuery);
54
+ }
49
55
  const child = spawn(spawnArgs.cmd, [...spawnArgs.args, ...args], {
50
56
  stdio: ["ignore", "pipe", "ignore"]
51
57
  });
@@ -133,6 +139,127 @@ function formatRecallAsMarkdown(result) {
133
139
  return lines.join("\n").trimEnd();
134
140
  }
135
141
 
142
+ // src/openclaw-plugin/session-query.ts
143
+ var SESSION_TOPIC_TTL_MS = 60 * 60 * 1e3;
144
+ var SESSION_TOPIC_MIN_LENGTH = 40;
145
+ var SESSION_QUERY_LOOKBACK = 3;
146
+ var sessionTopicStash = /* @__PURE__ */ new Map();
147
+ function isRecord(value) {
148
+ return typeof value === "object" && value !== null;
149
+ }
150
+ function extractTextFromUserMessage(message) {
151
+ if (!isRecord(message) || message["role"] !== "user") {
152
+ return "";
153
+ }
154
+ const content = message["content"];
155
+ if (typeof content === "string") {
156
+ return content.trim();
157
+ }
158
+ if (!Array.isArray(content)) {
159
+ return "";
160
+ }
161
+ const textParts = [];
162
+ for (const part of content) {
163
+ if (!isRecord(part) || part["type"] !== "text") {
164
+ continue;
165
+ }
166
+ const partText = part["text"];
167
+ if (typeof partText !== "string") {
168
+ continue;
169
+ }
170
+ const trimmed = partText.trim();
171
+ if (trimmed) {
172
+ textParts.push(trimmed);
173
+ }
174
+ }
175
+ return textParts.join(" ").trim();
176
+ }
177
+ function isThinPrompt(prompt) {
178
+ const trimmed = prompt.trim().toLowerCase();
179
+ return trimmed === "" || trimmed === "/new" || trimmed === "/reset";
180
+ }
181
+ function extractLastUserText(messages) {
182
+ try {
183
+ const collected = [];
184
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
185
+ const extracted = extractTextFromUserMessage(messages[index]);
186
+ if (!extracted) {
187
+ continue;
188
+ }
189
+ collected.push(extracted);
190
+ if (collected.length >= SESSION_QUERY_LOOKBACK) {
191
+ break;
192
+ }
193
+ }
194
+ const joined = collected.reverse().join(" ").trim();
195
+ return joined || "";
196
+ } catch {
197
+ return "";
198
+ }
199
+ }
200
+ function shouldStashTopic(text) {
201
+ if (text.length < SESSION_TOPIC_MIN_LENGTH) {
202
+ return false;
203
+ }
204
+ const wordCount = text.split(/\s+/).filter(Boolean).length;
205
+ return wordCount >= 5;
206
+ }
207
+ function sweepExpiredStash() {
208
+ const now = Date.now();
209
+ for (const [key, entry] of sessionTopicStash) {
210
+ if (now - entry.storedAt > SESSION_TOPIC_TTL_MS) {
211
+ sessionTopicStash.delete(key);
212
+ }
213
+ }
214
+ }
215
+ var sweepInterval = setInterval(sweepExpiredStash, 5 * 60 * 1e3);
216
+ if (sweepInterval !== void 0 && typeof sweepInterval.unref === "function") {
217
+ sweepInterval.unref();
218
+ }
219
+ function stripResetPrefix(prompt) {
220
+ const lower = prompt.toLowerCase();
221
+ for (const cmd of ["/new", "/reset"]) {
222
+ if (lower.startsWith(cmd + " ")) {
223
+ return prompt.slice(cmd.length).trim();
224
+ }
225
+ }
226
+ return prompt;
227
+ }
228
+ function resolveSessionQuery(prompt, sessionKey) {
229
+ let stashedText;
230
+ if (sessionKey) {
231
+ const entry = sessionTopicStash.get(sessionKey);
232
+ if (entry) {
233
+ sessionTopicStash.delete(sessionKey);
234
+ const expired = Date.now() - entry.storedAt > SESSION_TOPIC_TTL_MS;
235
+ if (!expired && entry.text.length > 0) {
236
+ stashedText = entry.text;
237
+ }
238
+ }
239
+ }
240
+ const normalized = (prompt ?? "").trim();
241
+ if (!isThinPrompt(normalized)) {
242
+ return stripResetPrefix(normalized);
243
+ }
244
+ return stashedText;
245
+ }
246
+ function stashSessionTopic(sessionKey, text) {
247
+ if (!shouldStashTopic(text)) {
248
+ return;
249
+ }
250
+ sessionTopicStash.set(sessionKey, {
251
+ text,
252
+ storedAt: Date.now()
253
+ });
254
+ }
255
+ function clearStash() {
256
+ sessionTopicStash.clear();
257
+ if (sweepInterval !== void 0) {
258
+ clearInterval(sweepInterval);
259
+ sweepInterval = void 0;
260
+ }
261
+ }
262
+
136
263
  // src/db/signals.ts
137
264
  async function fetchNewSignalEntries(db, sinceSeq, minImportance, limit, maxAgeSec = 0) {
138
265
  const ageArgs = maxAgeSec > 0 ? [minImportance, sinceSeq, new Date(Date.now() - maxAgeSec * 1e3).toISOString(), limit] : [minImportance, sinceSeq, limit];
@@ -712,7 +839,7 @@ var plugin = {
712
839
  register(api) {
713
840
  api.on(
714
841
  "before_prompt_build",
715
- async (_event, ctx) => {
842
+ async (event, ctx) => {
716
843
  try {
717
844
  const sessionKey = ctx.sessionKey ?? "";
718
845
  if (!sessionKey) {
@@ -735,7 +862,8 @@ var plugin = {
735
862
  const agenrPath = resolveAgenrPath(config);
736
863
  const budget = resolveBudget(config);
737
864
  const project = config?.project?.trim() || void 0;
738
- const recallResult = await runRecall(agenrPath, budget, project);
865
+ const queryText = resolveSessionQuery(event.prompt, ctx.sessionKey);
866
+ const recallResult = await runRecall(agenrPath, budget, project, queryText);
739
867
  if (recallResult) {
740
868
  const formatted = formatRecallAsMarkdown(recallResult);
741
869
  if (formatted.trim()) {
@@ -774,6 +902,27 @@ var plugin = {
774
902
  }
775
903
  }
776
904
  );
905
+ api.on("before_reset", (event, ctx) => {
906
+ try {
907
+ const sessionKey = ctx.sessionKey;
908
+ if (!sessionKey) {
909
+ return;
910
+ }
911
+ const messages = event.messages;
912
+ if (!Array.isArray(messages) || messages.length === 0) {
913
+ return;
914
+ }
915
+ const lastUserText = extractLastUserText(messages);
916
+ if (!shouldStashTopic(lastUserText)) {
917
+ return;
918
+ }
919
+ stashSessionTopic(sessionKey, lastUserText);
920
+ } catch (err) {
921
+ api.logger.warn(
922
+ `agenr plugin before_reset stash failed: ${err instanceof Error ? err.message : String(err)}`
923
+ );
924
+ }
925
+ });
777
926
  if (api.registerTool) {
778
927
  const config = api.pluginConfig;
779
928
  if (config?.enabled === false) {
@@ -924,7 +1073,16 @@ var plugin = {
924
1073
  }
925
1074
  }
926
1075
  };
1076
+ var __testing = {
1077
+ SESSION_TOPIC_TTL_MS,
1078
+ clearState() {
1079
+ clearStash();
1080
+ seenSessions.clear();
1081
+ sessionSignalState.clear();
1082
+ }
1083
+ };
927
1084
  var openclaw_plugin_default = plugin;
928
1085
  export {
1086
+ __testing,
929
1087
  openclaw_plugin_default as default
930
1088
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenr",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
4
4
  "openclaw": {
5
5
  "extensions": [
6
6
  "dist/openclaw-plugin/index.js"