ofiere-openclaw-plugin 4.27.3 → 4.29.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/openclaw.plugin.json +3 -0
- package/package.json +1 -1
- package/src/tools.ts +176 -0
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.29.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw plugin for Ofiere PM - 14 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, execution plan builder, SOP management, and agent brain (memory + self-improvement)",
|
|
6
6
|
"keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
|
package/src/tools.ts
CHANGED
|
@@ -5576,6 +5576,12 @@ export function registerTools(
|
|
|
5576
5576
|
api.logger.warn?.(`[ofiere] Brain context injection failed: ${e?.message || e}`);
|
|
5577
5577
|
});
|
|
5578
5578
|
|
|
5579
|
+
// ── Register agent_end hook for server-side brain extraction ──
|
|
5580
|
+
// This is the FIX for Bug 2: extraction was client-side only (useSocket.ts).
|
|
5581
|
+
// Now every completed agent turn — from ANY channel (Telegram, Discord,
|
|
5582
|
+
// webchat, scheduled) — triggers memory extraction server-side.
|
|
5583
|
+
registerBrainExtractionHook(api, supabase, userId, fallbackAgentId);
|
|
5584
|
+
|
|
5579
5585
|
// ── Count and log ──
|
|
5580
5586
|
const toolCount = 14;
|
|
5581
5587
|
const callerName = getCallingAgentName(api);
|
|
@@ -5585,6 +5591,176 @@ export function registerTools(
|
|
|
5585
5591
|
return toolCount;
|
|
5586
5592
|
}
|
|
5587
5593
|
|
|
5594
|
+
// ── Server-Side Brain Extraction (agent_end hook) ─────────────────────────
|
|
5595
|
+
// Fires after EVERY agent turn completes, regardless of channel.
|
|
5596
|
+
// Extracts L1 raw fragments and lightweight L2 summaries.
|
|
5597
|
+
// Replaces the client-side extractBrainMemory() in useSocket.ts.
|
|
5598
|
+
// Requires allowConversationAccess: true in plugin config on the OpenClaw side.
|
|
5599
|
+
|
|
5600
|
+
function registerBrainExtractionHook(
|
|
5601
|
+
api: any,
|
|
5602
|
+
supabase: SupabaseClient,
|
|
5603
|
+
userId: string,
|
|
5604
|
+
fallbackAgentId: string,
|
|
5605
|
+
): void {
|
|
5606
|
+
try {
|
|
5607
|
+
api.on("agent_end", async (event: any) => {
|
|
5608
|
+
try {
|
|
5609
|
+
// Extract messages from event — agent_end provides the conversation
|
|
5610
|
+
const messages: any[] = event?.messages || event?.context?.messages || [];
|
|
5611
|
+
if (!messages || messages.length < 2) return;
|
|
5612
|
+
|
|
5613
|
+
// Find last user + last assistant message
|
|
5614
|
+
let lastUser = "";
|
|
5615
|
+
let lastAssistant = "";
|
|
5616
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
5617
|
+
const msg = messages[i];
|
|
5618
|
+
const role = msg?.role || msg?.message?.role;
|
|
5619
|
+
const text = typeof msg === "string" ? msg
|
|
5620
|
+
: msg?.text || msg?.content || msg?.message?.content
|
|
5621
|
+
?.filter?.((c: any) => c.type === "text")
|
|
5622
|
+
?.map?.((c: any) => c.text)
|
|
5623
|
+
?.join?.("\n")
|
|
5624
|
+
|| (typeof msg?.message?.content === "string" ? msg.message.content : "");
|
|
5625
|
+
|
|
5626
|
+
if (!lastAssistant && (role === "assistant" || role === "model")) {
|
|
5627
|
+
lastAssistant = typeof text === "string" ? text : "";
|
|
5628
|
+
}
|
|
5629
|
+
if (!lastUser && role === "user") {
|
|
5630
|
+
lastUser = typeof text === "string" ? text : "";
|
|
5631
|
+
}
|
|
5632
|
+
if (lastUser && lastAssistant) break;
|
|
5633
|
+
}
|
|
5634
|
+
|
|
5635
|
+
// Skip trivial exchanges
|
|
5636
|
+
if (lastUser.length < 20 || lastAssistant.length < 30) return;
|
|
5637
|
+
|
|
5638
|
+
// Resolve agent identity from event context
|
|
5639
|
+
const agentId = event?.agentId
|
|
5640
|
+
|| event?.context?.agentId
|
|
5641
|
+
|| event?.sessionKey?.split?.(":")?.[0]
|
|
5642
|
+
|| fallbackAgentId
|
|
5643
|
+
|| "";
|
|
5644
|
+
|
|
5645
|
+
if (!agentId || agentId === "unknown" || agentId === "system") return;
|
|
5646
|
+
|
|
5647
|
+
// Resolve to actual DB agent_id
|
|
5648
|
+
let resolvedAgentId = agentId;
|
|
5649
|
+
try {
|
|
5650
|
+
const resolved = await resolveAgentId(agentId, userId, supabase);
|
|
5651
|
+
if (resolved) resolvedAgentId = resolved;
|
|
5652
|
+
} catch {
|
|
5653
|
+
// Use raw agentId
|
|
5654
|
+
}
|
|
5655
|
+
|
|
5656
|
+
const sessionKey = event?.sessionKey || event?.context?.sessionKey || undefined;
|
|
5657
|
+
|
|
5658
|
+
// ── Fast Stream: Write L1_focus ──
|
|
5659
|
+
const rawContent = `User: ${lastUser}\nAssistant: ${lastAssistant}`;
|
|
5660
|
+
if (rawContent.length > 50) {
|
|
5661
|
+
await supabase.from("agent_memories").insert({
|
|
5662
|
+
user_id: userId,
|
|
5663
|
+
agent_id: resolvedAgentId,
|
|
5664
|
+
tier: "L1_focus",
|
|
5665
|
+
content: rawContent.slice(0, 2000),
|
|
5666
|
+
source: "auto",
|
|
5667
|
+
importance: 3,
|
|
5668
|
+
context_key: sessionKey ? `conversation:${sessionKey}` : null,
|
|
5669
|
+
expires_at: new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString(),
|
|
5670
|
+
});
|
|
5671
|
+
}
|
|
5672
|
+
|
|
5673
|
+
// ── Lightweight L2 episode summary ──
|
|
5674
|
+
if (lastUser.length + lastAssistant.length > 200) {
|
|
5675
|
+
const userSummary = lastUser.split(/[.!?]\s/)[0];
|
|
5676
|
+
const assistSummary = lastAssistant.split(/[.!?]\s/)[0];
|
|
5677
|
+
const episodeSummary = `User asked: "${userSummary}". Agent responded: "${assistSummary}"`;
|
|
5678
|
+
|
|
5679
|
+
await supabase.from("agent_memories").insert({
|
|
5680
|
+
user_id: userId,
|
|
5681
|
+
agent_id: resolvedAgentId,
|
|
5682
|
+
tier: "L2_episode",
|
|
5683
|
+
content: episodeSummary.slice(0, 1000),
|
|
5684
|
+
source: "auto",
|
|
5685
|
+
importance: 4,
|
|
5686
|
+
context_key: sessionKey ? `episode:${sessionKey}` : null,
|
|
5687
|
+
});
|
|
5688
|
+
}
|
|
5689
|
+
|
|
5690
|
+
// ── Rule-based L3/L4 extraction from user message ──
|
|
5691
|
+
// Extract factual statements → L3_pattern
|
|
5692
|
+
const factPatterns = [
|
|
5693
|
+
/(?:my name is|i'm called|call me)\s+(\w+)/gi,
|
|
5694
|
+
/(?:i (?:work|am working) (?:at|for|with))\s+(.+?)(?:\.|,|$)/gi,
|
|
5695
|
+
/(?:i (?:like|love|prefer|enjoy|hate|dislike))\s+(.+?)(?:\.|,|$)/gi,
|
|
5696
|
+
/(?:i (?:am|'m))\s+(?:a|an)\s+(.+?)(?:\.|,|$)/gi,
|
|
5697
|
+
];
|
|
5698
|
+
|
|
5699
|
+
for (const pattern of factPatterns) {
|
|
5700
|
+
const matches = lastUser.matchAll(pattern);
|
|
5701
|
+
for (const match of matches) {
|
|
5702
|
+
const fact = match[0].trim();
|
|
5703
|
+
const contextKey = `fact:${fact.slice(0, 50).toLowerCase().replace(/\s+/g, "_")}`;
|
|
5704
|
+
// Dedup: check if context_key already exists
|
|
5705
|
+
const { data: existing } = await supabase.from("agent_memories")
|
|
5706
|
+
.select("id").eq("user_id", userId).eq("agent_id", resolvedAgentId)
|
|
5707
|
+
.eq("context_key", contextKey).is("superseded_by", null).limit(1);
|
|
5708
|
+
if (existing && existing.length > 0) continue;
|
|
5709
|
+
|
|
5710
|
+
await supabase.from("agent_memories").insert({
|
|
5711
|
+
user_id: userId,
|
|
5712
|
+
agent_id: resolvedAgentId,
|
|
5713
|
+
tier: "L3_pattern",
|
|
5714
|
+
content: fact,
|
|
5715
|
+
source: "auto",
|
|
5716
|
+
importance: 6,
|
|
5717
|
+
context_key: contextKey,
|
|
5718
|
+
});
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
|
|
5722
|
+
// Extract directives → L4_rule
|
|
5723
|
+
const rulePatterns = [
|
|
5724
|
+
/(?:always|never|make sure|don't|do not|please always)\s+(.+?)(?:\.|!|$)/gi,
|
|
5725
|
+
/(?:remember to|keep in mind|note that)\s+(.+?)(?:\.|!|$)/gi,
|
|
5726
|
+
];
|
|
5727
|
+
|
|
5728
|
+
for (const pattern of rulePatterns) {
|
|
5729
|
+
const matches = lastUser.matchAll(pattern);
|
|
5730
|
+
for (const match of matches) {
|
|
5731
|
+
const rule = match[0].trim();
|
|
5732
|
+
if (rule.length < 10) continue;
|
|
5733
|
+
const contextKey = `rule:${rule.slice(0, 50).toLowerCase().replace(/\s+/g, "_")}`;
|
|
5734
|
+
const { data: existing } = await supabase.from("agent_memories")
|
|
5735
|
+
.select("id").eq("user_id", userId).eq("agent_id", resolvedAgentId)
|
|
5736
|
+
.eq("context_key", contextKey).is("superseded_by", null).limit(1);
|
|
5737
|
+
if (existing && existing.length > 0) continue;
|
|
5738
|
+
|
|
5739
|
+
await supabase.from("agent_memories").insert({
|
|
5740
|
+
user_id: userId,
|
|
5741
|
+
agent_id: resolvedAgentId,
|
|
5742
|
+
tier: "L4_rule",
|
|
5743
|
+
content: rule,
|
|
5744
|
+
source: "auto",
|
|
5745
|
+
importance: 7,
|
|
5746
|
+
context_key: contextKey,
|
|
5747
|
+
});
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
|
|
5751
|
+
api.logger.debug?.(`[ofiere-brain] Extracted memory for agent=${resolvedAgentId} session=${sessionKey || "none"}`);
|
|
5752
|
+
} catch (e) {
|
|
5753
|
+
// Silent — brain extraction must never block chat
|
|
5754
|
+
api.logger.debug?.(`[ofiere-brain] Extraction error: ${e instanceof Error ? e.message : e}`);
|
|
5755
|
+
}
|
|
5756
|
+
});
|
|
5757
|
+
|
|
5758
|
+
api.logger.info("[ofiere] agent_end brain extraction hook registered");
|
|
5759
|
+
} catch {
|
|
5760
|
+
api.logger.debug?.("[ofiere] Could not register agent_end hook — may not be supported in this OpenClaw version");
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
|
|
5588
5764
|
// ── Brain Context Bootstrap Injection ──────────────────────────────────────
|
|
5589
5765
|
// Queries the agent's active memories at boot and injects them into the system
|
|
5590
5766
|
// prompt via api.on("before_prompt_build"). Uses the TMT hierarchy:
|