@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 +11 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +91 -12
- package/openclaw.plugin.json +7 -1
- package/package.json +2 -2
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 =
|
|
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 (
|
|
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") {
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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.
|
|
61
|
+
"@tpsdev-ai/flair-client": "0.27.0"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"typescript": "5.9.3"
|