@tpsdev-ai/openclaw-flair 0.25.4 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ Uses Flair's native Harper vector embeddings — no OpenAI API key required.
10
10
  - **Persistent storage** via `memory_store` → Ed25519-authenticated writes
11
11
  - **Memory retrieval** via `memory_get` → fetch by ID
12
12
  - **Auto-bootstrap** — injects relevant memories into context at session start
13
- - **Auto-capture** — automatically stores important information from conversations
13
+ - **Auto-capture** — automatically stores important information from conversations, live, on every turn — works in both discrete runs and long-lived persistent gateway sessions (#798)
14
14
  - **Multi-agent** — `agentId: "auto"` resolves per-session for shared gateways
15
15
  - **Durability levels** — permanent, persistent, standard, ephemeral
16
16
  - **Memory versioning** — `supersedes` field creates version chains
@@ -70,6 +70,16 @@ In your OpenClaw config (`openclaw.json`):
70
70
  | `autoCapture` | boolean | `true` | Auto-capture important info from conversations |
71
71
  | `autoRecall` | boolean | `true` | Inject relevant memories at session start |
72
72
  | `maxRecallResults` | number | `5` | Max results for `memory_search` |
73
+ | `autoCaptureMaxPerSession` | number | `3` | Cap on trigger-based auto-captures per session (see below) |
74
+
75
+ ### Auto-capture
76
+
77
+ Auto-capture scans conversation text for a small set of conservative trigger phrases (e.g. "remember this", "we decided", "my name is") and writes a matching excerpt to Flair. It runs on two hooks:
78
+
79
+ - **`agent_end`** — scans the full conversation once, at the true end of a discrete run. Also detects entities/relationships from the same pass.
80
+ - **`llm_input`/`llm_output`** — scans the live prompt/response on every model call. This is what makes auto-capture work in a long-lived, persistent gateway session, where `agent_end` never fires because the "run" never ends (#798).
81
+
82
+ Both hooks share one capture budget per agent session (`autoCaptureMaxPerSession`, default 3) and dedup by content hash, so a phrase captured live during a run isn't captured again when `agent_end` rescans that same run's history at the end. For a persistent session that never fires `agent_end`, the budget is never reset — it's a cap for the session's whole lifetime, not per calendar day. Raise `autoCaptureMaxPerSession` if a long-lived deployment needs more than 3 auto-captures over its life.
73
83
 
74
84
  ## Tool naming
75
85
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
2
  export declare function isValidAgentId(agentId: string | null | undefined): boolean;
3
3
  export declare function assertValidAgentId(agentId: string | null | undefined): asserts agentId is string;
4
+ interface CaptureState {
5
+ count: number;
6
+ hashes: Set<string>;
7
+ }
8
+ export declare function evaluateAutoCapture(text: string, state: Pick<CaptureState, "count" | "hashes">, maxPerSession?: number): {
9
+ excerpt: string;
10
+ hash: string;
11
+ } | null;
4
12
  declare const _default: {
5
13
  kind: "memory";
6
14
  register(api: OpenClawPluginApi): void;
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ export function assertValidAgentId(agentId) {
17
17
  const DEFAULT_URL = "http://127.0.0.1:19926";
18
18
  const DEFAULT_MAX_RECALL = 5;
19
19
  const DEFAULT_MAX_BOOTSTRAP_TOKENS = 4000;
20
+ const DEFAULT_AUTO_CAPTURE_MAX_PER_SESSION = 3;
20
21
  const WORKSPACE_SOUL_FILES = {
21
22
  "SOUL.md": "soul",
22
23
  "IDENTITY.md": "identity",
@@ -71,6 +72,24 @@ function shouldCapture(text) {
71
72
  function excerptForCapture(text, maxChars = 500) {
72
73
  return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
73
74
  }
75
+ function createCaptureState() {
76
+ return { count: 0, hashes: new Set() };
77
+ }
78
+ export function evaluateAutoCapture(text, state, maxPerSession = DEFAULT_AUTO_CAPTURE_MAX_PER_SESSION) {
79
+ if (!shouldCapture(text))
80
+ return null;
81
+ if (state.count >= maxPerSession)
82
+ return null;
83
+ const excerpt = excerptForCapture(text);
84
+ const hash = hashContent(excerpt);
85
+ if (state.hashes.has(hash))
86
+ return null;
87
+ return { excerpt, hash };
88
+ }
89
+ function recordCapture(state, hash) {
90
+ state.count++;
91
+ state.hashes.add(hash);
92
+ }
74
93
  const PERSON_PATTERNS = [
75
94
  /\b([A-Z][a-z]{2,})\s+(?:said|asked|mentioned|decided|approved|rejected|thinks|wants|needs|prefers)\b/g,
76
95
  /\b(?:ask|ping|tell|check with|talk to)\s+(?:@)?([A-Z][a-z]{2,})\b/g,
@@ -305,6 +324,34 @@ export default {
305
324
  const maxRecall = cfg.maxRecallResults ?? DEFAULT_MAX_RECALL;
306
325
  const autoCapture = cfg.autoCapture ?? false;
307
326
  const autoRecall = cfg.autoRecall ?? true;
327
+ const autoCaptureMaxPerSession = Math.max(1, cfg.autoCaptureMaxPerSession ?? DEFAULT_AUTO_CAPTURE_MAX_PER_SESSION);
328
+ const captureStatePool = new Map();
329
+ function getCaptureState(agentId) {
330
+ let state = captureStatePool.get(agentId);
331
+ if (!state) {
332
+ state = createCaptureState();
333
+ captureStatePool.set(agentId, state);
334
+ }
335
+ return state;
336
+ }
337
+ function resetCaptureState(agentId) {
338
+ captureStatePool.delete(agentId);
339
+ }
340
+ async function tryAutoCapture(client, agentId, text) {
341
+ const state = getCaptureState(agentId);
342
+ const decision = evaluateAutoCapture(text, state, autoCaptureMaxPerSession);
343
+ if (!decision)
344
+ return false;
345
+ const entities = detectEntities(text);
346
+ const subject = entities.length > 0 ? entities[0].name.toLowerCase() : undefined;
347
+ await client.memory.write(decision.excerpt, {
348
+ type: "session",
349
+ tags: ["auto-captured"],
350
+ subject,
351
+ });
352
+ recordCapture(state, decision.hash);
353
+ return true;
354
+ }
308
355
  let currentAgentId = isAutoMode ? (fallbackAgentId ?? undefined) : cfg.agentId;
309
356
  const configuredAgentId = cfg.agentId && cfg.agentId !== "auto" ? cfg.agentId : null;
310
357
  api.on("before_agent_start", async (event, ctx) => {
@@ -488,9 +535,12 @@ export default {
488
535
  });
489
536
  }
490
537
  if (autoCapture) {
491
- api.on("agent_end", async (event) => {
538
+ api.on("agent_end", async (event, ctx) => {
539
+ const agentId = ctx?.agentId || currentAgentId || fallbackAgentId || undefined;
540
+ if (!agentId)
541
+ return;
492
542
  try {
493
- const client = getCurrentClient();
543
+ const client = getClient(agentId);
494
544
  const messages = (event.messages ?? []);
495
545
  let stored = 0;
496
546
  const allEntities = new Map();
@@ -501,17 +551,8 @@ export default {
501
551
  const text = typeof msg.content === "string" ? msg.content : "";
502
552
  if (!text || text.length < MIN_CAPTURE_LENGTH)
503
553
  continue;
504
- if (shouldCapture(text) && stored < 3) {
505
- const excerpt = excerptForCapture(text);
506
- const entities = detectEntities(text);
507
- const subject = entities.length > 0 ? entities[0].name.toLowerCase() : undefined;
508
- await client.memory.write(excerpt, {
509
- type: "session",
510
- tags: ["auto-captured"],
511
- subject,
512
- });
554
+ if (await tryAutoCapture(client, agentId, text))
513
555
  stored++;
514
- }
515
556
  for (const entity of detectEntities(text)) {
516
557
  const key = entity.name.toLowerCase();
517
558
  const existing = allEntities.get(key);
@@ -548,6 +589,44 @@ export default {
548
589
  catch (err) {
549
590
  api.logger.warn(`openclaw-flair: auto-capture failed: ${err.message}`);
550
591
  }
592
+ finally {
593
+ resetCaptureState(agentId);
594
+ }
595
+ });
596
+ api.on("llm_input", async (event, ctx) => {
597
+ const agentId = ctx?.agentId || currentAgentId || fallbackAgentId || undefined;
598
+ if (!agentId)
599
+ return;
600
+ const text = typeof event?.prompt === "string" ? event.prompt : "";
601
+ if (!text)
602
+ return;
603
+ try {
604
+ const client = getClient(agentId);
605
+ const captured = await tryAutoCapture(client, agentId, text);
606
+ if (captured)
607
+ api.logger.info("openclaw-flair: auto-captured 1 memory from live turn (llm_input)");
608
+ }
609
+ catch (err) {
610
+ api.logger.warn(`openclaw-flair: live auto-capture (llm_input) failed: ${err.message}`);
611
+ }
612
+ });
613
+ api.on("llm_output", async (event, ctx) => {
614
+ const agentId = ctx?.agentId || currentAgentId || fallbackAgentId || undefined;
615
+ if (!agentId)
616
+ return;
617
+ const texts = Array.isArray(event?.assistantTexts) ? event.assistantTexts : [];
618
+ const text = texts.filter((t) => typeof t === "string").join("\n");
619
+ if (!text)
620
+ return;
621
+ try {
622
+ const client = getClient(agentId);
623
+ const captured = await tryAutoCapture(client, agentId, text);
624
+ if (captured)
625
+ api.logger.info("openclaw-flair: auto-captured 1 memory from live turn (llm_output)");
626
+ }
627
+ catch (err) {
628
+ api.logger.warn(`openclaw-flair: live auto-capture (llm_output) failed: ${err.message}`);
629
+ }
551
630
  });
552
631
  }
553
632
  if (typeof api.registerContextEngine === "function") {
@@ -29,6 +29,11 @@
29
29
  "autoRecall": {
30
30
  "label": "Auto-Recall",
31
31
  "help": "Automatically inject relevant memories into context at session start"
32
+ },
33
+ "autoCaptureMaxPerSession": {
34
+ "label": "Auto-Capture Max Per Session",
35
+ "help": "Cap on trigger-based auto-captures per session (default 3). For a long-lived persistent session, this is the cap for the session's whole lifetime, not per calendar day.",
36
+ "advanced": true
32
37
  }
33
38
  },
34
39
  "configSchema": {
@@ -41,7 +46,8 @@
41
46
  "autoCapture": { "type": "boolean" },
42
47
  "autoRecall": { "type": "boolean" },
43
48
  "maxRecallResults": { "type": "number", "minimum": 1, "maximum": 20 },
44
- "maxBootstrapTokens": { "type": "number", "minimum": 500, "maximum": 8000 }
49
+ "maxBootstrapTokens": { "type": "number", "minimum": 500, "maximum": 8000 },
50
+ "autoCaptureMaxPerSession": { "type": "number", "minimum": 1, "maximum": 50 }
45
51
  },
46
52
  "required": []
47
53
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/openclaw-flair",
3
- "version": "0.25.4",
3
+ "version": "0.27.0",
4
4
  "description": "OpenClaw memory plugin for Flair — agent identity and semantic memory",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -58,7 +58,7 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "@sinclair/typebox": "0.34.48",
61
- "@tpsdev-ai/flair-client": "0.25.4"
61
+ "@tpsdev-ai/flair-client": "0.27.0"
62
62
  },
63
63
  "devDependencies": {
64
64
  "typescript": "5.9.3"