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 +1 -1
- package/src/agent-resolver.ts +3 -17
- package/src/tools.ts +203 -123
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "4.
|
|
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"],
|
package/src/agent-resolver.ts
CHANGED
|
@@ -35,12 +35,12 @@ export async function resolveAgentId(
|
|
|
35
35
|
return cache.get(cacheKey)!;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// 2. Look up by name (
|
|
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
|
-
.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
// ──
|
|
5572
|
-
//
|
|
5573
|
-
//
|
|
5574
|
-
//
|
|
5575
|
-
|
|
5576
|
-
|
|
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
|
-
//
|
|
5639
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
5691
|
-
|
|
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
|
|
5701
|
-
for (const match of matches) {
|
|
5756
|
+
for (const match of lastUser.matchAll(pattern)) {
|
|
5702
5757
|
const fact = match[0].trim();
|
|
5703
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
5766
|
-
//
|
|
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 (
|
|
5829
|
+
// L4_rule — operational guardrails (non-superseded, sorted by importance)
|
|
5769
5830
|
// L5_persona — identity context (permanent, top 3)
|
|
5770
|
-
//
|
|
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
|
-
|
|
5838
|
+
function registerBrainContextHook(
|
|
5773
5839
|
api: any,
|
|
5774
5840
|
supabase: SupabaseClient,
|
|
5775
5841
|
userId: string,
|
|
5776
|
-
|
|
5777
|
-
):
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
const
|
|
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
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
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
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
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
|
-
|
|
5864
|
+
if (!resolvedAgentId) return;
|
|
5817
5865
|
|
|
5818
|
-
|
|
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
|
-
|
|
5821
|
-
|
|
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
|
-
|
|
5825
|
-
|
|
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
|
-
|
|
5829
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
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
|
}
|