agenr 0.8.19 → 0.8.21

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.
@@ -7,8 +7,6 @@ import {
7
7
  } from "../chunk-L7VVZDWF.js";
8
8
 
9
9
  // src/openclaw-plugin/index.ts
10
- import os from "os";
11
- import path3 from "path";
12
10
  import { Type } from "@sinclair/typebox";
13
11
 
14
12
  // src/openclaw-plugin/recall.ts
@@ -33,7 +31,7 @@ function buildSpawnArgs(agenrPath) {
33
31
  }
34
32
  return { cmd: agenrPath, args: [] };
35
33
  }
36
- async function runRecall(agenrPath, budget, project, query) {
34
+ async function runRecall(agenrPath, budget, project, query, options) {
37
35
  return await new Promise((resolve) => {
38
36
  let stdout = "";
39
37
  let settled = false;
@@ -45,14 +43,17 @@ async function runRecall(agenrPath, budget, project, query) {
45
43
  settled = true;
46
44
  resolve(value);
47
45
  }
48
- const args = ["recall", "--context", "session-start", "--budget", String(budget), "--json"];
46
+ const isBrowse = options?.context === "browse";
47
+ const args = isBrowse ? ["recall", "--browse", "--since", options?.since ?? "1d", "--json"] : ["recall", "--context", "session-start", "--budget", String(budget), "--json"];
49
48
  if (project) {
50
49
  args.push("--project", project);
51
50
  }
52
- const trimmedQuery = query?.trim() ?? "";
53
- const truncatedQuery = trimmedQuery.length > RECALL_QUERY_MAX_CHARS ? trimmedQuery.slice(0, RECALL_QUERY_MAX_CHARS) : trimmedQuery;
54
- if (truncatedQuery) {
55
- args.push(truncatedQuery);
51
+ if (!isBrowse) {
52
+ const trimmedQuery = query?.trim() ?? "";
53
+ const truncatedQuery = trimmedQuery.length > RECALL_QUERY_MAX_CHARS ? trimmedQuery.slice(0, RECALL_QUERY_MAX_CHARS) : trimmedQuery;
54
+ if (truncatedQuery) {
55
+ args.push(truncatedQuery);
56
+ }
56
57
  }
57
58
  const child = spawn(spawnArgs.cmd, [...spawnArgs.args, ...args], {
58
59
  stdio: ["ignore", "pipe", "ignore"]
@@ -148,6 +149,8 @@ var SESSION_TOPIC_TTL_MS = 60 * 60 * 1e3;
148
149
  var SESSION_TOPIC_MIN_LENGTH = 40;
149
150
  var ARCHIVED_SESSION_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
150
151
  var SESSION_QUERY_LOOKBACK = 3;
152
+ var EXCHANGE_TEXT_MAX_CHARS = 200;
153
+ var EXCHANGE_USER_TURN_LIMIT = 5;
151
154
  var sessionTopicStash = /* @__PURE__ */ new Map();
152
155
  function isRecord(value) {
153
156
  return typeof value === "object" && value !== null;
@@ -179,9 +182,40 @@ function extractTextFromUserMessage(message) {
179
182
  }
180
183
  return textParts.join(" ").trim();
181
184
  }
185
+ function extractTextFromAssistantMessage(message) {
186
+ if (!isRecord(message) || message["role"] !== "assistant") {
187
+ return "";
188
+ }
189
+ const content = message["content"];
190
+ if (typeof content === "string") {
191
+ return content.trim();
192
+ }
193
+ if (!Array.isArray(content)) {
194
+ return "";
195
+ }
196
+ const textParts = [];
197
+ for (const part of content) {
198
+ if (!isRecord(part) || part["type"] !== "text") {
199
+ continue;
200
+ }
201
+ const partText = part["text"];
202
+ if (typeof partText !== "string") {
203
+ continue;
204
+ }
205
+ const trimmed = partText.trim();
206
+ if (trimmed) {
207
+ textParts.push(trimmed);
208
+ }
209
+ }
210
+ return textParts.join(" ").trim();
211
+ }
212
+ function truncateMessageText(text) {
213
+ return text.length > EXCHANGE_TEXT_MAX_CHARS ? text.slice(0, EXCHANGE_TEXT_MAX_CHARS) : text;
214
+ }
215
+ var OPENCLAW_BARE_RESET_PREFIX = "a new session was started via /new";
182
216
  function isThinPrompt(prompt) {
183
217
  const trimmed = prompt.trim().toLowerCase();
184
- return trimmed === "" || trimmed === "/new" || trimmed === "/reset";
218
+ return trimmed === "" || trimmed === "/new" || trimmed === "/reset" || trimmed.startsWith(OPENCLAW_BARE_RESET_PREFIX);
185
219
  }
186
220
  function stripPromptMetadata(raw) {
187
221
  if (!raw) {
@@ -221,56 +255,41 @@ function extractLastUserText(messages) {
221
255
  return "";
222
256
  }
223
257
  }
224
- async function readLatestArchivedUserMessages(sessionsDir, maxAgeMs = ARCHIVED_SESSION_MAX_AGE_MS) {
258
+ function extractLastExchangeText(messages, maxTurns = EXCHANGE_USER_TURN_LIMIT) {
225
259
  try {
226
- const now = Date.now();
227
- const archivedFiles = [];
228
- const entries = await readdir(sessionsDir, { withFileTypes: true });
229
- for (const entry of entries) {
230
- if (!entry.isFile() || !entry.name.includes(".reset.")) {
231
- continue;
232
- }
233
- const filePath = path2.join(sessionsDir, entry.name);
234
- const fileStats = await stat(filePath);
235
- if (now - fileStats.mtimeMs > maxAgeMs) {
236
- continue;
237
- }
238
- archivedFiles.push({ filePath, mtimeMs: fileStats.mtimeMs });
260
+ if (!Array.isArray(messages) || messages.length === 0) {
261
+ return "";
239
262
  }
240
- if (archivedFiles.length === 0) {
241
- return [];
263
+ if (maxTurns <= 0) {
264
+ return "";
242
265
  }
243
- archivedFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
244
- for (const candidate of archivedFiles) {
245
- const raw = await readFile(candidate.filePath, "utf8");
246
- const userMessages = [];
247
- for (const line of raw.split(/\r?\n/)) {
248
- const trimmed = line.trim();
249
- if (!trimmed) {
250
- continue;
251
- }
252
- let parsed;
253
- try {
254
- parsed = JSON.parse(trimmed);
255
- } catch {
256
- continue;
257
- }
258
- if (!isRecord(parsed) || parsed["type"] !== "message") {
259
- continue;
260
- }
261
- const text = extractTextFromUserMessage(parsed["message"]);
262
- if (!text) {
263
- continue;
266
+ const collected = [];
267
+ let collectedUserTurns = 0;
268
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
269
+ const message = messages[index];
270
+ const userText = extractTextFromUserMessage(message);
271
+ if (userText) {
272
+ collected.push({ role: "user", text: truncateMessageText(userText) });
273
+ collectedUserTurns += 1;
274
+ if (collectedUserTurns >= maxTurns) {
275
+ break;
264
276
  }
265
- userMessages.push(text);
277
+ continue;
266
278
  }
267
- if (userMessages.length > 0) {
268
- return userMessages.slice(-SESSION_QUERY_LOOKBACK);
279
+ const assistantText = extractTextFromAssistantMessage(message);
280
+ if (assistantText) {
281
+ collected.push({
282
+ role: "assistant",
283
+ text: truncateMessageText(assistantText)
284
+ });
269
285
  }
270
286
  }
271
- return [];
287
+ if (collected.length === 0) {
288
+ return "";
289
+ }
290
+ return collected.reverse().map((turn) => `${turn.role === "user" ? "U" : "A"}: ${turn.text}`).join(" | ");
272
291
  } catch {
273
- return [];
292
+ return "";
274
293
  }
275
294
  }
276
295
  function shouldStashTopic(text) {
@@ -923,6 +942,7 @@ var plugin = {
923
942
  name: "agenr memory context",
924
943
  description: "Injects agenr long-term memory into every agent session via before_prompt_build",
925
944
  register(api) {
945
+ const config = api.pluginConfig;
926
946
  api.on(
927
947
  "before_prompt_build",
928
948
  async (event, ctx) => {
@@ -934,7 +954,6 @@ var plugin = {
934
954
  if (shouldSkipSession(sessionKey)) {
935
955
  return;
936
956
  }
937
- const config = api.pluginConfig;
938
957
  if (config?.enabled === false) {
939
958
  return;
940
959
  }
@@ -950,33 +969,48 @@ var plugin = {
950
969
  const project = config?.project?.trim() || void 0;
951
970
  const userPrompt = stripPromptMetadata(event.prompt ?? "");
952
971
  const queryText = resolveSessionQuery(userPrompt, ctx.sessionKey);
953
- let recallQueryText = queryText;
954
- if (isFirstInSession && queryText === userPrompt && userPrompt.length < SESSION_TOPIC_MIN_LENGTH) {
955
- const agentId = ctx.agentId?.trim() || "main";
972
+ let recallResult;
973
+ const isColdStart = isFirstInSession && queryText === void 0;
974
+ if (isColdStart) {
956
975
  if (!ctx.agentId?.trim()) {
957
- api.logger.debug?.("[agenr] session-start: agentId not provided, defaulting to 'main'");
958
- }
959
- const sessionsDir = path3.join(
960
- os.homedir(),
961
- ".openclaw",
962
- "agents",
963
- agentId,
964
- "sessions"
965
- );
966
- const archivedMessages = await readLatestArchivedUserMessages(sessionsDir);
967
- if (archivedMessages.length > 0) {
968
- recallQueryText = `${archivedMessages.join(" ")} ${userPrompt}`;
969
- api.logger.info?.(
970
- `[agenr] session-start: archived session fallback applied, ${archivedMessages.length} messages`
971
- );
976
+ api.logger.debug?.("[agenr] cold-start: agentId not in ctx, defaulting to 'main'");
972
977
  }
978
+ recallResult = await runRecall(agenrPath, budget, project, void 0, {
979
+ context: "browse",
980
+ since: "1d"
981
+ });
982
+ } else {
983
+ recallResult = await runRecall(agenrPath, budget, project, queryText);
973
984
  }
974
- const recallResult = await runRecall(agenrPath, budget, project, recallQueryText);
975
985
  if (recallResult) {
976
986
  const formatted = formatRecallAsMarkdown(recallResult);
977
987
  if (formatted.trim()) {
978
988
  markdown = formatted;
979
989
  }
990
+ if (isColdStart) {
991
+ for (const item of recallResult.results) {
992
+ const entry = item.entry;
993
+ const subject = typeof entry.subject === "string" ? entry.subject : "";
994
+ if (!subject.toLowerCase().startsWith("session handoff")) {
995
+ continue;
996
+ }
997
+ const entryId = typeof entry.id === "string" && entry.id.trim() ? entry.id.trim() : null;
998
+ if (entryId) {
999
+ await runRetireTool(agenrPath, {
1000
+ entry_id: entryId,
1001
+ reason: "consumed at session start"
1002
+ }).catch((err) => {
1003
+ api.logger.debug?.(
1004
+ `[agenr] session-start: retire handoff ${entryId} failed: ${err instanceof Error ? err.message : String(err)}`
1005
+ );
1006
+ });
1007
+ } else {
1008
+ api.logger.debug?.(
1009
+ "[agenr] session-start: handoff entry missing id, skipping retire"
1010
+ );
1011
+ }
1012
+ }
1013
+ }
980
1014
  }
981
1015
  }
982
1016
  let signal;
@@ -1021,10 +1055,35 @@ var plugin = {
1021
1055
  return;
1022
1056
  }
1023
1057
  const lastUserText = extractLastUserText(messages);
1024
- if (!shouldStashTopic(lastUserText)) {
1025
- return;
1058
+ if (shouldStashTopic(lastUserText)) {
1059
+ stashSessionTopic(sessionKey, lastUserText);
1060
+ }
1061
+ const handoffText = extractLastExchangeText(messages);
1062
+ if (handoffText) {
1063
+ const agenrPath = resolveAgenrPath(config);
1064
+ const defaultProject = config?.project?.trim() || void 0;
1065
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ");
1066
+ const handoffEntry = {
1067
+ entries: [
1068
+ {
1069
+ type: "event",
1070
+ importance: 10,
1071
+ subject: `session handoff ${timestamp}`,
1072
+ content: handoffText,
1073
+ tags: ["handoff", "session"]
1074
+ }
1075
+ ]
1076
+ };
1077
+ const storeConfig = {
1078
+ ...config,
1079
+ logger: api.logger
1080
+ };
1081
+ runStoreTool(agenrPath, handoffEntry, storeConfig, defaultProject).catch((err) => {
1082
+ api.logger.debug?.(
1083
+ `[agenr] before_reset: handoff store failed: ${err instanceof Error ? err.message : String(err)}`
1084
+ );
1085
+ });
1026
1086
  }
1027
- stashSessionTopic(sessionKey, lastUserText);
1028
1087
  } catch (err) {
1029
1088
  api.logger.warn(
1030
1089
  `agenr plugin before_reset stash failed: ${err instanceof Error ? err.message : String(err)}`
@@ -1032,7 +1091,6 @@ var plugin = {
1032
1091
  }
1033
1092
  });
1034
1093
  if (api.registerTool) {
1035
- const config = api.pluginConfig;
1036
1094
  if (config?.enabled === false) {
1037
1095
  return;
1038
1096
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenr",
3
- "version": "0.8.19",
3
+ "version": "0.8.21",
4
4
  "openclaw": {
5
5
  "extensions": [
6
6
  "dist/openclaw-plugin/index.js"
@@ -11,6 +11,13 @@
11
11
  "bin": {
12
12
  "agenr": "dist/cli.js"
13
13
  },
14
+ "scripts": {
15
+ "build": "tsup src/cli.ts src/cli-main.ts src/openclaw-plugin/index.ts --format esm --dts",
16
+ "dev": "tsup src/cli.ts src/cli-main.ts --format esm --watch",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
19
+ "typecheck": "tsc --noEmit"
20
+ },
14
21
  "dependencies": {
15
22
  "@clack/prompts": "^1.0.1",
16
23
  "@libsql/client": "^0.17.0",
@@ -54,11 +61,9 @@
54
61
  "README.md"
55
62
  ],
56
63
  "author": "agenr-ai",
57
- "scripts": {
58
- "build": "tsup src/cli.ts src/cli-main.ts src/openclaw-plugin/index.ts --format esm --dts",
59
- "dev": "tsup src/cli.ts src/cli-main.ts --format esm --watch",
60
- "test": "vitest run",
61
- "test:watch": "vitest",
62
- "typecheck": "tsc --noEmit"
64
+ "pnpm": {
65
+ "overrides": {
66
+ "fast-xml-parser": "^5.3.6"
67
+ }
63
68
  }
64
- }
69
+ }