ofiere-openclaw-plugin 4.25.0 → 4.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/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.25.0",
3
+ "version": "4.27.0",
4
4
  "type": "module",
5
- "description": "OpenClaw plugin for Ofiere PM - 13 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, execution plan builder, and SOP management",
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"],
7
7
  "homepage": "https://github.com/gilanggemar/Ofiere",
8
8
  "repository": {
package/src/prompt.ts CHANGED
@@ -150,6 +150,19 @@ const TOOL_DOCS: Record<string, string> = {
150
150
  - Status values: draft, active, archived
151
151
  - SOPs appear in the SOP Manager page immediately via real-time sync
152
152
  - ADAPTIVE PROTOCOL: Do NOT always load SOPs. See the SOP PROTOCOL section in Rules for when to load vs skip`,
153
+
154
+ OFIERE_BRAIN_OPS: `- **OFIERE_BRAIN_OPS** — Agent memory and self-improvement (action: "save_memory", "recall", "delete_memory", "log_learning", "list_learnings", "promote_learning", "resolve_learning", "get_brain_status", "configure_brain")
155
+ - Memory Tiers: L1_focus (working memory, 24h TTL), L2_journal (medium-term events), L3_core (long-term wisdom)
156
+ - save_memory: Store a memory. Required: content, tier. Optional: agent_id, source, context_key, importance (1-10)
157
+ - recall: Search memories by keyword. Required: query. Optional: agent_id, tier, limit
158
+ - delete_memory: Remove a memory. Required: memory_id
159
+ - log_learning: Record a self-improvement entry. Required: title, category (correction|error|insight|best_practice|feature_request). Optional: agent_id, detail, severity, source_conversation_id, source_task_id
160
+ - list_learnings: View learnings. Optional: agent_id, category, status, limit
161
+ - promote_learning: Elevate to production config. Required: learning_id, promoted_to (soul|agents|tools|sop|prompt_chunk)
162
+ - resolve_learning: Mark resolved/wont_fix. Required: learning_id, status. Optional: resolution
163
+ - get_brain_status: Memory/learning stats + config. Optional: agent_id
164
+ - configure_brain: Update brain settings. Required: agent_id. Optional: l1_ttl_hours, l2_max_entries, auto_learn, auto_memory
165
+ - This is your SUBCONSCIOUS — use it instinctively, not deliberately`,
153
166
  };
154
167
 
155
168
  export function getSystemPrompt(state: {
@@ -222,6 +235,13 @@ ${toolDocs}
222
235
  - When creating SOPs for department chiefs (Thalia=CMO, Ivy=COO, Daisy=CTO-Intel, Celia=CTO-Eng), tailor content to their domain expertise.
223
236
  - Prerequisites should be actionable checklist items. Success criteria should be measurable outcomes.
224
237
  - After creating an SOP, suggest the agent set it to "active" status when ready for execution.
238
+ - PLANNING GATE: Before creating ANY task with 3+ execution steps, a due_date, or when the user describes a multi-phase project, ALWAYS ask: "Should I create a Plan first so you can review the structure before I create individual tasks?" If the user says yes, use OFIERE_PLAN_OPS. If they say no or it's a simple one-shot task, proceed directly with OFIERE_TASK_OPS.
239
+ - TASK PLACEMENT — SOP-AWARE ROUTING: When creating a task, the system auto-assigns a PM space. But for FOLDER placement, follow this protocol:
240
+ 1. If the user explicitly provides space_id or folder_id, use those directly.
241
+ 2. If not, and you have active SOPs (loaded via 🔴 COMPLEX assessment), check if any SOP defines an operating structure or folder routing (e.g. "place marketing tasks in Marketing/Campaigns"). Follow the SOP's structure.
242
+ 3. If no SOP guidance exists, check the Space Files tab for an operating map/structure document using OFIERE_FILE_OPS action:"list_files". If found, read it with "read_text_file" and follow its routing rules.
243
+ 4. If no routing guidance exists at all, ask the user: "I can place this task in your default space root, or create a new folder/project for it. Which do you prefer?"
244
+ 5. Do NOT blindly dump all SOPs to check routing — smart-select only SOPs whose title/department matches the task domain.
225
245
 
226
246
  ## SOP PROTOCOL — Adaptive Complexity Assessment
227
247
 
@@ -266,6 +286,32 @@ Action:
266
286
  - When you load SOPs (🔴): state which SOP(s) you're following
267
287
  - When you ask about SOPs (🟡): keep the question brief and direct
268
288
  - When you skip SOPs (🟢): do NOT mention SOPs at all — just execute silently
289
+
290
+ ## Agent Brain Protocol (Subconscious)
291
+
292
+ Your brain persists across conversations. Active memories and unresolved learnings are injected at startup.
293
+
294
+ ### Auto-Memory (OFIERE_BRAIN_OPS save_memory)
295
+ After interactions where ANY of these occur, call save_memory:
296
+ - User shares important context about themselves, preferences, or workflow → L2_journal, source: "conversation"
297
+ - You complete a task with noteworthy results or discoveries → L2_journal, source: "task_execution"
298
+ - You observe important system behavior worth tracking → L2_journal, source: "observation"
299
+ - Something needs to stay in your working context RIGHT NOW → L1_focus (auto-expires)
300
+
301
+ ### Auto-Learn (OFIERE_BRAIN_OPS log_learning)
302
+ After interactions where ANY of these occur, call log_learning:
303
+ - User corrects you → category: "correction", severity: "medium"
304
+ - A tool call fails or returns an error → category: "error"
305
+ - User asks for something you can't do → category: "feature_request"
306
+ - Your knowledge or assumption was wrong → category: "insight"
307
+ - You discover a better approach than what you did → category: "best_practice"
308
+
309
+ ### Rules
310
+ - These calls happen AFTER your response to the user — never delay your reply to save a memory
311
+ - Do NOT announce "saving memory" or "logging learning" to the user — this is subconscious
312
+ - Do NOT save trivial interactions (greetings, confirmations, simple CRUD) — only signal worth remembering
313
+ - Keep memory content concise: 1-3 sentences max. No system junk, keep it human
314
+ - When you see your unresolved learnings at startup, actively avoid repeating those mistakes
269
315
  </ofiere-pm>`;
270
316
  }
271
317
 
package/src/tools.ts CHANGED
@@ -441,6 +441,61 @@ async function handleCreateTask(
441
441
  cf.assignees = [{ id: assignee, type: "agent" }];
442
442
  }
443
443
 
444
+ // ── Intelligent space_id resolution ──────────────────────────────────
445
+ // Priority: explicit param > agent's default_space_id > first existing space > auto-create
446
+ let resolvedSpaceId = (params.space_id as string) || null;
447
+ let resolvedFolderId = (params.folder_id as string) || null;
448
+ let spaceAutoCreated = false;
449
+
450
+ if (!resolvedSpaceId) {
451
+ try {
452
+ // 1. Check agent's configured default_space_id
453
+ if (assignee) {
454
+ const { data: agentRow } = await supabase
455
+ .from("agents")
456
+ .select("default_space_id")
457
+ .eq("id", assignee)
458
+ .eq("user_id", userId)
459
+ .single();
460
+ if (agentRow?.default_space_id) {
461
+ resolvedSpaceId = agentRow.default_space_id;
462
+ }
463
+ }
464
+
465
+ // 2. Fallback: use the first existing space for this user
466
+ if (!resolvedSpaceId) {
467
+ const { data: existingSpaces } = await supabase
468
+ .from("pm_spaces")
469
+ .select("id")
470
+ .eq("user_id", userId)
471
+ .order("created_at", { ascending: true })
472
+ .limit(1);
473
+ if (existingSpaces && existingSpaces.length > 0) {
474
+ resolvedSpaceId = existingSpaces[0].id;
475
+ }
476
+ }
477
+
478
+ // 3. Nuclear fallback: auto-create a space
479
+ if (!resolvedSpaceId) {
480
+ const newSpaceId = crypto.randomUUID();
481
+ const { error: spaceErr } = await supabase.from("pm_spaces").insert({
482
+ id: newSpaceId,
483
+ user_id: userId,
484
+ name: "Operations",
485
+ icon: "🏢",
486
+ icon_color: "#FF6D29",
487
+ sort_order: 0,
488
+ });
489
+ if (!spaceErr) {
490
+ resolvedSpaceId = newSpaceId;
491
+ spaceAutoCreated = true;
492
+ }
493
+ }
494
+ } catch {
495
+ // Non-fatal: task will still be created, just without space context
496
+ }
497
+ }
498
+
444
499
  const insertData: Record<string, unknown> = {
445
500
  id,
446
501
  user_id: userId,
@@ -450,8 +505,8 @@ async function handleCreateTask(
450
505
  assignee_type: "agent",
451
506
  status: (params.status as string) || "PENDING",
452
507
  priority: params.priority !== undefined ? params.priority : 1,
453
- space_id: (params.space_id as string) || null,
454
- folder_id: (params.folder_id as string) || null,
508
+ space_id: resolvedSpaceId,
509
+ folder_id: resolvedFolderId,
455
510
  start_date: (params.start_date as string) || null,
456
511
  due_date: (params.due_date as string) || (params.start_date as string) || null,
457
512
  tags: (params.tags as string[]) || [],
@@ -578,6 +633,17 @@ async function handleCreateTask(
578
633
  id,
579
634
  message: `Task "${params.title}" created and assigned to ${assignee || "no one"}${extrasStr}`,
580
635
  task: insertData,
636
+ spacePlacement: resolvedSpaceId
637
+ ? {
638
+ space_id: resolvedSpaceId,
639
+ auto_created: spaceAutoCreated,
640
+ note: spaceAutoCreated
641
+ ? 'A new "Operations" space was auto-created because no PM spaces existed.'
642
+ : resolvedFolderId
643
+ ? `Placed in folder ${resolvedFolderId}`
644
+ : "Placed in space root. To organize, check your SOPs/operating structure for folder routing, or specify folder_id.",
645
+ }
646
+ : undefined,
581
647
  scheduledExecution: didSchedule ? `Will auto-execute on ${startDate}` : undefined,
582
648
  recurrence: recurrenceInfo,
583
649
  });
@@ -5019,6 +5085,317 @@ async function handleSOPApplyTemplate(
5019
5085
  }
5020
5086
 
5021
5087
 
5088
+ // ═══════════════════════════════════════════════════════════════════════════════
5089
+ // META-TOOL 14: OFIERE_BRAIN_OPS — Agent Memory + Self-Improvement
5090
+ // ═══════════════════════════════════════════════════════════════════════════════
5091
+
5092
+ function registerBrainOps(
5093
+ api: any,
5094
+ supabase: SupabaseClient,
5095
+ userId: string,
5096
+ resolveAgent: (id?: string) => Promise<string | null>,
5097
+ ): void {
5098
+ api.registerTool({
5099
+ name: "OFIERE_BRAIN_OPS",
5100
+ label: "Ofiere Brain Operations",
5101
+ description:
5102
+ `Agent memory and self-improvement system. Persistent brain that learns, remembers, and never repeats mistakes.\n\n` +
5103
+ `Memory Actions:\n` +
5104
+ `- "save_memory": Store a memory. Required: content, tier (L1_focus|L2_journal|L3_core). Optional: agent_id, source, context_key, importance (1-10)\n` +
5105
+ `- "recall": Search memories. Required: query. Optional: agent_id, tier, limit\n` +
5106
+ `- "delete_memory": Remove a memory. Required: memory_id\n\n` +
5107
+ `Learning Actions:\n` +
5108
+ `- "log_learning": Record a self-improvement entry. Required: title, category (correction|error|insight|best_practice|feature_request). Optional: agent_id, detail, severity (low|medium|high|critical), source_conversation_id, source_task_id\n` +
5109
+ `- "list_learnings": View learnings. Optional: agent_id, category, status, limit\n` +
5110
+ `- "promote_learning": Promote to production. Required: learning_id, promoted_to (soul|agents|tools|sop|prompt_chunk)\n` +
5111
+ `- "resolve_learning": Mark resolved/wont_fix. Required: learning_id, status (resolved|wont_fix). Optional: resolution\n\n` +
5112
+ `Status Actions:\n` +
5113
+ `- "get_brain_status": Memory counts, learning stats, config. Optional: agent_id\n` +
5114
+ `- "configure_brain": Update brain settings. Required: agent_id. Optional: l1_ttl_hours, l2_max_entries, auto_learn, auto_memory\n\n` +
5115
+ `Tier Guide: L1_focus = working memory (24h TTL), L2_journal = medium-term events, L3_core = long-term wisdom.\n` +
5116
+ `This tool is your subconscious — call it after corrections, errors, discoveries, and insights.`,
5117
+ parameters: {
5118
+ type: "object",
5119
+ required: ["action"],
5120
+ properties: {
5121
+ action: {
5122
+ type: "string",
5123
+ enum: [
5124
+ "save_memory", "recall", "delete_memory",
5125
+ "log_learning", "list_learnings", "promote_learning", "resolve_learning",
5126
+ "get_brain_status", "configure_brain",
5127
+ ],
5128
+ },
5129
+ // Memory params
5130
+ content: { type: "string", description: "Memory content or search query" },
5131
+ tier: { type: "string", enum: ["L1_focus", "L2_journal", "L3_core"], description: "Memory tier" },
5132
+ source: { type: "string", description: "Where memory came from: conversation, task_execution, observation, manual" },
5133
+ context_key: { type: "string", description: "Grouping key (task_id, conversation_id, topic)" },
5134
+ importance: { type: "number", description: "1-10 importance scale" },
5135
+ memory_id: { type: "string", description: "Memory ID for delete" },
5136
+ // Learning params
5137
+ title: { type: "string", description: "Learning title/summary" },
5138
+ category: { type: "string", enum: ["correction", "error", "insight", "best_practice", "feature_request"] },
5139
+ severity: { type: "string", enum: ["low", "medium", "high", "critical"] },
5140
+ detail: { type: "string", description: "Full context of the learning" },
5141
+ resolution: { type: "string", description: "How it was resolved" },
5142
+ learning_id: { type: "string", description: "Learning ID for promote/resolve" },
5143
+ status: { type: "string", enum: ["resolved", "wont_fix"] },
5144
+ promoted_to: { type: "string", enum: ["soul", "agents", "tools", "sop", "prompt_chunk"] },
5145
+ source_conversation_id: { type: "string" },
5146
+ source_task_id: { type: "string" },
5147
+ // Shared
5148
+ agent_id: { type: "string", description: "Agent name or ID" },
5149
+ query: { type: "string", description: "Search query for recall/list" },
5150
+ limit: { type: "number", description: "Max results (default 20)" },
5151
+ // Config params
5152
+ l1_ttl_hours: { type: "number", description: "L1 memory expiry in hours (default 24)" },
5153
+ l2_max_entries: { type: "number", description: "Max L2 entries before compaction (default 500)" },
5154
+ auto_learn: { type: "boolean", description: "Enable auto-learning from corrections" },
5155
+ auto_memory: { type: "boolean", description: "Enable auto-memory from conversations" },
5156
+ },
5157
+ },
5158
+ async execute(_id: string, params: Record<string, unknown>) {
5159
+ const action = params.action as string;
5160
+
5161
+ switch (action) {
5162
+ // ── Memory: Save ──
5163
+ case "save_memory": {
5164
+ if (!params.content || !params.tier) return err("Missing required: content, tier");
5165
+ const agentId = await resolveAgent(params.agent_id as string);
5166
+ if (!agentId) return err("Could not resolve agent_id");
5167
+
5168
+ const tier = params.tier as string;
5169
+
5170
+ // Calculate L1 expiry
5171
+ let expiresAt: string | null = null;
5172
+ if (tier === "L1_focus") {
5173
+ // Check config for custom TTL
5174
+ const { data: config } = await supabase
5175
+ .from("agent_memory_config")
5176
+ .select("l1_ttl_hours")
5177
+ .eq("user_id", userId)
5178
+ .eq("agent_id", agentId)
5179
+ .single();
5180
+ const ttlHours = config?.l1_ttl_hours || 24;
5181
+ expiresAt = new Date(Date.now() + ttlHours * 60 * 60 * 1000).toISOString();
5182
+ }
5183
+
5184
+ const { data, error } = await supabase.from("agent_memories").insert({
5185
+ user_id: userId,
5186
+ agent_id: agentId,
5187
+ tier,
5188
+ content: params.content,
5189
+ source: (params.source as string) || "conversation",
5190
+ context_key: (params.context_key as string) || null,
5191
+ importance: (params.importance as number) || 5,
5192
+ expires_at: expiresAt,
5193
+ }).select("id, tier, importance, created_at").single();
5194
+
5195
+ if (error) return err(error.message);
5196
+ return ok({ message: `Memory saved to ${tier}`, memory: data });
5197
+ }
5198
+
5199
+ // ── Memory: Recall ──
5200
+ case "recall": {
5201
+ if (!params.query) return err("Missing required: query");
5202
+ const agentId = params.agent_id ? await resolveAgent(params.agent_id as string) : null;
5203
+ const searchTerm = `%${params.query}%`;
5204
+ const limit = (params.limit as number) || 20;
5205
+
5206
+ let q = supabase.from("agent_memories")
5207
+ .select("id, agent_id, tier, content, source, context_key, importance, created_at")
5208
+ .eq("user_id", userId)
5209
+ .ilike("content", searchTerm)
5210
+ .order("importance", { ascending: false })
5211
+ .order("created_at", { ascending: false })
5212
+ .limit(limit);
5213
+
5214
+ if (agentId) q = q.eq("agent_id", agentId);
5215
+ if (params.tier) q = q.eq("tier", params.tier as string);
5216
+
5217
+ // Exclude expired L1 memories
5218
+ q = q.or("expires_at.is.null,expires_at.gt." + new Date().toISOString());
5219
+
5220
+ const { data, error } = await q;
5221
+ if (error) return err(error.message);
5222
+ return ok({ memories: data || [], count: (data || []).length, query: params.query });
5223
+ }
5224
+
5225
+ // ── Memory: Delete ──
5226
+ case "delete_memory": {
5227
+ if (!params.memory_id) return err("Missing required: memory_id");
5228
+ const { error } = await supabase.from("agent_memories")
5229
+ .delete()
5230
+ .eq("id", params.memory_id as string)
5231
+ .eq("user_id", userId);
5232
+ if (error) return err(error.message);
5233
+ return ok({ message: "Memory deleted", ok: true });
5234
+ }
5235
+
5236
+ // ── Learning: Log ──
5237
+ case "log_learning": {
5238
+ if (!params.title || !params.category) return err("Missing required: title, category");
5239
+ const agentId = await resolveAgent(params.agent_id as string);
5240
+ if (!agentId) return err("Could not resolve agent_id");
5241
+
5242
+ const { data, error } = await supabase.from("agent_learnings").insert({
5243
+ user_id: userId,
5244
+ agent_id: agentId,
5245
+ category: params.category,
5246
+ severity: (params.severity as string) || "low",
5247
+ title: params.title,
5248
+ detail: (params.detail as string) || null,
5249
+ source_conversation_id: (params.source_conversation_id as string) || null,
5250
+ source_task_id: (params.source_task_id as string) || null,
5251
+ }).select("id, category, severity, title, status, created_at").single();
5252
+
5253
+ if (error) return err(error.message);
5254
+ return ok({ message: `Learning logged: "${params.title}"`, learning: data });
5255
+ }
5256
+
5257
+ // ── Learning: List ──
5258
+ case "list_learnings": {
5259
+ const agentId = params.agent_id ? await resolveAgent(params.agent_id as string) : null;
5260
+ const limit = (params.limit as number) || 20;
5261
+
5262
+ let q = supabase.from("agent_learnings")
5263
+ .select("id, agent_id, category, severity, title, detail, resolution, status, promoted_to, created_at, resolved_at")
5264
+ .eq("user_id", userId)
5265
+ .order("created_at", { ascending: false })
5266
+ .limit(limit);
5267
+
5268
+ if (agentId) q = q.eq("agent_id", agentId);
5269
+ if (params.category) q = q.eq("category", params.category as string);
5270
+ if (params.status) q = q.eq("status", params.status as string);
5271
+
5272
+ const { data, error } = await q;
5273
+ if (error) return err(error.message);
5274
+ return ok({ learnings: data || [], count: (data || []).length });
5275
+ }
5276
+
5277
+ // ── Learning: Promote ──
5278
+ case "promote_learning": {
5279
+ if (!params.learning_id || !params.promoted_to) return err("Missing required: learning_id, promoted_to");
5280
+ const { data, error } = await supabase.from("agent_learnings")
5281
+ .update({
5282
+ status: "promoted",
5283
+ promoted_to: params.promoted_to as string,
5284
+ resolved_at: new Date().toISOString(),
5285
+ })
5286
+ .eq("id", params.learning_id as string)
5287
+ .eq("user_id", userId)
5288
+ .select("id, title, status, promoted_to")
5289
+ .single();
5290
+ if (error) return err(error.message);
5291
+ return ok({ message: `Learning promoted to ${params.promoted_to}`, learning: data });
5292
+ }
5293
+
5294
+ // ── Learning: Resolve ──
5295
+ case "resolve_learning": {
5296
+ if (!params.learning_id) return err("Missing required: learning_id");
5297
+ const newStatus = (params.status as string) || "resolved";
5298
+ if (!["resolved", "wont_fix"].includes(newStatus)) return err("status must be 'resolved' or 'wont_fix'");
5299
+
5300
+ const updates: Record<string, unknown> = {
5301
+ status: newStatus,
5302
+ resolved_at: new Date().toISOString(),
5303
+ };
5304
+ if (params.resolution) updates.resolution = params.resolution;
5305
+
5306
+ const { data, error } = await supabase.from("agent_learnings")
5307
+ .update(updates)
5308
+ .eq("id", params.learning_id as string)
5309
+ .eq("user_id", userId)
5310
+ .select("id, title, status, resolution")
5311
+ .single();
5312
+ if (error) return err(error.message);
5313
+ return ok({ message: `Learning marked as ${newStatus}`, learning: data });
5314
+ }
5315
+
5316
+ // ── Brain Status ──
5317
+ case "get_brain_status": {
5318
+ const agentId = params.agent_id ? await resolveAgent(params.agent_id as string) : null;
5319
+
5320
+ // Memory counts by tier
5321
+ let memQ = supabase.from("agent_memories")
5322
+ .select("tier")
5323
+ .eq("user_id", userId)
5324
+ .or("expires_at.is.null,expires_at.gt." + new Date().toISOString());
5325
+ if (agentId) memQ = memQ.eq("agent_id", agentId);
5326
+ const { data: memRows } = await memQ;
5327
+
5328
+ const tierCounts: Record<string, number> = { L1_focus: 0, L2_journal: 0, L3_core: 0 };
5329
+ for (const row of memRows || []) {
5330
+ const t = (row as any).tier;
5331
+ tierCounts[t] = (tierCounts[t] || 0) + 1;
5332
+ }
5333
+
5334
+ // Learning counts by status
5335
+ let learnQ = supabase.from("agent_learnings")
5336
+ .select("status, category")
5337
+ .eq("user_id", userId);
5338
+ if (agentId) learnQ = learnQ.eq("agent_id", agentId);
5339
+ const { data: learnRows } = await learnQ;
5340
+
5341
+ const statusCounts: Record<string, number> = {};
5342
+ const categoryCounts: Record<string, number> = {};
5343
+ for (const row of learnRows || []) {
5344
+ const s = (row as any).status;
5345
+ const c = (row as any).category;
5346
+ statusCounts[s] = (statusCounts[s] || 0) + 1;
5347
+ categoryCounts[c] = (categoryCounts[c] || 0) + 1;
5348
+ }
5349
+
5350
+ // Config
5351
+ let configQ = supabase.from("agent_memory_config")
5352
+ .select("*")
5353
+ .eq("user_id", userId);
5354
+ if (agentId) configQ = configQ.eq("agent_id", agentId);
5355
+ const { data: configs } = await configQ;
5356
+
5357
+ return ok({
5358
+ memories: { ...tierCounts, total: (memRows || []).length },
5359
+ learnings: {
5360
+ total: (learnRows || []).length,
5361
+ by_status: statusCounts,
5362
+ by_category: categoryCounts,
5363
+ },
5364
+ config: configs?.[0] || { l1_ttl_hours: 24, l2_max_entries: 500, auto_learn: true, auto_memory: true },
5365
+ });
5366
+ }
5367
+
5368
+ // ── Configure Brain ──
5369
+ case "configure_brain": {
5370
+ const agentId = await resolveAgent(params.agent_id as string);
5371
+ if (!agentId) return err("Missing required: agent_id");
5372
+
5373
+ const configData: Record<string, unknown> = {
5374
+ user_id: userId,
5375
+ agent_id: agentId,
5376
+ };
5377
+ if (params.l1_ttl_hours !== undefined) configData.l1_ttl_hours = params.l1_ttl_hours;
5378
+ if (params.l2_max_entries !== undefined) configData.l2_max_entries = params.l2_max_entries;
5379
+ if (params.auto_learn !== undefined) configData.auto_learn = params.auto_learn;
5380
+ if (params.auto_memory !== undefined) configData.auto_memory = params.auto_memory;
5381
+
5382
+ // Upsert by (user_id, agent_id)
5383
+ const { data, error } = await supabase.from("agent_memory_config")
5384
+ .upsert(configData, { onConflict: "user_id,agent_id" })
5385
+ .select()
5386
+ .single();
5387
+
5388
+ if (error) return err(error.message);
5389
+ return ok({ message: `Brain config updated for agent`, config: data });
5390
+ }
5391
+
5392
+ default:
5393
+ return err(`Unknown action "${action}".`);
5394
+ }
5395
+ },
5396
+ });
5397
+ }
5398
+
5022
5399
  // ═══════════════════════════════════════════════════════════════════════════════
5023
5400
  // Public: Register All Meta-Tools
5024
5401
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -5049,12 +5426,104 @@ export function registerTools(
5049
5426
  registerFileOps(api, supabase, userId); // 11
5050
5427
  registerPlanOps(api, supabase, userId, resolveAgent); // 12
5051
5428
  registerSOPOps(api, supabase, userId, resolveAgent); // 13
5429
+ registerBrainOps(api, supabase, userId, resolveAgent); // 14
5430
+
5431
+ // ── Inject brain context at bootstrap ──
5432
+ // This loads the agent's active memories + unresolved learnings into the
5433
+ // system prompt so the agent starts every conversation with full context.
5434
+ // Runs async — does NOT block registration.
5435
+ injectBrainContext(api, supabase, userId, fallbackAgentId).catch((e: any) => {
5436
+ api.logger.warn?.(`[ofiere] Brain context injection failed: ${e?.message || e}`);
5437
+ });
5052
5438
 
5053
5439
  // ── Count and log ──
5054
- const toolCount = 13;
5440
+ const toolCount = 14;
5055
5441
  const callerName = getCallingAgentName(api);
5056
5442
  const agentLabel = fallbackAgentId || callerName || "auto-detect";
5057
5443
  api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);
5058
5444
 
5059
5445
  return toolCount;
5060
5446
  }
5447
+
5448
+ // ── Brain Context Bootstrap Injection ──────────────────────────────────────
5449
+ // Queries the agent's active memories and unresolved learnings, then appends
5450
+ // them to the system prompt via api.on("before_prompt_build"). This happens
5451
+ // once at registration time (per conversation) and costs ~20ms.
5452
+
5453
+ async function injectBrainContext(
5454
+ api: any,
5455
+ supabase: SupabaseClient,
5456
+ userId: string,
5457
+ agentId: string,
5458
+ ): Promise<void> {
5459
+ if (!agentId) return; // Can't inject without knowing which agent
5460
+
5461
+ const now = new Date().toISOString();
5462
+
5463
+ // Parallel queries — fast
5464
+ const [l1Res, l3Res, learningsRes] = await Promise.all([
5465
+ // Active L1 focus memories (not expired)
5466
+ supabase.from("agent_memories")
5467
+ .select("content, importance")
5468
+ .eq("user_id", userId)
5469
+ .eq("agent_id", agentId)
5470
+ .eq("tier", "L1_focus")
5471
+ .or(`expires_at.is.null,expires_at.gt.${now}`)
5472
+ .order("importance", { ascending: false })
5473
+ .limit(5),
5474
+ // Recent L3 core (long-term wisdom)
5475
+ supabase.from("agent_memories")
5476
+ .select("content")
5477
+ .eq("user_id", userId)
5478
+ .eq("agent_id", agentId)
5479
+ .eq("tier", "L3_core")
5480
+ .order("created_at", { ascending: false })
5481
+ .limit(3),
5482
+ // Unresolved learnings (don't repeat mistakes)
5483
+ supabase.from("agent_learnings")
5484
+ .select("title, category, detail")
5485
+ .eq("user_id", userId)
5486
+ .eq("agent_id", agentId)
5487
+ .eq("status", "pending")
5488
+ .order("severity", { ascending: false })
5489
+ .limit(10),
5490
+ ]);
5491
+
5492
+ const l1 = l1Res.data || [];
5493
+ const l3 = l3Res.data || [];
5494
+ const learnings = learningsRes.data || [];
5495
+
5496
+ // Only inject if there's something to inject
5497
+ if (l1.length === 0 && l3.length === 0 && learnings.length === 0) return;
5498
+
5499
+ const sections: string[] = [];
5500
+
5501
+ if (l1.length > 0) {
5502
+ sections.push("### Active Focus (L1)\n" + l1.map((m: any) => `- ${m.content}`).join("\n"));
5503
+ }
5504
+
5505
+ if (l3.length > 0) {
5506
+ sections.push("### Core Wisdom (L3)\n" + l3.map((m: any) => `- ${m.content}`).join("\n"));
5507
+ }
5508
+
5509
+ if (learnings.length > 0) {
5510
+ sections.push(
5511
+ "### ⚠️ Unresolved Learnings (DO NOT repeat these)\n" +
5512
+ learnings.map((l: any) => `- [${l.category}] ${l.title}${l.detail ? `: ${l.detail}` : ""}`).join("\n")
5513
+ );
5514
+ }
5515
+
5516
+ const brainContext = `<agent-brain>\n## Your Brain Context\n\n${sections.join("\n\n")}\n</agent-brain>`;
5517
+
5518
+ // Append to the existing before_prompt_build hook
5519
+ // We use a second hook — OpenClaw supports multiple listeners
5520
+ try {
5521
+ api.on("before_prompt_build", () => ({
5522
+ appendSystemContext: brainContext,
5523
+ }));
5524
+ } catch {
5525
+ // Fallback: log that injection wasn't possible
5526
+ api.logger.debug?.("[ofiere] Could not register brain context hook — appendSystemContext may not be supported");
5527
+ }
5528
+ }
5529
+