ofiere-openclaw-plugin 4.29.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.29.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,
@@ -5095,7 +5101,9 @@ function registerBrainOps(
5095
5101
  userId: string,
5096
5102
  resolveAgent: (id?: string) => Promise<string | null>,
5097
5103
  ): void {
5098
- api.registerTool({
5104
+ // Register as a FACTORY so OpenClaw passes ToolContext with agentAccountId.
5105
+ // This is the primary fix for the Daisy→Celia misattribution bug.
5106
+ api.registerTool((toolCtx: any) => ({
5099
5107
  name: "OFIERE_BRAIN_OPS",
5100
5108
  label: "Ofiere Brain Operations",
5101
5109
  description:
@@ -5165,6 +5173,21 @@ function registerBrainOps(
5165
5173
  async execute(_id: string, params: Record<string, unknown>) {
5166
5174
  const action = params.action as string;
5167
5175
 
5176
+ // ── Resolve agent identity ──────────────────────────────────
5177
+ // Priority: explicit param > factory context > resolver fallback
5178
+ // The factory context (toolCtx) carries the actual calling agent's
5179
+ // accountId from OpenClaw, fixing the Daisy→Celia misattribution.
5180
+ const ctxAgentHint = toolCtx?.agentAccountId || toolCtx?.agentId || "";
5181
+
5182
+ async function resolveCallingAgent(explicitId?: string): Promise<string | null> {
5183
+ // 1. Explicit param from LLM
5184
+ if (explicitId && explicitId.trim()) return resolveAgent(explicitId);
5185
+ // 2. OpenClaw tool context (the fix)
5186
+ if (ctxAgentHint) return resolveAgent(ctxAgentHint);
5187
+ // 3. Original resolver fallback
5188
+ return resolveAgent();
5189
+ }
5190
+
5168
5191
  switch (action) {
5169
5192
  // ── Memory: Save ──
5170
5193
  case "save_memory": {
@@ -5174,7 +5197,7 @@ function registerBrainOps(
5174
5197
  if (!params.tier) missing.push("tier");
5175
5198
  if (missing.length > 0) return err(`Missing required: ${missing.join(", ")}`);
5176
5199
  }
5177
- const agentId = await resolveAgent(params.agent_id as string);
5200
+ const agentId = await resolveCallingAgent(params.agent_id as string);
5178
5201
  if (!agentId) return err("Could not resolve agent_id");
5179
5202
 
5180
5203
  const tier = params.tier as string;
@@ -5196,13 +5219,19 @@ function registerBrainOps(
5196
5219
  }).select("id, tier, importance, decay_score, created_at").single();
5197
5220
 
5198
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
+
5199
5226
  return ok({ message: `Memory saved to ${tier}`, memory: data });
5200
5227
  }
5201
5228
 
5202
5229
  // ── Memory: Recall (Full-text search) ──
5203
5230
  case "recall": {
5204
5231
  if (!params.query) return err("Missing required: query");
5205
- const agentId = params.agent_id ? await resolveAgent(params.agent_id as string) : null;
5232
+ const agentId = params.agent_id
5233
+ ? await resolveAgent(params.agent_id as string)
5234
+ : (ctxAgentHint ? await resolveAgent(ctxAgentHint) : null);
5206
5235
  const limit = (params.limit as number) || 20;
5207
5236
  const queryText = params.query as string;
5208
5237
 
@@ -5281,7 +5310,7 @@ function registerBrainOps(
5281
5310
  if (!params.category) missing.push("category");
5282
5311
  if (missing.length > 0) return err(`Missing required: ${missing.join(", ")}`);
5283
5312
  }
5284
- const agentId = await resolveAgent(params.agent_id as string);
5313
+ const agentId = await resolveCallingAgent(params.agent_id as string);
5285
5314
  if (!agentId) return err("Could not resolve agent_id");
5286
5315
 
5287
5316
  const content = `[${params.category}] ${params.title}${params.detail ? `: ${params.detail}` : ""}`;
@@ -5305,7 +5334,9 @@ function registerBrainOps(
5305
5334
 
5306
5335
  // ── Learning: List (→ L4_rule query) ──
5307
5336
  case "list_learnings": {
5308
- const agentId = params.agent_id ? await resolveAgent(params.agent_id as string) : null;
5337
+ const agentId = params.agent_id
5338
+ ? await resolveAgent(params.agent_id as string)
5339
+ : (ctxAgentHint ? await resolveAgent(ctxAgentHint) : null);
5309
5340
  const limit = (params.limit as number) || 20;
5310
5341
 
5311
5342
  let q = supabase.from("agent_memories")
@@ -5331,7 +5362,7 @@ function registerBrainOps(
5331
5362
 
5332
5363
  if (params.resolution) {
5333
5364
  // Create a superseding memory with the resolution
5334
- const agentId = await resolveAgent(params.agent_id as string);
5365
+ const agentId = await resolveCallingAgent(params.agent_id as string);
5335
5366
  const { data: newMem } = await supabase.from("agent_memories").insert({
5336
5367
  user_id: userId,
5337
5368
  agent_id: agentId || "system",
@@ -5364,7 +5395,7 @@ function registerBrainOps(
5364
5395
  case "save_entity": {
5365
5396
  if (!params.label) return err("Missing required: label");
5366
5397
  if (!params.node_type) return err("Missing required: node_type");
5367
- const agentId = await resolveAgent(params.agent_id as string);
5398
+ const agentId = await resolveCallingAgent(params.agent_id as string);
5368
5399
  if (!agentId) return err("Could not resolve agent_id");
5369
5400
 
5370
5401
  const { data, error } = await supabase.from("knowledge_graph_nodes").insert({
@@ -5404,7 +5435,9 @@ function registerBrainOps(
5404
5435
  // ── Knowledge Graph: Query ──
5405
5436
  case "query_graph": {
5406
5437
  if (!params.query) return err("Missing required: query");
5407
- const agentId = params.agent_id ? await resolveAgent(params.agent_id as string) : null;
5438
+ const agentId = params.agent_id
5439
+ ? await resolveAgent(params.agent_id as string)
5440
+ : (ctxAgentHint ? await resolveAgent(ctxAgentHint) : null);
5408
5441
  const limit = (params.limit as number) || 20;
5409
5442
 
5410
5443
  let q = supabase.from("knowledge_graph_nodes")
@@ -5436,7 +5469,7 @@ function registerBrainOps(
5436
5469
 
5437
5470
  // ── Trajectory: Start ──
5438
5471
  case "start_trajectory": {
5439
- const agentId = await resolveAgent(params.agent_id as string);
5472
+ const agentId = await resolveCallingAgent(params.agent_id as string);
5440
5473
  if (!agentId) return err("Could not resolve agent_id");
5441
5474
 
5442
5475
  const { data, error } = await supabase.from("execution_trajectories").insert({
@@ -5475,7 +5508,9 @@ function registerBrainOps(
5475
5508
 
5476
5509
  // ── Brain Status ──
5477
5510
  case "get_brain_status": {
5478
- const agentId = params.agent_id ? await resolveAgent(params.agent_id as string) : null;
5511
+ const agentId = params.agent_id
5512
+ ? await resolveAgent(params.agent_id as string)
5513
+ : (ctxAgentHint ? await resolveAgent(ctxAgentHint) : null);
5479
5514
  const now = new Date().toISOString();
5480
5515
 
5481
5516
  // Parallel queries for speed
@@ -5533,7 +5568,7 @@ function registerBrainOps(
5533
5568
  return err(`Unknown action "${action}". Valid: save_memory, recall, delete_memory, promote_memory, log_learning, list_learnings, resolve_learning, save_entity, link_entities, query_graph, start_trajectory, end_trajectory, get_brain_status`);
5534
5569
  }
5535
5570
  },
5536
- });
5571
+ }));
5537
5572
  }
5538
5573
 
5539
5574
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -5568,13 +5603,12 @@ export function registerTools(
5568
5603
  registerSOPOps(api, supabase, userId, resolveAgent); // 13
5569
5604
  registerBrainOps(api, supabase, userId, resolveAgent); // 14
5570
5605
 
5571
- // ── Inject brain context at bootstrap ──
5572
- // This loads the agent's active memories + unresolved learnings into the
5573
- // system prompt so the agent starts every conversation with full context.
5574
- // Runs async does NOT block registration.
5575
- injectBrainContext(api, supabase, userId, fallbackAgentId).catch((e: any) => {
5576
- api.logger.warn?.(`[ofiere] Brain context injection failed: ${e?.message || e}`);
5577
- });
5606
+ // ── Register dynamic brain context hook ──
5607
+ // FIX (v4.30.0): Was injectBrainContext() which loaded once at registration.
5608
+ // Now registers a before_prompt_build hook that dynamically resolves the
5609
+ // calling agent from ctx.agentId and loads THAT agent's brain memories.
5610
+ // Each agent sees its own brain context — Daisy sees Daisy's, Ivy sees Ivy's.
5611
+ registerBrainContextHook(api, supabase, userId, fallbackAgentId);
5578
5612
 
5579
5613
  // ── Register agent_end hook for server-side brain extraction ──
5580
5614
  // This is the FIX for Bug 2: extraction was client-side only (useSocket.ts).
@@ -5596,6 +5630,10 @@ export function registerTools(
5596
5630
  // Extracts L1 raw fragments and lightweight L2 summaries.
5597
5631
  // Replaces the client-side extractBrainMemory() in useSocket.ts.
5598
5632
  // Requires allowConversationAccess: true in plugin config on the OpenClaw side.
5633
+ //
5634
+ // FIX (v4.30.0): OpenClaw passes (event, ctx: PluginHookAgentContext) where
5635
+ // ctx.agentId is the calling agent's accountId. Previously we only read the
5636
+ // event arg, causing all writes to fall back to the first DB agent alphabetically.
5599
5637
 
5600
5638
  function registerBrainExtractionHook(
5601
5639
  api: any,
@@ -5604,7 +5642,7 @@ function registerBrainExtractionHook(
5604
5642
  fallbackAgentId: string,
5605
5643
  ): void {
5606
5644
  try {
5607
- api.on("agent_end", async (event: any) => {
5645
+ api.on("agent_end", async (event: any, ctx: any) => {
5608
5646
  try {
5609
5647
  // Extract messages from event — agent_end provides the conversation
5610
5648
  const messages: any[] = event?.messages || event?.context?.messages || [];
@@ -5635,9 +5673,14 @@ function registerBrainExtractionHook(
5635
5673
  // Skip trivial exchanges
5636
5674
  if (lastUser.length < 20 || lastAssistant.length < 30) return;
5637
5675
 
5638
- // Resolve agent identity from event context
5639
- const agentId = event?.agentId
5676
+ // ── Agent identity resolution (FIX) ──────────────────────────
5677
+ // Priority: ctx.agentId (from PluginHookAgentContext, the CORRECT source)
5678
+ // > event fields (legacy compat)
5679
+ // > fallbackAgentId (last resort)
5680
+ const agentId = ctx?.agentId
5681
+ || event?.agentId
5640
5682
  || event?.context?.agentId
5683
+ || ctx?.sessionKey?.split?.(":")?.[0]
5641
5684
  || event?.sessionKey?.split?.(":")?.[0]
5642
5685
  || fallbackAgentId
5643
5686
  || "";
@@ -5653,12 +5696,16 @@ function registerBrainExtractionHook(
5653
5696
  // Use raw agentId
5654
5697
  }
5655
5698
 
5656
- const sessionKey = event?.sessionKey || event?.context?.sessionKey || undefined;
5699
+ const sessionKey = ctx?.sessionKey || event?.sessionKey || event?.context?.sessionKey || undefined;
5700
+
5701
+ api.logger.debug?.(`[ofiere-brain] agent_end identity: ctx.agentId=${ctx?.agentId || "(none)"} event.agentId=${event?.agentId || "(none)"} resolved=${resolvedAgentId}`);
5657
5702
 
5658
- // ── Fast Stream: Write L1_focus ──
5703
+ // ── Fast Stream: Write L1_focus + L2_episode in parallel ──
5659
5704
  const rawContent = `User: ${lastUser}\nAssistant: ${lastAssistant}`;
5705
+ const immediateWrites: Promise<any>[] = [];
5706
+
5660
5707
  if (rawContent.length > 50) {
5661
- await supabase.from("agent_memories").insert({
5708
+ immediateWrites.push(supabase.from("agent_memories").insert({
5662
5709
  user_id: userId,
5663
5710
  agent_id: resolvedAgentId,
5664
5711
  tier: "L1_focus",
@@ -5667,16 +5714,14 @@ function registerBrainExtractionHook(
5667
5714
  importance: 3,
5668
5715
  context_key: sessionKey ? `conversation:${sessionKey}` : null,
5669
5716
  expires_at: new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString(),
5670
- });
5717
+ }));
5671
5718
  }
5672
5719
 
5673
- // ── Lightweight L2 episode summary ──
5674
5720
  if (lastUser.length + lastAssistant.length > 200) {
5675
5721
  const userSummary = lastUser.split(/[.!?]\s/)[0];
5676
5722
  const assistSummary = lastAssistant.split(/[.!?]\s/)[0];
5677
5723
  const episodeSummary = `User asked: "${userSummary}". Agent responded: "${assistSummary}"`;
5678
-
5679
- await supabase.from("agent_memories").insert({
5724
+ immediateWrites.push(supabase.from("agent_memories").insert({
5680
5725
  user_id: userId,
5681
5726
  agent_id: resolvedAgentId,
5682
5727
  tier: "L2_episode",
@@ -5684,70 +5729,86 @@ function registerBrainExtractionHook(
5684
5729
  source: "auto",
5685
5730
  importance: 4,
5686
5731
  context_key: sessionKey ? `episode:${sessionKey}` : null,
5687
- });
5732
+ }));
5688
5733
  }
5689
5734
 
5690
- // ── Rule-based L3/L4 extraction from user message ──
5691
- // 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.
5692
5740
  const factPatterns = [
5693
5741
  /(?:my name is|i'm called|call me)\s+(\w+)/gi,
5694
5742
  /(?:i (?:work|am working) (?:at|for|with))\s+(.+?)(?:\.|,|$)/gi,
5695
5743
  /(?:i (?:like|love|prefer|enjoy|hate|dislike))\s+(.+?)(?:\.|,|$)/gi,
5696
5744
  /(?:i (?:am|'m))\s+(?:a|an)\s+(.+?)(?:\.|,|$)/gi,
5697
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[] = [];
5698
5754
 
5699
5755
  for (const pattern of factPatterns) {
5700
- const matches = lastUser.matchAll(pattern);
5701
- for (const match of matches) {
5756
+ for (const match of lastUser.matchAll(pattern)) {
5702
5757
  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,
5758
+ candidates.push({
5713
5759
  tier: "L3_pattern",
5714
5760
  content: fact,
5715
- source: "auto",
5761
+ contextKey: `fact:${fact.slice(0, 50).toLowerCase().replace(/\s+/g, "_")}`,
5716
5762
  importance: 6,
5717
- context_key: contextKey,
5718
5763
  });
5719
5764
  }
5720
5765
  }
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
5766
  for (const pattern of rulePatterns) {
5729
- const matches = lastUser.matchAll(pattern);
5730
- for (const match of matches) {
5767
+ for (const match of lastUser.matchAll(pattern)) {
5731
5768
  const rule = match[0].trim();
5732
5769
  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,
5770
+ candidates.push({
5742
5771
  tier: "L4_rule",
5743
5772
  content: rule,
5744
- source: "auto",
5773
+ contextKey: `rule:${rule.slice(0, 50).toLowerCase().replace(/\s+/g, "_")}`,
5745
5774
  importance: 7,
5746
- context_key: contextKey,
5747
5775
  });
5748
5776
  }
5749
5777
  }
5750
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
+
5751
5812
  api.logger.debug?.(`[ofiere-brain] Extracted memory for agent=${resolvedAgentId} session=${sessionKey || "none"}`);
5752
5813
  } catch (e) {
5753
5814
  // Silent — brain extraction must never block chat
@@ -5762,79 +5823,98 @@ function registerBrainExtractionHook(
5762
5823
  }
5763
5824
 
5764
5825
  // ── Brain Context Bootstrap Injection ──────────────────────────────────────
5765
- // Queries the agent's active memories at boot and injects them into the system
5766
- // prompt via api.on("before_prompt_build"). Uses the TMT hierarchy:
5826
+ // Injects the calling agent's active memories into the system prompt via
5827
+ // api.on("before_prompt_build"). Uses the TMT hierarchy:
5767
5828
  // L1_focus — active working memory (not expired, sorted by importance)
5768
- // L4_rule — operational guardrails (source='reflection', sorted by importance)
5829
+ // L4_rule — operational guardrails (non-superseded, sorted by importance)
5769
5830
  // L5_persona — identity context (permanent, top 3)
5770
- // Runs once at registration time (~20ms, 3 parallel queries).
5831
+ //
5832
+ // FIX (v4.30.0): Previously loaded brain context ONCE at registration time
5833
+ // using fallbackAgentId (= first agent alphabetically). Now the hook is
5834
+ // DYNAMIC — it reads ctx.agentId from the PluginHookAgentContext passed by
5835
+ // OpenClaw on every prompt build, resolves the correct agent, and loads
5836
+ // that specific agent's brain. Each agent gets its own memories.
5771
5837
 
5772
- async function injectBrainContext(
5838
+ function registerBrainContextHook(
5773
5839
  api: any,
5774
5840
  supabase: SupabaseClient,
5775
5841
  userId: string,
5776
- agentId: string,
5777
- ): Promise<void> {
5778
- if (!agentId) return;
5779
-
5780
- const now = new Date().toISOString();
5842
+ fallbackAgentId: string,
5843
+ ): void {
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;
5781
5847
 
5782
- const [l1Res, l4Res, l5Res] = await Promise.all([
5783
- // L1: Active working memory (not expired)
5784
- supabase.from("agent_memories")
5785
- .select("content, importance")
5786
- .eq("user_id", userId)
5787
- .eq("agent_id", agentId)
5788
- .eq("tier", "L1_focus")
5789
- .gt("decay_score", 0.1)
5790
- .or(`expires_at.is.null,expires_at.gt.${now}`)
5791
- .order("importance", { ascending: false })
5792
- .limit(5),
5793
- // L4: Guardrails (non-superseded reflections)
5794
- supabase.from("agent_memories")
5795
- .select("content, importance")
5796
- .eq("user_id", userId)
5797
- .eq("agent_id", agentId)
5798
- .eq("tier", "L4_rule")
5799
- .is("superseded_by", null)
5800
- .order("importance", { ascending: false })
5801
- .limit(10),
5802
- // L5: Persona (permanent identity)
5803
- supabase.from("agent_memories")
5804
- .select("content")
5805
- .eq("user_id", userId)
5806
- .eq("agent_id", agentId)
5807
- .eq("tier", "L5_persona")
5808
- .order("importance", { ascending: false })
5809
- .limit(3),
5810
- ]);
5848
+ try {
5849
+ api.on("before_prompt_build", async (_event: any, ctx: any) => {
5850
+ try {
5851
+ // ── Resolve which agent is building its prompt ──
5852
+ const ctxAgentId = ctx?.agentId || "";
5853
+ let resolvedAgentId = fallbackAgentId;
5811
5854
 
5812
- const l1 = l1Res.data || [];
5813
- const l4 = l4Res.data || [];
5814
- const l5 = l5Res.data || [];
5855
+ if (ctxAgentId && !isSystemName(ctxAgentId)) {
5856
+ try {
5857
+ const resolved = await resolveAgentId(ctxAgentId, userId, supabase);
5858
+ if (resolved) resolvedAgentId = resolved;
5859
+ } catch {
5860
+ // Fall through to fallback
5861
+ }
5862
+ }
5815
5863
 
5816
- if (l1.length === 0 && l4.length === 0 && l5.length === 0) return;
5864
+ if (!resolvedAgentId) return;
5817
5865
 
5818
- const sections: string[] = [];
5866
+ // ── Check cache ──
5867
+ const cached = brainCache.get(resolvedAgentId);
5868
+ if (cached && Date.now() - cached.at < CACHE_TTL_MS) {
5869
+ return cached.text ? { appendSystemContext: cached.text } : undefined;
5870
+ }
5819
5871
 
5820
- if (l5.length > 0) {
5821
- sections.push("### Identity (L5_persona)\n" + l5.map((m: any) => `- ${m.content}`).join("\n"));
5822
- }
5872
+ // ── Query brain memories for THIS specific agent ──
5873
+ // v4.30.0: Single combined query instead of 3 separate tier queries.
5874
+ const now = new Date().toISOString();
5823
5875
 
5824
- if (l4.length > 0) {
5825
- sections.push("### ⚠️ Guardrails (L4_rule — DO NOT violate)\n" + l4.map((m: any) => `- ${m.content}`).join("\n"));
5826
- }
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);
5893
+
5894
+ if (l1.length === 0 && l4.length === 0 && l5.length === 0) {
5895
+ brainCache.set(resolvedAgentId, { text: "", at: Date.now() });
5896
+ return;
5897
+ }
5827
5898
 
5828
- if (l1.length > 0) {
5829
- sections.push("### Active Focus (L1_focus)\n" + l1.map((m: any) => `- ${m.content}`).join("\n"));
5830
- }
5899
+ const sections: string[] = [];
5900
+ if (l5.length > 0) {
5901
+ sections.push("### Identity (L5_persona)\n" + l5.map((m: any) => `- ${m.content}`).join("\n"));
5902
+ }
5903
+ if (l4.length > 0) {
5904
+ sections.push("### ⚠️ Guardrails (L4_rule — DO NOT violate)\n" + l4.map((m: any) => `- ${m.content}`).join("\n"));
5905
+ }
5906
+ if (l1.length > 0) {
5907
+ sections.push("### Active Focus (L1_focus)\n" + l1.map((m: any) => `- ${m.content}`).join("\n"));
5908
+ }
5831
5909
 
5832
- const brainContext = `<agent-brain>\n## Your Brain Context (TMT)\n\n${sections.join("\n\n")}\n</agent-brain>`;
5910
+ const brainContext = `<agent-brain>\n## Your Brain Context (TMT)\n\n${sections.join("\n\n")}\n</agent-brain>`;
5911
+ brainCache.set(resolvedAgentId, { text: brainContext, at: Date.now() });
5833
5912
 
5834
- try {
5835
- api.on("before_prompt_build", () => ({
5836
- appendSystemContext: brainContext,
5837
- }));
5913
+ return { appendSystemContext: brainContext };
5914
+ } catch (e) {
5915
+ api.logger.debug?.(`[ofiere-brain] before_prompt_build error: ${e instanceof Error ? e.message : e}`);
5916
+ }
5917
+ });
5838
5918
  } catch {
5839
5919
  api.logger.debug?.("[ofiere] Could not register brain context hook — appendSystemContext may not be supported");
5840
5920
  }