ofiere-openclaw-plugin 4.30.0 → 4.31.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.30.0",
3
+ "version": "4.31.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"],
@@ -35,12 +35,12 @@ export async function resolveAgentId(
35
35
  return cache.get(cacheKey)!;
36
36
  }
37
37
 
38
- // 2. Look up by name (case-insensitive)
38
+ // 2. Look up by name OR codename in a single query (v4.30.0 optimization)
39
39
  const { data: existing } = await supabase
40
40
  .from("agents")
41
41
  .select("id")
42
42
  .eq("user_id", userId)
43
- .ilike("name", accountId)
43
+ .or(`name.ilike.${accountId},codename.ilike.${accountId}`)
44
44
  .limit(1)
45
45
  .single();
46
46
 
@@ -49,21 +49,7 @@ export async function resolveAgentId(
49
49
  return existing.id;
50
50
  }
51
51
 
52
- // 3. Also try matching by codename
53
- const { data: byCodename } = await supabase
54
- .from("agents")
55
- .select("id")
56
- .eq("user_id", userId)
57
- .ilike("codename", accountId)
58
- .limit(1)
59
- .single();
60
-
61
- if (byCodename?.id) {
62
- cache.set(cacheKey, byCodename.id);
63
- return byCodename.id;
64
- }
65
-
66
- // 4. Auto-register a new agent
52
+ // 3. Auto-register a new agent
67
53
  const newId = `agent-${accountId.toLowerCase()}-${Date.now()}`;
68
54
  const { data: created } = await supabase
69
55
  .from("agents")
package/src/tools.ts CHANGED
@@ -113,10 +113,16 @@ const SYSTEM_NAME_BLOCKLIST = new Set([
113
113
  "ofiere pm plugin", "ofiere-openclaw-plugin",
114
114
  ]);
115
115
 
116
+ // Module-scoped brain context cache (v4.30.0)
117
+ // Shared between registerBrainContextHook and BRAIN_OPS save_memory
118
+ // so explicit saves invalidate the cache for immediate consistency.
119
+ const brainCache = new Map<string, { text: string; at: number }>();
120
+
116
121
  function isSystemName(name: string): boolean {
117
122
  return SYSTEM_NAME_BLOCKLIST.has(name.toLowerCase().trim());
118
123
  }
119
124
 
125
+
120
126
  function createAgentResolver(
121
127
  api: any,
122
128
  supabase: SupabaseClient,
@@ -5213,6 +5219,10 @@ function registerBrainOps(
5213
5219
  }).select("id, tier, importance, decay_score, created_at").single();
5214
5220
 
5215
5221
  if (error) return err(error.message);
5222
+
5223
+ // Invalidate brain context cache so next prompt build sees the new memory
5224
+ if (agentId) brainCache.delete(agentId);
5225
+
5216
5226
  return ok({ message: `Memory saved to ${tier}`, memory: data });
5217
5227
  }
5218
5228
 
@@ -5690,10 +5700,12 @@ function registerBrainExtractionHook(
5690
5700
 
5691
5701
  api.logger.debug?.(`[ofiere-brain] agent_end identity: ctx.agentId=${ctx?.agentId || "(none)"} event.agentId=${event?.agentId || "(none)"} resolved=${resolvedAgentId}`);
5692
5702
 
5693
- // ── Fast Stream: Write L1_focus ──
5703
+ // ── Fast Stream: Write L1_focus + L2_episode in parallel ──
5694
5704
  const rawContent = `User: ${lastUser}\nAssistant: ${lastAssistant}`;
5705
+ const immediateWrites: Promise<any>[] = [];
5706
+
5695
5707
  if (rawContent.length > 50) {
5696
- await supabase.from("agent_memories").insert({
5708
+ immediateWrites.push(supabase.from("agent_memories").insert({
5697
5709
  user_id: userId,
5698
5710
  agent_id: resolvedAgentId,
5699
5711
  tier: "L1_focus",
@@ -5702,16 +5714,14 @@ function registerBrainExtractionHook(
5702
5714
  importance: 3,
5703
5715
  context_key: sessionKey ? `conversation:${sessionKey}` : null,
5704
5716
  expires_at: new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString(),
5705
- });
5717
+ }));
5706
5718
  }
5707
5719
 
5708
- // ── Lightweight L2 episode summary ──
5709
5720
  if (lastUser.length + lastAssistant.length > 200) {
5710
5721
  const userSummary = lastUser.split(/[.!?]\s/)[0];
5711
5722
  const assistSummary = lastAssistant.split(/[.!?]\s/)[0];
5712
5723
  const episodeSummary = `User asked: "${userSummary}". Agent responded: "${assistSummary}"`;
5713
-
5714
- await supabase.from("agent_memories").insert({
5724
+ immediateWrites.push(supabase.from("agent_memories").insert({
5715
5725
  user_id: userId,
5716
5726
  agent_id: resolvedAgentId,
5717
5727
  tier: "L2_episode",
@@ -5719,70 +5729,86 @@ function registerBrainExtractionHook(
5719
5729
  source: "auto",
5720
5730
  importance: 4,
5721
5731
  context_key: sessionKey ? `episode:${sessionKey}` : null,
5722
- });
5732
+ }));
5723
5733
  }
5724
5734
 
5725
- // ── Rule-based L3/L4 extraction from user message ──
5726
- // Extract factual statements → L3_pattern
5735
+ // Fire L1+L2 writes in parallel (no await needed between them)
5736
+ await Promise.all(immediateWrites);
5737
+
5738
+ // ── Batched L3/L4 extraction (v4.30.0 optimization) ──
5739
+ // Collect all candidates first, dedup in one parallel batch, insert in one batch.
5727
5740
  const factPatterns = [
5728
5741
  /(?:my name is|i'm called|call me)\s+(\w+)/gi,
5729
5742
  /(?:i (?:work|am working) (?:at|for|with))\s+(.+?)(?:\.|,|$)/gi,
5730
5743
  /(?:i (?:like|love|prefer|enjoy|hate|dislike))\s+(.+?)(?:\.|,|$)/gi,
5731
5744
  /(?:i (?:am|'m))\s+(?:a|an)\s+(.+?)(?:\.|,|$)/gi,
5732
5745
  ];
5746
+ const rulePatterns = [
5747
+ /(?:always|never|make sure|don't|do not|please always)\s+(.+?)(?:\.|!|$)/gi,
5748
+ /(?:remember to|keep in mind|note that)\s+(.+?)(?:\.|!|$)/gi,
5749
+ ];
5750
+
5751
+ // Phase 1: Collect all regex candidates
5752
+ type MemCandidate = { tier: string; content: string; contextKey: string; importance: number };
5753
+ const candidates: MemCandidate[] = [];
5733
5754
 
5734
5755
  for (const pattern of factPatterns) {
5735
- const matches = lastUser.matchAll(pattern);
5736
- for (const match of matches) {
5756
+ for (const match of lastUser.matchAll(pattern)) {
5737
5757
  const fact = match[0].trim();
5738
- const contextKey = `fact:${fact.slice(0, 50).toLowerCase().replace(/\s+/g, "_")}`;
5739
- // Dedup: check if context_key already exists
5740
- const { data: existing } = await supabase.from("agent_memories")
5741
- .select("id").eq("user_id", userId).eq("agent_id", resolvedAgentId)
5742
- .eq("context_key", contextKey).is("superseded_by", null).limit(1);
5743
- if (existing && existing.length > 0) continue;
5744
-
5745
- await supabase.from("agent_memories").insert({
5746
- user_id: userId,
5747
- agent_id: resolvedAgentId,
5758
+ candidates.push({
5748
5759
  tier: "L3_pattern",
5749
5760
  content: fact,
5750
- source: "auto",
5761
+ contextKey: `fact:${fact.slice(0, 50).toLowerCase().replace(/\s+/g, "_")}`,
5751
5762
  importance: 6,
5752
- context_key: contextKey,
5753
5763
  });
5754
5764
  }
5755
5765
  }
5756
-
5757
- // Extract directives → L4_rule
5758
- const rulePatterns = [
5759
- /(?:always|never|make sure|don't|do not|please always)\s+(.+?)(?:\.|!|$)/gi,
5760
- /(?:remember to|keep in mind|note that)\s+(.+?)(?:\.|!|$)/gi,
5761
- ];
5762
-
5763
5766
  for (const pattern of rulePatterns) {
5764
- const matches = lastUser.matchAll(pattern);
5765
- for (const match of matches) {
5767
+ for (const match of lastUser.matchAll(pattern)) {
5766
5768
  const rule = match[0].trim();
5767
5769
  if (rule.length < 10) continue;
5768
- const contextKey = `rule:${rule.slice(0, 50).toLowerCase().replace(/\s+/g, "_")}`;
5769
- const { data: existing } = await supabase.from("agent_memories")
5770
- .select("id").eq("user_id", userId).eq("agent_id", resolvedAgentId)
5771
- .eq("context_key", contextKey).is("superseded_by", null).limit(1);
5772
- if (existing && existing.length > 0) continue;
5773
-
5774
- await supabase.from("agent_memories").insert({
5775
- user_id: userId,
5776
- agent_id: resolvedAgentId,
5770
+ candidates.push({
5777
5771
  tier: "L4_rule",
5778
5772
  content: rule,
5779
- source: "auto",
5773
+ contextKey: `rule:${rule.slice(0, 50).toLowerCase().replace(/\s+/g, "_")}`,
5780
5774
  importance: 7,
5781
- context_key: contextKey,
5782
5775
  });
5783
5776
  }
5784
5777
  }
5785
5778
 
5779
+ // Phase 2: Parallel dedup check (1 query per candidate, all at once)
5780
+ if (candidates.length > 0) {
5781
+ const dedupResults = await Promise.all(
5782
+ candidates.map(c =>
5783
+ supabase.from("agent_memories")
5784
+ .select("id")
5785
+ .eq("user_id", userId)
5786
+ .eq("agent_id", resolvedAgentId)
5787
+ .eq("context_key", c.contextKey)
5788
+ .is("superseded_by", null)
5789
+ .limit(1)
5790
+ )
5791
+ );
5792
+
5793
+ // Phase 3: Batch insert only new candidates
5794
+ const toInsert = candidates
5795
+ .filter((_, i) => !dedupResults[i].data?.length)
5796
+ .map(c => ({
5797
+ user_id: userId,
5798
+ agent_id: resolvedAgentId,
5799
+ tier: c.tier,
5800
+ content: c.content,
5801
+ source: "auto" as const,
5802
+ importance: c.importance,
5803
+ context_key: c.contextKey,
5804
+ decay_score: 1.0,
5805
+ }));
5806
+
5807
+ if (toInsert.length > 0) {
5808
+ await supabase.from("agent_memories").insert(toInsert);
5809
+ }
5810
+ }
5811
+
5786
5812
  api.logger.debug?.(`[ofiere-brain] Extracted memory for agent=${resolvedAgentId} session=${sessionKey || "none"}`);
5787
5813
  } catch (e) {
5788
5814
  // Silent — brain extraction must never block chat
@@ -5815,11 +5841,9 @@ function registerBrainContextHook(
5815
5841
  userId: string,
5816
5842
  fallbackAgentId: string,
5817
5843
  ): void {
5818
- // Cache to avoid querying Supabase on every single prompt build.
5819
- // Key: resolved agent UUID. Value: { brainContext, loadedAt }.
5820
- // Cache TTL: 60 seconds — brain context refreshes at most once per minute.
5821
- const brainCache = new Map<string, { text: string; at: number }>();
5822
- const CACHE_TTL_MS = 60_000;
5844
+ // Uses module-scoped brainCache (declared at top of file) for cross-function
5845
+ // invalidation. save_memory calls brainCache.delete(agentId) for immediate consistency.
5846
+ const CACHE_TTL_MS = 300_000;
5823
5847
 
5824
5848
  try {
5825
5849
  api.on("before_prompt_build", async (_event: any, ctx: any) => {
@@ -5846,38 +5870,26 @@ function registerBrainContextHook(
5846
5870
  }
5847
5871
 
5848
5872
  // ── Query brain memories for THIS specific agent ──
5873
+ // v4.30.0: Single combined query instead of 3 separate tier queries.
5849
5874
  const now = new Date().toISOString();
5850
5875
 
5851
- const [l1Res, l4Res, l5Res] = await Promise.all([
5852
- supabase.from("agent_memories")
5853
- .select("content, importance")
5854
- .eq("user_id", userId)
5855
- .eq("agent_id", resolvedAgentId)
5856
- .eq("tier", "L1_focus")
5857
- .gt("decay_score", 0.1)
5858
- .or(`expires_at.is.null,expires_at.gt.${now}`)
5859
- .order("importance", { ascending: false })
5860
- .limit(5),
5861
- supabase.from("agent_memories")
5862
- .select("content, importance")
5863
- .eq("user_id", userId)
5864
- .eq("agent_id", resolvedAgentId)
5865
- .eq("tier", "L4_rule")
5866
- .is("superseded_by", null)
5867
- .order("importance", { ascending: false })
5868
- .limit(10),
5869
- supabase.from("agent_memories")
5870
- .select("content")
5871
- .eq("user_id", userId)
5872
- .eq("agent_id", resolvedAgentId)
5873
- .eq("tier", "L5_persona")
5874
- .order("importance", { ascending: false })
5875
- .limit(3),
5876
- ]);
5877
-
5878
- const l1 = l1Res.data || [];
5879
- const l4 = l4Res.data || [];
5880
- const l5 = l5Res.data || [];
5876
+ const { data: allBrain } = await supabase
5877
+ .from("agent_memories")
5878
+ .select("content, importance, tier")
5879
+ .eq("user_id", userId)
5880
+ .eq("agent_id", resolvedAgentId)
5881
+ .in("tier", ["L1_focus", "L4_rule", "L5_persona"])
5882
+ .gt("decay_score", 0.1)
5883
+ .is("superseded_by", null)
5884
+ .or(`expires_at.is.null,expires_at.gt.${now}`)
5885
+ .order("importance", { ascending: false })
5886
+ .limit(18); // 5 L1 + 10 L4 + 3 L5
5887
+
5888
+ // Client-side split by tier
5889
+ const all = allBrain || [];
5890
+ const l1 = all.filter((m: any) => m.tier === "L1_focus").slice(0, 5);
5891
+ const l4 = all.filter((m: any) => m.tier === "L4_rule").slice(0, 10);
5892
+ const l5 = all.filter((m: any) => m.tier === "L5_persona").slice(0, 3);
5881
5893
 
5882
5894
  if (l1.length === 0 && l4.length === 0 && l5.length === 0) {
5883
5895
  brainCache.set(resolvedAgentId, { text: "", at: Date.now() });