ofiere-openclaw-plugin 4.27.2 → 4.28.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.2",
3
+ "version": "4.28.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/prompt.ts CHANGED
@@ -151,17 +151,21 @@ const TOOL_DOCS: Record<string, string> = {
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
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
154
+ OFIERE_BRAIN_OPS: `- **OFIERE_BRAIN_OPS** — Agent memory, knowledge graph, and self-improvement (TMT/MAGMA architecture)
155
+ - Memory Tiers: L1_focus (24h), L2_episode (days-weeks), L3_pattern (weeks-months), L4_rule (permanent guardrails), L5_persona (permanent identity)
156
+ - save_memory: Store a memory. Required: content, tier. Optional: agent_id, source (auto|manual|reflection|tool), context_key, importance (1-10)
157
+ - recall: Full-text search memories. Required: query. Optional: agent_id, tier, limit
158
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
159
+ - promote_memory: Move memory up a tier. Required: memory_id, new_tier
160
+ - log_learning: Record as L4_rule guardrail. Required: title, category. Optional: agent_id, detail, severity
161
+ - list_learnings: View active L4 guardrails. Optional: agent_id, category, limit
162
+ - resolve_learning: Supersede a guardrail. Required: memory_id. Optional: resolution
163
+ - save_entity: Add to Knowledge Graph. Required: label, node_type (entity|concept|event|action). Optional: agent_id, properties
164
+ - link_entities: Connect graph nodes. Required: source_id, target_id, relation_type. Optional: graph_type, weight, evidence
165
+ - query_graph: Search Knowledge Graph. Required: query. Optional: agent_id, node_type, limit
166
+ - start_trajectory: Begin recording execution trace. Optional: agent_id, conversation_id
167
+ - end_trajectory: Close trajectory. Required: trajectory_id, outcome (success|failure|partial). Optional: satisfaction_signal
168
+ - get_brain_status: Full brain dashboard — memory tiers, graph stats, trajectory stats. Optional: agent_id
165
169
  - This is your SUBCONSCIOUS — use it instinctively, not deliberately`,
166
170
  };
167
171
 
@@ -287,31 +291,45 @@ Action:
287
291
  - When you ask about SOPs (🟡): keep the question brief and direct
288
292
  - When you skip SOPs (🟢): do NOT mention SOPs at all — just execute silently
289
293
 
290
- ## Agent Brain Protocol (Subconscious)
294
+ ## Agent Brain Protocol (TMT Subconscious)
291
295
 
292
- Your brain persists across conversations. Active memories and unresolved learnings are injected at startup.
296
+ Your brain persists across conversations via the TMT (Temporal Memory Tree) hierarchy. At startup, your L5 persona, L4 guardrails, and L1 focus memories are injected automatically.
293
297
 
294
298
  ### Auto-Memory (OFIERE_BRAIN_OPS save_memory)
295
299
  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
+ - User shares important context → L2_episode, source: "auto"
301
+ - You complete a task with noteworthy results → L2_episode, source: "tool"
302
+ - You observe important system behavior → L2_episode, source: "auto"
303
+ - Something needs immediate working context → L1_focus (auto-expires 24h)
304
+ - A pattern emerges across multiple episodes → L3_pattern, source: "reflection"
300
305
 
301
306
  ### Auto-Learn (OFIERE_BRAIN_OPS log_learning)
302
- After interactions where ANY of these occur, call log_learning:
307
+ After interactions where ANY of these occur, call log_learning (stored as L4_rule):
303
308
  - User corrects you → category: "correction", severity: "medium"
304
309
  - A tool call fails or returns an error → category: "error"
305
310
  - User asks for something you can't do → category: "feature_request"
306
311
  - Your knowledge or assumption was wrong → category: "insight"
307
- - You discover a better approach than what you did → category: "best_practice"
312
+ - You discover a better approach → category: "best_practice"
313
+
314
+ ### Knowledge Graph (OFIERE_BRAIN_OPS save_entity/link_entities)
315
+ When you encounter key entities, concepts, or causal relationships:
316
+ - Create entities for users, projects, tools, or recurring concepts
317
+ - Link entities with typed edges (causes, depends_on, related_to)
318
+ - Query the graph when reasoning about relationships or dependencies
319
+
320
+ ### Trajectory Recording (OFIERE_BRAIN_OPS start_trajectory/end_trajectory)
321
+ For complex multi-step tasks:
322
+ - Call start_trajectory before executing a tool chain
323
+ - Call end_trajectory with outcome when the chain completes
324
+ - This feeds the ReasoningBank for autonomous self-improvement
308
325
 
309
326
  ### Rules
310
327
  - These calls happen AFTER your response to the user — never delay your reply to save a memory
311
328
  - Do NOT announce "saving memory" or "logging learning" to the user — this is subconscious
312
329
  - Do NOT save trivial interactions (greetings, confirmations, simple CRUD) — only signal worth remembering
313
330
  - 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
331
+ - When you see your L4 guardrails at startup, actively avoid violating them
332
+ - Use promote_memory when a pattern solidifies: L2→L3→L4→L5
315
333
  </ofiere-pm>`;
316
334
  }
317
335
 
package/src/tools.ts CHANGED
@@ -5099,56 +5099,67 @@ function registerBrainOps(
5099
5099
  name: "OFIERE_BRAIN_OPS",
5100
5100
  label: "Ofiere Brain Operations",
5101
5101
  description:
5102
- `Agent memory and self-improvement system. Persistent brain that learns, remembers, and never repeats mistakes.\n\n` +
5102
+ `Agent memory, knowledge graph, and self-improvement system (TMT/MAGMA architecture).\n\n` +
5103
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.`,
5104
+ `- "save_memory": Store a memory. Required: content, tier. Optional: agent_id, source, context_key, importance (1-10)\n` +
5105
+ `- "recall": Full-text search memories. Required: query. Optional: agent_id, tier, limit\n` +
5106
+ `- "delete_memory": Remove a memory. Required: memory_id\n` +
5107
+ `- "promote_memory": Move memory up a tier. Required: memory_id, new_tier\n\n` +
5108
+ `Learning Actions (stored as L4_rule memories):\n` +
5109
+ `- "log_learning": Record a self-improvement entry as L4 guardrail. Required: title, category (correction|error|insight|best_practice|feature_request). Optional: agent_id, detail, severity\n` +
5110
+ `- "list_learnings": View L4 guardrails. Optional: agent_id, category, limit\n` +
5111
+ `- "resolve_learning": Supersede a guardrail. Required: memory_id. Optional: resolution\n\n` +
5112
+ `Knowledge Graph Actions:\n` +
5113
+ `- "save_entity": Add entity/concept/event to graph. Required: label, node_type (entity|concept|event|action). Optional: agent_id, properties\n` +
5114
+ `- "link_entities": Connect two nodes. Required: source_id, target_id, relation_type. Optional: graph_type (semantic|temporal|causal|entity), weight, evidence\n` +
5115
+ `- "query_graph": Find nodes + edges. Required: query. Optional: agent_id, node_type, limit\n\n` +
5116
+ `Trajectory Actions:\n` +
5117
+ `- "start_trajectory": Begin recording execution trace. Optional: agent_id, conversation_id\n` +
5118
+ `- "end_trajectory": Close trajectory. Required: trajectory_id, outcome (success|failure|partial). Optional: satisfaction_signal\n\n` +
5119
+ `Status:\n` +
5120
+ `- "get_brain_status": Full brain dashboard — memory tiers, graph stats, trajectory stats. Optional: agent_id\n\n` +
5121
+ `Tiers: L1_focus (transient, 24h), L2_episode (days-weeks), L3_pattern (weeks-months), L4_rule (permanent guardrails), L5_persona (permanent identity).`,
5117
5122
  parameters: {
5118
5123
  type: "object",
5119
5124
  required: ["action"],
5120
5125
  properties: {
5121
5126
  action: {
5122
5127
  type: "string",
5123
- description: "The operation to perform: save_memory, recall, delete_memory, log_learning, list_learnings, promote_learning, resolve_learning, get_brain_status, configure_brain",
5128
+ description: "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",
5124
5129
  },
5125
5130
  // Memory params
5126
- content: { type: "string", description: "Memory content or search query" },
5127
- tier: { type: "string", enum: ["L1_focus", "L2_journal", "L3_core"], description: "Memory tier" },
5128
- source: { type: "string", description: "Where memory came from: conversation, task_execution, observation, manual" },
5131
+ content: { type: "string", description: "Memory content" },
5132
+ tier: { type: "string", enum: ["L1_focus", "L2_episode", "L3_pattern", "L4_rule", "L5_persona"], description: "Memory tier" },
5133
+ new_tier: { type: "string", enum: ["L1_focus", "L2_episode", "L3_pattern", "L4_rule", "L5_persona"], description: "Target tier for promote" },
5134
+ source: { type: "string", enum: ["auto", "manual", "reflection", "tool"], description: "Memory source" },
5129
5135
  context_key: { type: "string", description: "Grouping key (task_id, conversation_id, topic)" },
5130
5136
  importance: { type: "number", description: "1-10 importance scale" },
5131
- memory_id: { type: "string", description: "Memory ID for delete" },
5137
+ memory_id: { type: "string", description: "Memory ID for delete/promote/resolve" },
5132
5138
  // Learning params
5133
5139
  title: { type: "string", description: "Learning title/summary" },
5134
5140
  category: { type: "string", enum: ["correction", "error", "insight", "best_practice", "feature_request"] },
5135
5141
  severity: { type: "string", enum: ["low", "medium", "high", "critical"] },
5136
5142
  detail: { type: "string", description: "Full context of the learning" },
5137
5143
  resolution: { type: "string", description: "How it was resolved" },
5138
- learning_id: { type: "string", description: "Learning ID for promote/resolve" },
5139
- status: { type: "string", enum: ["pending", "resolved", "wont_fix", "promoted"] },
5140
- promoted_to: { type: "string", enum: ["soul", "agents", "tools", "sop", "prompt_chunk"] },
5141
- source_conversation_id: { type: "string" },
5142
- source_task_id: { type: "string" },
5144
+ // Knowledge Graph params
5145
+ label: { type: "string", description: "Node label for save_entity" },
5146
+ node_type: { type: "string", enum: ["entity", "concept", "event", "action"], description: "Graph node type" },
5147
+ properties: { type: "object", description: "Arbitrary properties for graph node" },
5148
+ source_id: { type: "string", description: "Source node ID for link_entities" },
5149
+ target_id: { type: "string", description: "Target node ID for link_entities" },
5150
+ relation_type: { type: "string", description: "Edge relation type (e.g. 'causes', 'related_to', 'depends_on')" },
5151
+ graph_type: { type: "string", enum: ["semantic", "temporal", "causal", "entity"], description: "Graph type for edge" },
5152
+ weight: { type: "number", description: "Edge weight (0-1)" },
5153
+ evidence: { type: "string", description: "Evidence text for edge" },
5154
+ // Trajectory params
5155
+ trajectory_id: { type: "string", description: "Trajectory ID for end_trajectory" },
5156
+ outcome: { type: "string", enum: ["success", "failure", "partial"], description: "Trajectory outcome" },
5157
+ satisfaction_signal: { type: "string", description: "User satisfaction signal" },
5158
+ conversation_id: { type: "string", description: "Conversation ID for trajectory" },
5143
5159
  // Shared
5144
5160
  agent_id: { type: "string", description: "Agent name or ID" },
5145
- query: { type: "string", description: "Search query for recall/list" },
5161
+ query: { type: "string", description: "Search query for recall/query_graph" },
5146
5162
  limit: { type: "number", description: "Max results (default 20)" },
5147
- // Config params
5148
- l1_ttl_hours: { type: "number", description: "L1 memory expiry in hours (default 24)" },
5149
- l2_max_entries: { type: "number", description: "Max L2 entries before compaction (default 500)" },
5150
- auto_learn: { type: "boolean", description: "Enable auto-learning from corrections" },
5151
- auto_memory: { type: "boolean", description: "Enable auto-memory from conversations" },
5152
5163
  },
5153
5164
  },
5154
5165
  async execute(_id: string, params: Record<string, unknown>) {
@@ -5167,19 +5178,9 @@ function registerBrainOps(
5167
5178
  if (!agentId) return err("Could not resolve agent_id");
5168
5179
 
5169
5180
  const tier = params.tier as string;
5170
-
5171
- // Calculate L1 expiry
5172
5181
  let expiresAt: string | null = null;
5173
5182
  if (tier === "L1_focus") {
5174
- // Check config for custom TTL
5175
- const { data: config } = await supabase
5176
- .from("agent_memory_config")
5177
- .select("l1_ttl_hours")
5178
- .eq("user_id", userId)
5179
- .eq("agent_id", agentId)
5180
- .single();
5181
- const ttlHours = config?.l1_ttl_hours || 24;
5182
- expiresAt = new Date(Date.now() + ttlHours * 60 * 60 * 1000).toISOString();
5183
+ expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
5183
5184
  }
5184
5185
 
5185
5186
  const { data, error } = await supabase.from("agent_memories").insert({
@@ -5187,27 +5188,31 @@ function registerBrainOps(
5187
5188
  agent_id: agentId,
5188
5189
  tier,
5189
5190
  content: params.content,
5190
- source: (params.source as string) || "conversation",
5191
+ source: (params.source as string) || "auto",
5191
5192
  context_key: (params.context_key as string) || null,
5192
5193
  importance: (params.importance as number) || 5,
5194
+ decay_score: 1.0,
5193
5195
  expires_at: expiresAt,
5194
- }).select("id, tier, importance, created_at").single();
5196
+ }).select("id, tier, importance, decay_score, created_at").single();
5195
5197
 
5196
5198
  if (error) return err(error.message);
5197
5199
  return ok({ message: `Memory saved to ${tier}`, memory: data });
5198
5200
  }
5199
5201
 
5200
- // ── Memory: Recall ──
5202
+ // ── Memory: Recall (Full-text search) ──
5201
5203
  case "recall": {
5202
5204
  if (!params.query) return err("Missing required: query");
5203
5205
  const agentId = params.agent_id ? await resolveAgent(params.agent_id as string) : null;
5204
- const searchTerm = `%${params.query}%`;
5205
5206
  const limit = (params.limit as number) || 20;
5207
+ const queryText = params.query as string;
5206
5208
 
5209
+ // Use full-text search via tsvector
5207
5210
  let q = supabase.from("agent_memories")
5208
- .select("id, agent_id, tier, content, source, context_key, importance, created_at")
5211
+ .select("id, agent_id, tier, content, source, context_key, importance, decay_score, access_count, created_at, last_accessed_at")
5209
5212
  .eq("user_id", userId)
5210
- .ilike("content", searchTerm)
5213
+ .textSearch("search_vector", queryText, { type: "plain" })
5214
+ .gt("decay_score", 0.05)
5215
+ .or("expires_at.is.null,expires_at.gt." + new Date().toISOString())
5211
5216
  .order("importance", { ascending: false })
5212
5217
  .order("created_at", { ascending: false })
5213
5218
  .limit(limit);
@@ -5215,12 +5220,31 @@ function registerBrainOps(
5215
5220
  if (agentId) q = q.eq("agent_id", agentId);
5216
5221
  if (params.tier) q = q.eq("tier", params.tier as string);
5217
5222
 
5218
- // Exclude expired L1 memories
5219
- q = q.or("expires_at.is.null,expires_at.gt." + new Date().toISOString());
5220
-
5221
5223
  const { data, error } = await q;
5222
- if (error) return err(error.message);
5223
- return ok({ memories: data || [], count: (data || []).length, query: params.query });
5224
+ if (error) {
5225
+ // Fallback to ILIKE if tsvector search fails
5226
+ let fallback = supabase.from("agent_memories")
5227
+ .select("id, agent_id, tier, content, source, context_key, importance, decay_score, created_at")
5228
+ .eq("user_id", userId)
5229
+ .ilike("content", `%${queryText}%`)
5230
+ .gt("decay_score", 0.05)
5231
+ .or("expires_at.is.null,expires_at.gt." + new Date().toISOString())
5232
+ .order("importance", { ascending: false })
5233
+ .limit(limit);
5234
+ if (agentId) fallback = fallback.eq("agent_id", agentId);
5235
+ if (params.tier) fallback = fallback.eq("tier", params.tier as string);
5236
+ const { data: fbData, error: fbErr } = await fallback;
5237
+ if (fbErr) return err(fbErr.message);
5238
+ return ok({ memories: fbData || [], count: (fbData || []).length, query: queryText, search_mode: "fallback" });
5239
+ }
5240
+
5241
+ // Touch access_count + last_accessed_at for FadeMem
5242
+ if (data && data.length > 0) {
5243
+ const ids = data.map((m: any) => m.id);
5244
+ supabase.rpc("touch_memories", { memory_ids: ids }).then(() => {}).catch(() => {});
5245
+ }
5246
+
5247
+ return ok({ memories: data || [], count: (data || []).length, query: queryText, search_mode: "fulltext" });
5224
5248
  }
5225
5249
 
5226
5250
  // ── Memory: Delete ──
@@ -5234,7 +5258,22 @@ function registerBrainOps(
5234
5258
  return ok({ message: "Memory deleted", ok: true });
5235
5259
  }
5236
5260
 
5237
- // ── Learning: Log ──
5261
+ // ── Memory: Promote ──
5262
+ case "promote_memory": {
5263
+ if (!params.memory_id) return err("Missing required: memory_id");
5264
+ if (!params.new_tier) return err("Missing required: new_tier");
5265
+ const { data, error } = await supabase.from("agent_memories")
5266
+ .update({ tier: params.new_tier as string })
5267
+ .eq("id", params.memory_id as string)
5268
+ .eq("user_id", userId)
5269
+ .select("id, tier, content, importance")
5270
+ .single();
5271
+ if (error) return err(error.message);
5272
+ if (!data) return err("Memory not found");
5273
+ return ok({ message: `Memory promoted to ${params.new_tier}`, memory: data });
5274
+ }
5275
+
5276
+ // ── Learning: Log (→ L4_rule memory) ──
5238
5277
  case "log_learning": {
5239
5278
  {
5240
5279
  const missing: string[] = [];
@@ -5245,163 +5284,253 @@ function registerBrainOps(
5245
5284
  const agentId = await resolveAgent(params.agent_id as string);
5246
5285
  if (!agentId) return err("Could not resolve agent_id");
5247
5286
 
5248
- const { data, error } = await supabase.from("agent_learnings").insert({
5287
+ const content = `[${params.category}] ${params.title}${params.detail ? `: ${params.detail}` : ""}`;
5288
+ const severityToImportance: Record<string, number> = { low: 5, medium: 7, high: 8, critical: 10 };
5289
+ const imp = severityToImportance[(params.severity as string) || "low"] || 5;
5290
+
5291
+ const { data, error } = await supabase.from("agent_memories").insert({
5249
5292
  user_id: userId,
5250
5293
  agent_id: agentId,
5251
- category: params.category,
5252
- severity: (params.severity as string) || "low",
5253
- title: params.title,
5254
- detail: (params.detail as string) || null,
5255
- source_conversation_id: (params.source_conversation_id as string) || null,
5256
- source_task_id: (params.source_task_id as string) || null,
5257
- }).select("id, category, severity, title, status, created_at").single();
5294
+ tier: "L4_rule",
5295
+ content,
5296
+ source: "reflection",
5297
+ context_key: (params.category as string) || null,
5298
+ importance: imp,
5299
+ decay_score: 1.0,
5300
+ }).select("id, tier, content, importance, created_at").single();
5258
5301
 
5259
5302
  if (error) return err(error.message);
5260
- return ok({ message: `Learning logged: "${params.title}"`, learning: data });
5303
+ return ok({ message: `Learning logged as L4 guardrail: "${params.title}"`, memory: data });
5261
5304
  }
5262
5305
 
5263
- // ── Learning: List ──
5306
+ // ── Learning: List (→ L4_rule query) ──
5264
5307
  case "list_learnings": {
5265
5308
  const agentId = params.agent_id ? await resolveAgent(params.agent_id as string) : null;
5266
5309
  const limit = (params.limit as number) || 20;
5267
5310
 
5268
- let q = supabase.from("agent_learnings")
5269
- .select("id, agent_id, category, severity, title, detail, resolution, status, promoted_to, created_at, resolved_at")
5311
+ let q = supabase.from("agent_memories")
5312
+ .select("id, agent_id, tier, content, context_key, importance, decay_score, created_at, superseded_by")
5270
5313
  .eq("user_id", userId)
5314
+ .eq("tier", "L4_rule")
5315
+ .is("superseded_by", null)
5316
+ .order("importance", { ascending: false })
5271
5317
  .order("created_at", { ascending: false })
5272
5318
  .limit(limit);
5273
5319
 
5274
5320
  if (agentId) q = q.eq("agent_id", agentId);
5275
- if (params.category) q = q.eq("category", params.category as string);
5276
- if (params.status) q = q.eq("status", params.status as string);
5321
+ if (params.category) q = q.eq("context_key", params.category as string);
5277
5322
 
5278
5323
  const { data, error } = await q;
5279
5324
  if (error) return err(error.message);
5280
5325
  return ok({ learnings: data || [], count: (data || []).length });
5281
5326
  }
5282
5327
 
5283
- // ── Learning: Promote ──
5284
- case "promote_learning": {
5285
- {
5286
- const missing: string[] = [];
5287
- if (!params.learning_id) missing.push("learning_id");
5288
- if (!params.promoted_to) missing.push("promoted_to");
5289
- if (missing.length > 0) return err(`Missing required: ${missing.join(", ")}`);
5328
+ // ── Learning: Resolve (supersede) ──
5329
+ case "resolve_learning": {
5330
+ if (!params.memory_id) return err("Missing required: memory_id");
5331
+
5332
+ if (params.resolution) {
5333
+ // Create a superseding memory with the resolution
5334
+ const agentId = await resolveAgent(params.agent_id as string);
5335
+ const { data: newMem } = await supabase.from("agent_memories").insert({
5336
+ user_id: userId,
5337
+ agent_id: agentId || "system",
5338
+ tier: "L4_rule",
5339
+ content: `[resolved] ${params.resolution}`,
5340
+ source: "reflection",
5341
+ importance: 5,
5342
+ decay_score: 1.0,
5343
+ }).select("id").single();
5344
+
5345
+ if (newMem) {
5346
+ await supabase.from("agent_memories")
5347
+ .update({ superseded_by: newMem.id })
5348
+ .eq("id", params.memory_id as string)
5349
+ .eq("user_id", userId);
5350
+ }
5351
+ return ok({ message: "Learning resolved with new guardrail", superseded_by: newMem?.id });
5290
5352
  }
5291
- const { data, error } = await supabase.from("agent_learnings")
5292
- .update({
5293
- status: "promoted",
5294
- promoted_to: params.promoted_to as string,
5295
- resolved_at: new Date().toISOString(),
5296
- })
5297
- .eq("id", params.learning_id as string)
5353
+
5354
+ // Just delete if no resolution
5355
+ const { error } = await supabase.from("agent_memories")
5356
+ .delete()
5357
+ .eq("id", params.memory_id as string)
5358
+ .eq("user_id", userId);
5359
+ if (error) return err(error.message);
5360
+ return ok({ message: "Learning removed", ok: true });
5361
+ }
5362
+
5363
+ // ── Knowledge Graph: Save Entity ──
5364
+ case "save_entity": {
5365
+ if (!params.label) return err("Missing required: label");
5366
+ if (!params.node_type) return err("Missing required: node_type");
5367
+ const agentId = await resolveAgent(params.agent_id as string);
5368
+ if (!agentId) return err("Could not resolve agent_id");
5369
+
5370
+ const { data, error } = await supabase.from("knowledge_graph_nodes").insert({
5371
+ user_id: userId,
5372
+ agent_id: agentId,
5373
+ label: params.label as string,
5374
+ node_type: params.node_type as string,
5375
+ properties: (params.properties as Record<string, unknown>) || {},
5376
+ }).select("id, label, node_type, created_at").single();
5377
+
5378
+ if (error) return err(error.message);
5379
+ return ok({ message: `Entity "${params.label}" saved`, node: data });
5380
+ }
5381
+
5382
+ // ── Knowledge Graph: Link Entities ──
5383
+ case "link_entities": {
5384
+ const missing: string[] = [];
5385
+ if (!params.source_id) missing.push("source_id");
5386
+ if (!params.target_id) missing.push("target_id");
5387
+ if (!params.relation_type) missing.push("relation_type");
5388
+ if (missing.length > 0) return err(`Missing required: ${missing.join(", ")}`);
5389
+
5390
+ const { data, error } = await supabase.from("knowledge_graph_edges").insert({
5391
+ user_id: userId,
5392
+ source_id: params.source_id as string,
5393
+ target_id: params.target_id as string,
5394
+ relation_type: params.relation_type as string,
5395
+ graph_type: (params.graph_type as string) || "semantic",
5396
+ weight: (params.weight as number) || 1.0,
5397
+ evidence: (params.evidence as string) || null,
5398
+ }).select("id, relation_type, graph_type, weight, created_at").single();
5399
+
5400
+ if (error) return err(error.message);
5401
+ return ok({ message: `Edge "${params.relation_type}" created`, edge: data });
5402
+ }
5403
+
5404
+ // ── Knowledge Graph: Query ──
5405
+ case "query_graph": {
5406
+ if (!params.query) return err("Missing required: query");
5407
+ const agentId = params.agent_id ? await resolveAgent(params.agent_id as string) : null;
5408
+ const limit = (params.limit as number) || 20;
5409
+
5410
+ let q = supabase.from("knowledge_graph_nodes")
5411
+ .select("id, agent_id, label, node_type, properties, created_at")
5298
5412
  .eq("user_id", userId)
5299
- .select("id, title, status, promoted_to")
5300
- .single();
5413
+ .ilike("label", `%${params.query}%`)
5414
+ .limit(limit);
5415
+
5416
+ if (agentId) q = q.eq("agent_id", agentId);
5417
+ if (params.node_type) q = q.eq("node_type", params.node_type as string);
5418
+
5419
+ const { data: nodes, error } = await q;
5301
5420
  if (error) return err(error.message);
5302
- return ok({ message: `Learning promoted to ${params.promoted_to}`, learning: data });
5421
+
5422
+ // Fetch edges for found nodes
5423
+ let edges: any[] = [];
5424
+ if (nodes && nodes.length > 0) {
5425
+ const nodeIds = nodes.map((n: any) => n.id);
5426
+ const { data: edgeData } = await supabase.from("knowledge_graph_edges")
5427
+ .select("id, source_id, target_id, relation_type, graph_type, weight, evidence")
5428
+ .eq("user_id", userId)
5429
+ .or(`source_id.in.(${nodeIds.join(",")}),target_id.in.(${nodeIds.join(",")})`)
5430
+ .limit(50);
5431
+ edges = edgeData || [];
5432
+ }
5433
+
5434
+ return ok({ nodes: nodes || [], edges, node_count: (nodes || []).length, edge_count: edges.length });
5303
5435
  }
5304
5436
 
5305
- // ── Learning: Resolve ──
5306
- case "resolve_learning": {
5307
- if (!params.learning_id) return err("Missing required: learning_id");
5308
- const newStatus = (params.status as string) || "resolved";
5309
- if (!["resolved", "wont_fix"].includes(newStatus)) return err("status must be 'resolved' or 'wont_fix'");
5437
+ // ── Trajectory: Start ──
5438
+ case "start_trajectory": {
5439
+ const agentId = await resolveAgent(params.agent_id as string);
5440
+ if (!agentId) return err("Could not resolve agent_id");
5441
+
5442
+ const { data, error } = await supabase.from("execution_trajectories").insert({
5443
+ user_id: userId,
5444
+ agent_id: agentId,
5445
+ conversation_id: (params.conversation_id as string) || null,
5446
+ trajectory: [],
5447
+ outcome: "pending",
5448
+ analysis_status: "pending",
5449
+ }).select("id, agent_id, created_at").single();
5450
+
5451
+ if (error) return err(error.message);
5452
+ return ok({ message: "Trajectory recording started", trajectory: data });
5453
+ }
5454
+
5455
+ // ── Trajectory: End ──
5456
+ case "end_trajectory": {
5457
+ if (!params.trajectory_id) return err("Missing required: trajectory_id");
5458
+ if (!params.outcome) return err("Missing required: outcome");
5310
5459
 
5311
5460
  const updates: Record<string, unknown> = {
5312
- status: newStatus,
5313
- resolved_at: new Date().toISOString(),
5461
+ outcome: params.outcome as string,
5462
+ completed_at: new Date().toISOString(),
5314
5463
  };
5315
- if (params.resolution) updates.resolution = params.resolution;
5464
+ if (params.satisfaction_signal) updates.user_satisfaction_signal = params.satisfaction_signal;
5316
5465
 
5317
- const { data, error } = await supabase.from("agent_learnings")
5466
+ const { data, error } = await supabase.from("execution_trajectories")
5318
5467
  .update(updates)
5319
- .eq("id", params.learning_id as string)
5468
+ .eq("id", params.trajectory_id as string)
5320
5469
  .eq("user_id", userId)
5321
- .select("id, title, status, resolution")
5470
+ .select("id, outcome, completed_at")
5322
5471
  .single();
5323
5472
  if (error) return err(error.message);
5324
- return ok({ message: `Learning marked as ${newStatus}`, learning: data });
5473
+ return ok({ message: `Trajectory closed: ${params.outcome}`, trajectory: data });
5325
5474
  }
5326
5475
 
5327
5476
  // ── Brain Status ──
5328
5477
  case "get_brain_status": {
5329
5478
  const agentId = params.agent_id ? await resolveAgent(params.agent_id as string) : null;
5330
-
5331
- // Memory counts by tier
5332
- let memQ = supabase.from("agent_memories")
5333
- .select("tier")
5334
- .eq("user_id", userId)
5335
- .or("expires_at.is.null,expires_at.gt." + new Date().toISOString());
5336
- if (agentId) memQ = memQ.eq("agent_id", agentId);
5337
- const { data: memRows } = await memQ;
5338
-
5339
- const tierCounts: Record<string, number> = { L1_focus: 0, L2_journal: 0, L3_core: 0 };
5340
- for (const row of memRows || []) {
5479
+ const now = new Date().toISOString();
5480
+
5481
+ // Parallel queries for speed
5482
+ const memFilter = agentId
5483
+ ? supabase.from("agent_memories").select("tier").eq("user_id", userId).eq("agent_id", agentId).or("expires_at.is.null,expires_at.gt." + now)
5484
+ : supabase.from("agent_memories").select("tier").eq("user_id", userId).or("expires_at.is.null,expires_at.gt." + now);
5485
+
5486
+ const graphFilter = agentId
5487
+ ? supabase.from("knowledge_graph_nodes").select("id").eq("user_id", userId).eq("agent_id", agentId)
5488
+ : supabase.from("knowledge_graph_nodes").select("id").eq("user_id", userId);
5489
+
5490
+ const trajFilter = agentId
5491
+ ? supabase.from("execution_trajectories").select("outcome").eq("user_id", userId).eq("agent_id", agentId)
5492
+ : supabase.from("execution_trajectories").select("outcome").eq("user_id", userId);
5493
+
5494
+ const consolFilter = agentId
5495
+ ? supabase.from("memory_consolidation_log").select("operation").eq("user_id", userId).eq("agent_id", agentId).limit(50)
5496
+ : supabase.from("memory_consolidation_log").select("operation").eq("user_id", userId).limit(50);
5497
+
5498
+ const [memRes, graphRes, edgeRes, trajRes, consolRes] = await Promise.all([
5499
+ memFilter,
5500
+ graphFilter,
5501
+ supabase.from("knowledge_graph_edges").select("id").eq("user_id", userId),
5502
+ trajFilter,
5503
+ consolFilter,
5504
+ ]);
5505
+
5506
+ const tierCounts: Record<string, number> = { L1_focus: 0, L2_episode: 0, L3_pattern: 0, L4_rule: 0, L5_persona: 0 };
5507
+ for (const row of memRes.data || []) {
5341
5508
  const t = (row as any).tier;
5342
5509
  tierCounts[t] = (tierCounts[t] || 0) + 1;
5343
5510
  }
5344
5511
 
5345
- // Learning counts by status
5346
- let learnQ = supabase.from("agent_learnings")
5347
- .select("status, category")
5348
- .eq("user_id", userId);
5349
- if (agentId) learnQ = learnQ.eq("agent_id", agentId);
5350
- const { data: learnRows } = await learnQ;
5351
-
5352
- const statusCounts: Record<string, number> = {};
5353
- const categoryCounts: Record<string, number> = {};
5354
- for (const row of learnRows || []) {
5355
- const s = (row as any).status;
5356
- const c = (row as any).category;
5357
- statusCounts[s] = (statusCounts[s] || 0) + 1;
5358
- categoryCounts[c] = (categoryCounts[c] || 0) + 1;
5512
+ const trajOutcomes: Record<string, number> = {};
5513
+ for (const row of trajRes.data || []) {
5514
+ const o = (row as any).outcome;
5515
+ trajOutcomes[o] = (trajOutcomes[o] || 0) + 1;
5359
5516
  }
5360
5517
 
5361
- // Config
5362
- let configQ = supabase.from("agent_memory_config")
5363
- .select("*")
5364
- .eq("user_id", userId);
5365
- if (agentId) configQ = configQ.eq("agent_id", agentId);
5366
- const { data: configs } = await configQ;
5518
+ const consolOps: Record<string, number> = {};
5519
+ for (const row of consolRes.data || []) {
5520
+ const o = (row as any).operation;
5521
+ consolOps[o] = (consolOps[o] || 0) + 1;
5522
+ }
5367
5523
 
5368
5524
  return ok({
5369
- memories: { ...tierCounts, total: (memRows || []).length },
5370
- learnings: {
5371
- total: (learnRows || []).length,
5372
- by_status: statusCounts,
5373
- by_category: categoryCounts,
5374
- },
5375
- config: configs?.[0] || { l1_ttl_hours: 24, l2_max_entries: 500, auto_learn: true, auto_memory: true },
5525
+ memories: { ...tierCounts, total: (memRes.data || []).length },
5526
+ knowledge_graph: { nodes: (graphRes.data || []).length, edges: (edgeRes.data || []).length },
5527
+ trajectories: { total: (trajRes.data || []).length, by_outcome: trajOutcomes },
5528
+ consolidation: { operations: (consolRes.data || []).length, by_type: consolOps },
5376
5529
  });
5377
5530
  }
5378
5531
 
5379
- // ── Configure Brain ──
5380
- case "configure_brain": {
5381
- const agentId = await resolveAgent(params.agent_id as string);
5382
- if (!agentId) return err("Missing required: agent_id");
5383
-
5384
- const configData: Record<string, unknown> = {
5385
- user_id: userId,
5386
- agent_id: agentId,
5387
- };
5388
- if (params.l1_ttl_hours !== undefined) configData.l1_ttl_hours = params.l1_ttl_hours;
5389
- if (params.l2_max_entries !== undefined) configData.l2_max_entries = params.l2_max_entries;
5390
- if (params.auto_learn !== undefined) configData.auto_learn = params.auto_learn;
5391
- if (params.auto_memory !== undefined) configData.auto_memory = params.auto_memory;
5392
-
5393
- // Upsert by (user_id, agent_id)
5394
- const { data, error } = await supabase.from("agent_memory_config")
5395
- .upsert(configData, { onConflict: "user_id,agent_id" })
5396
- .select()
5397
- .single();
5398
-
5399
- if (error) return err(error.message);
5400
- return ok({ message: `Brain config updated for agent`, config: data });
5401
- }
5402
-
5403
5532
  default:
5404
- return err(`Unknown action "${action}".`);
5533
+ 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`);
5405
5534
  }
5406
5535
  },
5407
5536
  });
@@ -5447,6 +5576,12 @@ export function registerTools(
5447
5576
  api.logger.warn?.(`[ofiere] Brain context injection failed: ${e?.message || e}`);
5448
5577
  });
5449
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
+
5450
5585
  // ── Count and log ──
5451
5586
  const toolCount = 14;
5452
5587
  const callerName = getCallingAgentName(api);
@@ -5456,10 +5591,183 @@ export function registerTools(
5456
5591
  return toolCount;
5457
5592
  }
5458
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
+
5459
5764
  // ── Brain Context Bootstrap Injection ──────────────────────────────────────
5460
- // Queries the agent's active memories and unresolved learnings, then appends
5461
- // them to the system prompt via api.on("before_prompt_build"). This happens
5462
- // once at registration time (per conversation) and costs ~20ms.
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:
5767
+ // L1_focus — active working memory (not expired, sorted by importance)
5768
+ // L4_rule — operational guardrails (source='reflection', sorted by importance)
5769
+ // L5_persona — identity context (permanent, top 3)
5770
+ // Runs once at registration time (~20ms, 3 parallel queries).
5463
5771
 
5464
5772
  async function injectBrainContext(
5465
5773
  api: any,
@@ -5467,73 +5775,67 @@ async function injectBrainContext(
5467
5775
  userId: string,
5468
5776
  agentId: string,
5469
5777
  ): Promise<void> {
5470
- if (!agentId) return; // Can't inject without knowing which agent
5778
+ if (!agentId) return;
5471
5779
 
5472
5780
  const now = new Date().toISOString();
5473
5781
 
5474
- // Parallel queries fast
5475
- const [l1Res, l3Res, learningsRes] = await Promise.all([
5476
- // Active L1 focus memories (not expired)
5782
+ const [l1Res, l4Res, l5Res] = await Promise.all([
5783
+ // L1: Active working memory (not expired)
5477
5784
  supabase.from("agent_memories")
5478
5785
  .select("content, importance")
5479
5786
  .eq("user_id", userId)
5480
5787
  .eq("agent_id", agentId)
5481
5788
  .eq("tier", "L1_focus")
5789
+ .gt("decay_score", 0.1)
5482
5790
  .or(`expires_at.is.null,expires_at.gt.${now}`)
5483
5791
  .order("importance", { ascending: false })
5484
5792
  .limit(5),
5485
- // Recent L3 core (long-term wisdom)
5793
+ // L4: Guardrails (non-superseded reflections)
5486
5794
  supabase.from("agent_memories")
5487
- .select("content")
5795
+ .select("content, importance")
5488
5796
  .eq("user_id", userId)
5489
5797
  .eq("agent_id", agentId)
5490
- .eq("tier", "L3_core")
5491
- .order("created_at", { ascending: false })
5492
- .limit(3),
5493
- // Unresolved learnings (don't repeat mistakes)
5494
- supabase.from("agent_learnings")
5495
- .select("title, category, detail")
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")
5496
5805
  .eq("user_id", userId)
5497
5806
  .eq("agent_id", agentId)
5498
- .eq("status", "pending")
5499
- .order("severity", { ascending: false })
5500
- .limit(10),
5807
+ .eq("tier", "L5_persona")
5808
+ .order("importance", { ascending: false })
5809
+ .limit(3),
5501
5810
  ]);
5502
5811
 
5503
5812
  const l1 = l1Res.data || [];
5504
- const l3 = l3Res.data || [];
5505
- const learnings = learningsRes.data || [];
5813
+ const l4 = l4Res.data || [];
5814
+ const l5 = l5Res.data || [];
5506
5815
 
5507
- // Only inject if there's something to inject
5508
- if (l1.length === 0 && l3.length === 0 && learnings.length === 0) return;
5816
+ if (l1.length === 0 && l4.length === 0 && l5.length === 0) return;
5509
5817
 
5510
5818
  const sections: string[] = [];
5511
5819
 
5512
- if (l1.length > 0) {
5513
- sections.push("### Active Focus (L1)\n" + l1.map((m: any) => `- ${m.content}`).join("\n"));
5820
+ if (l5.length > 0) {
5821
+ sections.push("### Identity (L5_persona)\n" + l5.map((m: any) => `- ${m.content}`).join("\n"));
5514
5822
  }
5515
5823
 
5516
- if (l3.length > 0) {
5517
- sections.push("### Core Wisdom (L3)\n" + l3.map((m: any) => `- ${m.content}`).join("\n"));
5824
+ if (l4.length > 0) {
5825
+ sections.push("### ⚠️ Guardrails (L4_rule — DO NOT violate)\n" + l4.map((m: any) => `- ${m.content}`).join("\n"));
5518
5826
  }
5519
5827
 
5520
- if (learnings.length > 0) {
5521
- sections.push(
5522
- "### ⚠️ Unresolved Learnings (DO NOT repeat these)\n" +
5523
- learnings.map((l: any) => `- [${l.category}] ${l.title}${l.detail ? `: ${l.detail}` : ""}`).join("\n")
5524
- );
5828
+ if (l1.length > 0) {
5829
+ sections.push("### Active Focus (L1_focus)\n" + l1.map((m: any) => `- ${m.content}`).join("\n"));
5525
5830
  }
5526
5831
 
5527
- const brainContext = `<agent-brain>\n## Your Brain Context\n\n${sections.join("\n\n")}\n</agent-brain>`;
5832
+ const brainContext = `<agent-brain>\n## Your Brain Context (TMT)\n\n${sections.join("\n\n")}\n</agent-brain>`;
5528
5833
 
5529
- // Append to the existing before_prompt_build hook
5530
- // We use a second hook — OpenClaw supports multiple listeners
5531
5834
  try {
5532
5835
  api.on("before_prompt_build", () => ({
5533
5836
  appendSystemContext: brainContext,
5534
5837
  }));
5535
5838
  } catch {
5536
- // Fallback: log that injection wasn't possible
5537
5839
  api.logger.debug?.("[ofiere] Could not register brain context hook — appendSystemContext may not be supported");
5538
5840
  }
5539
5841
  }