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.
@@ -28,6 +28,9 @@
28
28
  }
29
29
  }
30
30
  },
31
+ "hooks": {
32
+ "allowConversationAccess": true
33
+ },
31
34
  "uiHints": {
32
35
  "enabled": {
33
36
  "label": "Enable Ofiere PM",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.27.3",
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: