agenr 0.6.7 → 0.6.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,10 +1,32 @@
1
+ # Changelog
2
+
3
+ ## [0.6.10] - 2026-02-19
4
+
5
+ ### Fixed
6
+ - OpenClaw plugin: AGENR.md now writes a compact summary (subjects only + entry count + recall instructions) instead of full content, preventing double-injection of full context if loaded into Project Context
7
+ - Note: version 0.6.9 was published with a stale build and unpublished; 0.6.10 is the correct release of these changes
8
+
9
+ ## [0.6.9] - 2026-02-19
10
+
11
+ ### Fixed
12
+ - OpenClaw plugin: session-seen guard prevents recall firing on every turn (fires once per session)
13
+ - OpenClaw plugin: sessionKey now read from ctx (second handler arg) instead of event
14
+ - OpenClaw plugin: DEFAULT_AGENR_PATH uses correct 2-level relative path to dist/cli.js
15
+ - OpenClaw plugin: spawn strategy detects .js vs executable binary
16
+
17
+ ### Added
18
+ - OpenClaw plugin: writes AGENR.md to ctx.workspaceDir after successful recall (fire-and-forget)
19
+
20
+ ## [0.6.8] - 2026-02-19
21
+
22
+ ### Fixed
23
+ - fix(openclaw-plugin): OpenClaw plugin now uses api.on("before_agent_start") instead of api.registerHook("agent:bootstrap"). The previous approach registered the handler in the gateway bundle's internal handlers map, which is a different module instance from the embedded agent runner. The typed hook system (api.on) uses the shared global plugin registry and works correctly across both bundles.
24
+
1
25
  ## [0.6.7] - 2026-02-19
2
26
 
3
27
  ### Fixed
4
28
  - fix(openclaw-plugin): add name and description to registerHook opts to resolve OpenClaw hook registration warning
5
29
 
6
- # Changelog
7
-
8
30
  ## [0.6.6] - 2026-02-19
9
31
 
10
32
  ### Added
package/dist/cli-main.js CHANGED
@@ -12451,7 +12451,22 @@ function resolveWatcherPidPath(configDir) {
12451
12451
  async function writeWatcherPid(configDir) {
12452
12452
  const pidPath = resolveWatcherPidPath(configDir);
12453
12453
  await fs26.mkdir(path25.dirname(pidPath), { recursive: true });
12454
- await fs26.writeFile(pidPath, String(process.pid), "utf8");
12454
+ const tmpPath = `${pidPath}.${process.pid}.${Date.now()}.tmp`;
12455
+ let handle = null;
12456
+ try {
12457
+ handle = await fs26.open(tmpPath, "w");
12458
+ await handle.writeFile(String(process.pid), "utf8");
12459
+ await handle.sync();
12460
+ await handle.close();
12461
+ handle = null;
12462
+ await fs26.rename(tmpPath, pidPath);
12463
+ } catch (error) {
12464
+ if (handle) {
12465
+ await handle.close().catch(() => void 0);
12466
+ }
12467
+ await fs26.unlink(tmpPath).catch(() => void 0);
12468
+ throw error;
12469
+ }
12455
12470
  }
12456
12471
  async function deleteWatcherPid(configDir) {
12457
12472
  try {
@@ -12778,6 +12793,8 @@ async function runIngestCommand(inputPaths, options, deps) {
12778
12793
  loadWatchStateFn: deps?.loadWatchStateFn ?? loadWatchState,
12779
12794
  saveWatchStateFn: deps?.saveWatchStateFn ?? saveWatchState,
12780
12795
  isWatcherRunningFn: deps?.isWatcherRunningFn ?? isWatcherRunning,
12796
+ readWatcherPidFn: deps?.readWatcherPidFn ?? readWatcherPid,
12797
+ resolveWatcherPidPathFn: deps?.resolveWatcherPidPathFn ?? resolveWatcherPidPath,
12781
12798
  nowFn: deps?.nowFn ?? (() => /* @__PURE__ */ new Date()),
12782
12799
  sleepFn: deps?.sleepFn ?? sleep,
12783
12800
  shouldShutdownFn: deps?.shouldShutdownFn ?? isShutdownRequested
@@ -12785,8 +12802,8 @@ async function runIngestCommand(inputPaths, options, deps) {
12785
12802
  const clackOutput = { output: process.stderr };
12786
12803
  clack4.intro(banner(), clackOutput);
12787
12804
  if (await resolvedDeps.isWatcherRunningFn()) {
12788
- const pid = await readWatcherPid();
12789
- const pidFile = resolveWatcherPidPath();
12805
+ const pid = await resolvedDeps.readWatcherPidFn();
12806
+ const pidFile = resolvedDeps.resolveWatcherPidPathFn();
12790
12807
  clack4.log.error(
12791
12808
  formatError(
12792
12809
  `agenr watcher is running (PID ${pid ?? "unknown"}). Stop the watcher before running ingest.
@@ -16259,6 +16276,7 @@ async function runWatchCommand(file, options, deps) {
16259
16276
  const clackOutput = { output: process.stderr };
16260
16277
  warnIfLocked();
16261
16278
  installSignalHandlers();
16279
+ clack10.intro(banner(), clackOutput);
16262
16280
  try {
16263
16281
  await resolvedDeps.writeWatcherPidFn();
16264
16282
  } catch (error) {
@@ -16286,7 +16304,6 @@ async function runWatchCommand(file, options, deps) {
16286
16304
  const raw = options.raw === true;
16287
16305
  const contextEnabled = Boolean(options.context);
16288
16306
  const modeConfig = await resolveWatchMode(file, options, resolvedDeps.statFileFn);
16289
- clack10.intro(banner(), clackOutput);
16290
16307
  for (const warning of modeConfig.warnings) {
16291
16308
  process.stderr.write(`${warning}
16292
16309
  `);
@@ -16364,13 +16381,12 @@ async function runWatchCommand(file, options, deps) {
16364
16381
  },
16365
16382
  onCycle: (result, ctx) => {
16366
16383
  cycleCount += 1;
16367
- const timestamp = formatClock(resolvedDeps.nowFn());
16384
+ const now = resolvedDeps.nowFn();
16385
+ const timestamp = formatClock(now);
16368
16386
  const fileLabel = result.filePath ? ` | file=${result.filePath}` : "";
16369
16387
  if (json) {
16370
- process.stdout.write(
16371
- `${JSON.stringify({ cycle: cycleCount, at: resolvedDeps.nowFn().toISOString(), ...result })}
16372
- `
16373
- );
16388
+ process.stdout.write(`${JSON.stringify({ cycle: cycleCount, at: now.toISOString(), ...result })}
16389
+ `);
16374
16390
  }
16375
16391
  if (result.error) {
16376
16392
  clack10.log.warn(formatWarn(`[${timestamp}] Cycle ${cycleCount}: ${result.error}${fileLabel}`), clackOutput);
@@ -1,10 +1,15 @@
1
- type HookEvent = {
2
- type: string;
3
- action: string;
4
- sessionKey: string;
5
- context: Record<string, unknown>;
6
- timestamp: Date;
7
- messages: string[];
1
+ type BeforeAgentStartEvent = {
2
+ prompt?: string;
3
+ messages?: unknown[];
4
+ [key: string]: unknown;
5
+ };
6
+ type PluginHookAgentContext = {
7
+ sessionKey?: string;
8
+ workspaceDir?: string;
9
+ [key: string]: unknown;
10
+ };
11
+ type BeforeAgentStartResult = {
12
+ prependContext?: string;
8
13
  };
9
14
  type PluginLogger = {
10
15
  debug?: (message: string) => void;
@@ -18,7 +23,7 @@ type PluginApi = {
18
23
  version?: string;
19
24
  pluginConfig?: Record<string, unknown>;
20
25
  logger: PluginLogger;
21
- registerHook: (events: string | string[], handler: (event: HookEvent) => Promise<void> | void) => void;
26
+ on: (hook: "before_agent_start", handler: (event: BeforeAgentStartEvent, ctx: PluginHookAgentContext) => Promise<BeforeAgentStartResult | undefined> | BeforeAgentStartResult | undefined) => void;
22
27
  };
23
28
 
24
29
  declare const plugin: {
@@ -1,31 +1,30 @@
1
- // src/openclaw-plugin/index.ts
2
- import os from "os";
3
- import path2 from "path";
4
-
5
1
  // src/openclaw-plugin/recall.ts
6
2
  import { spawn } from "child_process";
3
+ import { writeFile } from "fs/promises";
7
4
  import path from "path";
8
5
  import { fileURLToPath } from "url";
9
6
  var RECALL_TIMEOUT_MS = 5e3;
10
7
  var DEFAULT_BUDGET = 2e3;
11
- var DEFAULT_AGENR_PATH = path.resolve(
12
- fileURLToPath(import.meta.url),
13
- "..",
14
- "..",
15
- "..",
16
- "dist",
17
- "cli.js"
18
- );
8
+ var MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
9
+ var PACKAGE_ROOT = path.resolve(MODULE_DIR, "..", "..");
10
+ var DEFAULT_AGENR_PATH = path.join(PACKAGE_ROOT, "dist", "cli.js");
19
11
  function resolveAgenrPath(config) {
20
12
  return config?.agenrPath?.trim() || process.env["AGENR_BIN"]?.trim() || DEFAULT_AGENR_PATH;
21
13
  }
22
14
  function resolveBudget(config) {
23
15
  return config?.budget ?? DEFAULT_BUDGET;
24
16
  }
17
+ function buildSpawnArgs(agenrPath) {
18
+ if (agenrPath.endsWith(".js")) {
19
+ return { cmd: process.execPath, args: [agenrPath] };
20
+ }
21
+ return { cmd: agenrPath, args: [] };
22
+ }
25
23
  async function runRecall(agenrPath, budget) {
26
24
  return await new Promise((resolve) => {
27
25
  let stdout = "";
28
26
  let settled = false;
27
+ const spawnArgs = buildSpawnArgs(agenrPath);
29
28
  function finish(value) {
30
29
  if (settled) {
31
30
  return;
@@ -34,8 +33,8 @@ async function runRecall(agenrPath, budget) {
34
33
  resolve(value);
35
34
  }
36
35
  const child = spawn(
37
- process.execPath,
38
- [agenrPath, "recall", "--context", "session-start", "--budget", String(budget), "--json"],
36
+ spawnArgs.cmd,
37
+ [...spawnArgs.args, "recall", "--context", "session-start", "--budget", String(budget), "--json"],
39
38
  { stdio: ["ignore", "pipe", "ignore"] }
40
39
  );
41
40
  const timer = setTimeout(() => {
@@ -65,26 +64,35 @@ function isRecallResult(value) {
65
64
  }
66
65
  var TODO_TYPES = /* @__PURE__ */ new Set(["todo"]);
67
66
  var PREFERENCE_TYPES = /* @__PURE__ */ new Set(["preference", "decision"]);
68
- function formatRecallAsMarkdown(result) {
67
+ function groupValidEntries(result) {
68
+ const grouped = {
69
+ todos: [],
70
+ preferences: [],
71
+ facts: []
72
+ };
69
73
  if (!result.results || result.results.length === 0) {
70
- return "";
74
+ return grouped;
71
75
  }
72
- const todos = [];
73
- const preferences = [];
74
- const facts = [];
75
76
  for (const item of result.results) {
76
77
  const entry = item.entry;
77
78
  if (!entry || typeof entry.type !== "string" || typeof entry.subject !== "string" || typeof entry.content !== "string") {
78
79
  continue;
79
80
  }
80
81
  if (TODO_TYPES.has(entry.type)) {
81
- todos.push(entry);
82
+ grouped.todos.push(entry);
82
83
  } else if (PREFERENCE_TYPES.has(entry.type)) {
83
- preferences.push(entry);
84
+ grouped.preferences.push(entry);
84
85
  } else {
85
- facts.push(entry);
86
+ grouped.facts.push(entry);
86
87
  }
87
88
  }
89
+ return grouped;
90
+ }
91
+ function formatRecallAsMarkdown(result) {
92
+ if (!result.results || result.results.length === 0) {
93
+ return "";
94
+ }
95
+ const { todos, preferences, facts } = groupValidEntries(result);
88
96
  if (todos.length === 0 && preferences.length === 0 && facts.length === 0) {
89
97
  return "";
90
98
  }
@@ -112,52 +120,139 @@ function formatRecallAsMarkdown(result) {
112
120
  }
113
121
  return lines.join("\n").trimEnd();
114
122
  }
123
+ function formatRecallAsSummary(result, timestamp) {
124
+ if (!result.results || result.results.length === 0) {
125
+ return "";
126
+ }
127
+ const { todos, preferences, facts } = groupValidEntries(result);
128
+ const totalEntries = todos.length + preferences.length + facts.length;
129
+ if (totalEntries === 0) {
130
+ return "";
131
+ }
132
+ const lines = [timestamp ? `## agenr Memory -- ${timestamp}` : "## agenr Memory", ""];
133
+ lines.push(
134
+ `${totalEntries} entries recalled. Full context injected into this session automatically.`,
135
+ "To pull specific memories: ask your agent, or run:",
136
+ ' mcporter call agenr.agenr_recall query="your topic" limit=5',
137
+ ""
138
+ );
139
+ if (todos.length > 0) {
140
+ lines.push(`### Active Todos (${todos.length})`, "");
141
+ for (const entry of todos) {
142
+ lines.push(`- ${entry.subject}`);
143
+ }
144
+ lines.push("");
145
+ }
146
+ if (preferences.length > 0) {
147
+ lines.push(`### Preferences and Decisions (${preferences.length})`, "");
148
+ for (const entry of preferences) {
149
+ lines.push(`- ${entry.subject}`);
150
+ }
151
+ lines.push("");
152
+ }
153
+ if (facts.length > 0) {
154
+ lines.push(`### Facts and Events (${facts.length})`, "");
155
+ for (const entry of facts) {
156
+ lines.push(`- ${entry.subject}`);
157
+ }
158
+ lines.push("");
159
+ }
160
+ return lines.join("\n").trimEnd();
161
+ }
162
+ async function writeAgenrMd(markdown, workspaceDir) {
163
+ try {
164
+ const outputPath = path.join(workspaceDir, "AGENR.md");
165
+ await writeFile(outputPath, markdown, "utf8");
166
+ } catch {
167
+ }
168
+ }
115
169
 
116
170
  // src/openclaw-plugin/index.ts
117
- var AGENR_CONTEXT_PATH = path2.join(os.homedir(), ".agenr", "AGENR.md");
118
171
  var SKIP_SESSION_PATTERNS = [":subagent:", ":cron:"];
172
+ var DEFAULT_MAX_SEEN_SESSIONS = 1e3;
173
+ var seenSessions = /* @__PURE__ */ new Map();
174
+ function resolveMaxSeenSessions() {
175
+ const raw = process.env.AGENR_OPENCLAW_MAX_SEEN_SESSIONS;
176
+ if (!raw) {
177
+ return DEFAULT_MAX_SEEN_SESSIONS;
178
+ }
179
+ const parsed = Number.parseInt(raw, 10);
180
+ if (Number.isFinite(parsed) && parsed > 0) {
181
+ return parsed;
182
+ }
183
+ return DEFAULT_MAX_SEEN_SESSIONS;
184
+ }
185
+ var maxSeenSessions = resolveMaxSeenSessions();
119
186
  function shouldSkipSession(sessionKey) {
120
187
  return SKIP_SESSION_PATTERNS.some((pattern) => sessionKey.includes(pattern));
121
188
  }
189
+ function hasSeenSession(sessionKey) {
190
+ const seen = seenSessions.has(sessionKey);
191
+ if (!seen) {
192
+ return false;
193
+ }
194
+ seenSessions.delete(sessionKey);
195
+ seenSessions.set(sessionKey, true);
196
+ return true;
197
+ }
198
+ function markSessionSeen(sessionKey) {
199
+ seenSessions.set(sessionKey, true);
200
+ while (seenSessions.size > maxSeenSessions) {
201
+ const oldestKey = seenSessions.keys().next().value;
202
+ if (!oldestKey) {
203
+ break;
204
+ }
205
+ seenSessions.delete(oldestKey);
206
+ }
207
+ }
122
208
  var plugin = {
123
209
  id: "agenr",
124
210
  name: "agenr memory context",
125
- description: "Injects agenr long-term memory into every agent session via agent:bootstrap",
211
+ description: "Injects agenr long-term memory into every agent session via before_agent_start",
126
212
  register(api) {
127
- api.registerHook("agent:bootstrap", async (event) => {
128
- try {
129
- const ctx = event.context;
130
- if (!Array.isArray(ctx.bootstrapFiles)) {
131
- return;
132
- }
133
- const sessionKey = ctx.sessionKey ?? event.sessionKey ?? "";
134
- if (shouldSkipSession(sessionKey)) {
213
+ api.on(
214
+ "before_agent_start",
215
+ async (_event, ctx) => {
216
+ try {
217
+ const sessionKey = ctx.sessionKey ?? "";
218
+ if (shouldSkipSession(sessionKey)) {
219
+ return;
220
+ }
221
+ if (sessionKey && hasSeenSession(sessionKey)) {
222
+ return;
223
+ }
224
+ const config = api.pluginConfig;
225
+ if (config?.enabled === false) {
226
+ return;
227
+ }
228
+ if (sessionKey) {
229
+ markSessionSeen(sessionKey);
230
+ }
231
+ const agenrPath = resolveAgenrPath(config);
232
+ const budget = resolveBudget(config);
233
+ const result = await runRecall(agenrPath, budget);
234
+ if (!result) {
235
+ return;
236
+ }
237
+ const markdown = formatRecallAsMarkdown(result);
238
+ if (!markdown.trim()) {
239
+ return;
240
+ }
241
+ const workspaceDir = typeof ctx.workspaceDir === "string" ? ctx.workspaceDir.trim() : "";
242
+ if (workspaceDir) {
243
+ const now = /* @__PURE__ */ new Date();
244
+ const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
245
+ void writeAgenrMd(formatRecallAsSummary(result, timestamp), workspaceDir);
246
+ }
247
+ return { prependContext: markdown };
248
+ } catch (err) {
249
+ api.logger.warn(
250
+ `agenr plugin before_agent_start recall failed: ${err instanceof Error ? err.message : String(err)}`
251
+ );
135
252
  return;
136
253
  }
137
- const config = api.pluginConfig;
138
- if (config?.enabled === false) {
139
- return;
140
- }
141
- const agenrPath = resolveAgenrPath(config);
142
- const budget = resolveBudget(config);
143
- const result = await runRecall(agenrPath, budget);
144
- if (!result) {
145
- return;
146
- }
147
- const markdown = formatRecallAsMarkdown(result);
148
- if (!markdown.trim()) {
149
- return;
150
- }
151
- const file = {
152
- name: "AGENR.md",
153
- path: AGENR_CONTEXT_PATH,
154
- content: markdown,
155
- missing: false
156
- };
157
- ctx.bootstrapFiles.push(file);
158
- } catch {
159
254
  }
160
- });
255
+ );
161
256
  }
162
257
  };
163
258
  var openclaw_plugin_default = plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenr",
3
- "version": "0.6.7",
3
+ "version": "0.6.10",
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",
@@ -53,11 +60,9 @@
53
60
  "README.md"
54
61
  ],
55
62
  "author": "agenr-ai",
56
- "scripts": {
57
- "build": "tsup src/cli.ts src/cli-main.ts src/openclaw-plugin/index.ts --format esm --dts",
58
- "dev": "tsup src/cli.ts src/cli-main.ts --format esm --watch",
59
- "test": "vitest run",
60
- "test:watch": "vitest",
61
- "typecheck": "tsc --noEmit"
63
+ "pnpm": {
64
+ "overrides": {
65
+ "fast-xml-parser": "^5.3.6"
66
+ }
62
67
  }
63
- }
68
+ }