ofiere-openclaw-plugin 4.42.0 → 4.43.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/attachments.ts +66 -5
- package/src/staffPersona.ts +82 -0
- package/src/tools.ts +155 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.43.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw plugin for Ofiere PM - 16 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, execution plan builder, SOP management, agent brain, talent management, and corporate frameworks",
|
|
6
6
|
"keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
|
package/src/attachments.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
invalidateAgentTier,
|
|
16
16
|
} from "./agent-tier.js";
|
|
17
17
|
import { issueAttachmentToken, verifyAttachmentToken } from "./attach-token.js";
|
|
18
|
+
import { loadSubagentRow, buildStaffPersonaBlock, readDispatchSubagentId, type SubagentRow } from "./staffPersona.js";
|
|
18
19
|
|
|
19
20
|
interface ToolResult {
|
|
20
21
|
content: Array<{ type: "text"; text: string }>;
|
|
@@ -360,8 +361,29 @@ async function buildAttachmentBlock(args: {
|
|
|
360
361
|
supabase: SupabaseClient;
|
|
361
362
|
userId: string;
|
|
362
363
|
agentId: string;
|
|
364
|
+
subagentId?: string | null;
|
|
363
365
|
}): Promise<string> {
|
|
364
|
-
const { supabase, userId, agentId } = args;
|
|
366
|
+
const { supabase, userId, agentId, subagentId } = args;
|
|
367
|
+
|
|
368
|
+
// When a staff dispatch arrives, prefer attachments scoped to the subagent
|
|
369
|
+
// so a chief and its staff never share an attachment surface implicitly.
|
|
370
|
+
if (subagentId) {
|
|
371
|
+
const { data: staffConv } = await supabase
|
|
372
|
+
.from("conversations")
|
|
373
|
+
.select("id, attached_sop_ids, attached_framework_ids")
|
|
374
|
+
.eq("user_id", userId)
|
|
375
|
+
.eq("agent_id", agentId)
|
|
376
|
+
.eq("subagent_id", subagentId)
|
|
377
|
+
.order("updated_at", { ascending: false })
|
|
378
|
+
.limit(1)
|
|
379
|
+
.maybeSingle();
|
|
380
|
+
if (staffConv) {
|
|
381
|
+
const sopIds: string[] = (staffConv.attached_sop_ids as string[] | null) || [];
|
|
382
|
+
const fwIds: string[] = (staffConv.attached_framework_ids as string[] | null) || [];
|
|
383
|
+
return renderBlockForIds({ supabase, userId, sopIds, fwIds });
|
|
384
|
+
}
|
|
385
|
+
// No staff-scoped conversation yet — fall through to chief-level lookup.
|
|
386
|
+
}
|
|
365
387
|
|
|
366
388
|
// Find the most recently active conversation for this agent. The dashboard
|
|
367
389
|
// bumps `updated_at` whenever a message is sent or attachments change, so
|
|
@@ -402,6 +424,36 @@ export function registerAttachmentContextHook(args: {
|
|
|
402
424
|
}
|
|
403
425
|
if (!resolvedAgentId) return;
|
|
404
426
|
|
|
427
|
+
// Cycle 7b — staff persona injection. Only fires when subagent_id is
|
|
428
|
+
// present in dispatch metadata (task-dispatcher / scheduler / explicit
|
|
429
|
+
// dispatch params). Plain user chats with the chief never persona-swap.
|
|
430
|
+
const subagentId = readDispatchSubagentId(ctx);
|
|
431
|
+
let staffPrefix = "";
|
|
432
|
+
let staffRow: SubagentRow | null = null;
|
|
433
|
+
if (subagentId) {
|
|
434
|
+
staffRow = await loadSubagentRow(supabase, userId, subagentId);
|
|
435
|
+
if (staffRow && staffRow.chief_agent_id === resolvedAgentId) {
|
|
436
|
+
staffPrefix = buildStaffPersonaBlock(staffRow) + "\n\n---\n\n";
|
|
437
|
+
api.logger?.debug?.(
|
|
438
|
+
`[ofiere-staff] subagent ${subagentId} (${staffRow.name}) reporting to ${resolvedAgentId} — persona injected`,
|
|
439
|
+
);
|
|
440
|
+
} else if (staffRow) {
|
|
441
|
+
api.logger?.warn?.(
|
|
442
|
+
`[ofiere-staff] subagent ${subagentId} chief_mismatch (got ${staffRow.chief_agent_id}, expected ${resolvedAgentId}) — ignoring persona`,
|
|
443
|
+
);
|
|
444
|
+
staffRow = null;
|
|
445
|
+
} else {
|
|
446
|
+
api.logger?.warn?.(`[ofiere-staff] subagent ${subagentId} not found — ignoring persona`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// wrap any return-block with the staff prefix (or pass through unchanged
|
|
451
|
+
// when no staff dispatch).
|
|
452
|
+
const wrap = (block: string | undefined | null): { appendSystemContext: string } | undefined => {
|
|
453
|
+
const finalBlock = staffPrefix + (block || "");
|
|
454
|
+
return finalBlock ? { appendSystemContext: finalBlock } : undefined;
|
|
455
|
+
};
|
|
456
|
+
|
|
405
457
|
// Dispatch-params path: workflow executor + task-dispatcher edge function
|
|
406
458
|
// can stash explicit `attached_sop_ids` / `attached_framework_ids` on the
|
|
407
459
|
// chat.send frame's metadata. When present, prefer them over the
|
|
@@ -416,7 +468,16 @@ export function registerAttachmentContextHook(args: {
|
|
|
416
468
|
sopIds: dispatchIds.sopIds,
|
|
417
469
|
fwIds: dispatchIds.fwIds,
|
|
418
470
|
});
|
|
419
|
-
return block
|
|
471
|
+
return wrap(block);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Staff-mode runs bypass the chief-level attachment cache so a chief
|
|
475
|
+
// and its staff never share a cached attachment block.
|
|
476
|
+
if (subagentId) {
|
|
477
|
+
const block = await buildAttachmentBlock({
|
|
478
|
+
supabase, userId, agentId: resolvedAgentId, subagentId,
|
|
479
|
+
});
|
|
480
|
+
return wrap(block);
|
|
420
481
|
}
|
|
421
482
|
|
|
422
483
|
// Multi-tenant: a single plugin process can serve multiple users — key
|
|
@@ -426,7 +487,7 @@ export function registerAttachmentContextHook(args: {
|
|
|
426
487
|
if (cached) {
|
|
427
488
|
const age = Date.now() - cached.at;
|
|
428
489
|
if (age < ATTACH_CACHE_TTL_MS) {
|
|
429
|
-
return cached.text
|
|
490
|
+
return wrap(cached.text);
|
|
430
491
|
}
|
|
431
492
|
if (age < ATTACH_CACHE_STALE_MS) {
|
|
432
493
|
// Refresh in background
|
|
@@ -436,13 +497,13 @@ export function registerAttachmentContextHook(args: {
|
|
|
436
497
|
attachmentCache.set(cacheKey, { text: fresh, at: Date.now() });
|
|
437
498
|
} catch { /* ignore */ }
|
|
438
499
|
})();
|
|
439
|
-
return cached.text
|
|
500
|
+
return wrap(cached.text);
|
|
440
501
|
}
|
|
441
502
|
}
|
|
442
503
|
|
|
443
504
|
const block = await buildAttachmentBlock({ supabase, userId, agentId: resolvedAgentId });
|
|
444
505
|
attachmentCache.set(cacheKey, { text: block, at: Date.now() });
|
|
445
|
-
return block
|
|
506
|
+
return wrap(block);
|
|
446
507
|
} catch (e) {
|
|
447
508
|
api.logger?.debug?.(`[ofiere-attach] before_prompt_build error: ${e instanceof Error ? e.message : e}`);
|
|
448
509
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
|
|
3
|
+
export interface SubagentRow {
|
|
4
|
+
id: string;
|
|
5
|
+
chief_agent_id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
role: string | null;
|
|
8
|
+
codename: string | null;
|
|
9
|
+
system_prompt: string | null;
|
|
10
|
+
mission: string | null;
|
|
11
|
+
responsibilities: string | null;
|
|
12
|
+
instructions: string | null;
|
|
13
|
+
primary_model: string | null;
|
|
14
|
+
coding_model: string | null;
|
|
15
|
+
tool_call_model: string | null;
|
|
16
|
+
function_call_model: string | null;
|
|
17
|
+
vision_model: string | null;
|
|
18
|
+
mcp_server_ids: string[] | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function loadSubagentRow(
|
|
22
|
+
supabase: SupabaseClient,
|
|
23
|
+
userId: string,
|
|
24
|
+
subagentId: string,
|
|
25
|
+
): Promise<SubagentRow | null> {
|
|
26
|
+
const { data, error } = await supabase
|
|
27
|
+
.from("agent_subagents")
|
|
28
|
+
.select(
|
|
29
|
+
"id, chief_agent_id, name, role, codename, system_prompt, mission, responsibilities, instructions, primary_model, coding_model, tool_call_model, function_call_model, vision_model, mcp_server_ids",
|
|
30
|
+
)
|
|
31
|
+
.eq("user_id", userId)
|
|
32
|
+
.eq("id", subagentId)
|
|
33
|
+
.maybeSingle();
|
|
34
|
+
if (error || !data) return null;
|
|
35
|
+
return data as SubagentRow;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Cycle 7b — pluck subagent_id from dispatch metadata only. Plain user chats
|
|
39
|
+
// must NOT trigger the staff persona swap or report emission, so we deliberately
|
|
40
|
+
// do not consult ctx.subagentId / ctx.subagent_id at the top level.
|
|
41
|
+
export function readDispatchSubagentId(ctx: any, event?: any): string | null {
|
|
42
|
+
const candidates: Array<unknown> = [
|
|
43
|
+
ctx?.metadata?.subagent_id,
|
|
44
|
+
ctx?.metadata?.dispatch?.subagent_id,
|
|
45
|
+
ctx?.params?.metadata?.subagent_id,
|
|
46
|
+
ctx?.payload?.metadata?.subagent_id,
|
|
47
|
+
ctx?.request?.metadata?.subagent_id,
|
|
48
|
+
ctx?.options?.metadata?.subagent_id,
|
|
49
|
+
ctx?.attachments?.subagent_id,
|
|
50
|
+
ctx?.dispatch?.subagent_id,
|
|
51
|
+
event?.metadata?.subagent_id,
|
|
52
|
+
event?.context?.metadata?.subagent_id,
|
|
53
|
+
];
|
|
54
|
+
for (const c of candidates) {
|
|
55
|
+
if (typeof c === "string" && c.length) return c;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildStaffPersonaBlock(staff: SubagentRow): string {
|
|
61
|
+
const lines: string[] = [];
|
|
62
|
+
lines.push(`# STAFF IDENTITY`);
|
|
63
|
+
lines.push(
|
|
64
|
+
`You are **${staff.name}**${staff.role ? ` (${staff.role})` : ""}, a staff member reporting to chief \`${staff.chief_agent_id}\`.`,
|
|
65
|
+
);
|
|
66
|
+
lines.push(
|
|
67
|
+
`You are NOT the chief. Execute the assigned task, then return a structured report. Do not adopt the chief's persona.`,
|
|
68
|
+
);
|
|
69
|
+
if (staff.system_prompt?.trim()) {
|
|
70
|
+
lines.push(``, `## Persona`, staff.system_prompt.trim());
|
|
71
|
+
}
|
|
72
|
+
if (staff.mission?.trim()) {
|
|
73
|
+
lines.push(``, `## Mission`, staff.mission.trim());
|
|
74
|
+
}
|
|
75
|
+
if (staff.responsibilities?.trim()) {
|
|
76
|
+
lines.push(``, `## Responsibilities`, staff.responsibilities.trim());
|
|
77
|
+
}
|
|
78
|
+
if (staff.instructions?.trim()) {
|
|
79
|
+
lines.push(``, `## Operating Instructions`, staff.instructions.trim());
|
|
80
|
+
}
|
|
81
|
+
return lines.join("\n");
|
|
82
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
registerAttachmentContextHook,
|
|
20
20
|
} from "./attachments.js";
|
|
21
21
|
import { invalidateAgentTier } from "./agent-tier.js";
|
|
22
|
+
import { loadSubagentRow, readDispatchSubagentId } from "./staffPersona.js";
|
|
22
23
|
|
|
23
24
|
// ─── Tool result shape (matches OpenClaw SDK) ────────────────────────────────
|
|
24
25
|
|
|
@@ -1174,7 +1175,9 @@ function registerAgentOps(
|
|
|
1174
1175
|
`Actions:\n` +
|
|
1175
1176
|
`- "list": List all top-level agents (chiefs / native + OpenClaw) with their IDs, names, roles, and status. Use this to find the correct agent_id for task assignment.\n` +
|
|
1176
1177
|
`- "list_subagents": List staff subagents under a chief. Required: chief_agent_id.\n` +
|
|
1178
|
+
`- "get_subagent": Fetch a single subagent's full row (including persona + model overrides + mcp_server_ids). Required: subagent_id.\n` +
|
|
1177
1179
|
`- "create_subagent": Create a staff subagent under a chief (max 5 per chief). Required: chief_agent_id, name. Optional: role (default "Staff"), codename, color_hex (default "#64748b").\n` +
|
|
1180
|
+
`- "update_subagent": Update a staff subagent's persona, model overrides, MCP allowlist, or identity fields. Required: subagent_id. Any of name, role, codename, color_hex, system_prompt, mission, responsibilities, instructions, primary_model, coding_model, tool_call_model, function_call_model, vision_model, mcp_server_ids may be set.\n` +
|
|
1178
1181
|
`- "delete_subagent": Remove a staff subagent. Required: subagent_id.\n` +
|
|
1179
1182
|
`- "invalidate_tier_cache": Flush the plugin's in-process tier resolver cache (5 min TTL). Optional: agent_id to flush a single entry; omit to flush all entries for the calling user. Use after any direct-DB mutation of agent_tier_overrides.`,
|
|
1180
1183
|
parameters: {
|
|
@@ -1184,15 +1187,29 @@ function registerAgentOps(
|
|
|
1184
1187
|
action: {
|
|
1185
1188
|
type: "string",
|
|
1186
1189
|
description: "The operation to perform",
|
|
1187
|
-
enum: ["list", "list_subagents", "create_subagent", "delete_subagent", "invalidate_tier_cache"],
|
|
1190
|
+
enum: ["list", "list_subagents", "get_subagent", "create_subagent", "update_subagent", "delete_subagent", "invalidate_tier_cache"],
|
|
1188
1191
|
},
|
|
1189
1192
|
chief_agent_id: { type: "string", description: "Chief agent ID (required for list_subagents, create_subagent)" },
|
|
1190
|
-
subagent_id: { type: "string", description: "Subagent ID (required for delete_subagent)" },
|
|
1193
|
+
subagent_id: { type: "string", description: "Subagent ID (required for get/update/delete_subagent)" },
|
|
1191
1194
|
agent_id: { type: "string", description: "Agent ID (optional for invalidate_tier_cache — omit to flush all entries for the user)" },
|
|
1192
1195
|
name: { type: "string", description: "Subagent display name (required for create_subagent)" },
|
|
1193
1196
|
role: { type: "string", description: "Subagent role label, e.g. 'Staff', 'Analyst'. Defaults to 'Staff'." },
|
|
1194
1197
|
codename: { type: "string", description: "Optional subagent codename" },
|
|
1195
1198
|
color_hex: { type: "string", description: "Optional subagent UI color, default '#64748b'" },
|
|
1199
|
+
system_prompt: { type: "string", description: "Staff persona / identity (Markdown). Optional." },
|
|
1200
|
+
mission: { type: "string", description: "Short-form mission statement. Optional." },
|
|
1201
|
+
responsibilities: { type: "string", description: "Bulleted Markdown list of duties. Optional." },
|
|
1202
|
+
instructions: { type: "string", description: "Detailed how-they-work instructions (Markdown). Optional." },
|
|
1203
|
+
primary_model: { type: "string", description: "Override primary slot. NULL/omit = inherit chief." },
|
|
1204
|
+
coding_model: { type: "string", description: "Override coding slot." },
|
|
1205
|
+
tool_call_model: { type: "string", description: "Override tool-call slot." },
|
|
1206
|
+
function_call_model: { type: "string", description: "Override function-call slot." },
|
|
1207
|
+
vision_model: { type: "string", description: "Override vision slot." },
|
|
1208
|
+
mcp_server_ids: {
|
|
1209
|
+
type: "array",
|
|
1210
|
+
items: { type: "string" },
|
|
1211
|
+
description: "Allowlist of MCP server UUIDs. Reserved for cycle 8 enforcement.",
|
|
1212
|
+
},
|
|
1196
1213
|
},
|
|
1197
1214
|
},
|
|
1198
1215
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
@@ -1203,14 +1220,18 @@ function registerAgentOps(
|
|
|
1203
1220
|
return handleListAgents(api, supabase, userId, fallbackAgentId);
|
|
1204
1221
|
case "list_subagents":
|
|
1205
1222
|
return handleListSubagents(supabase, userId, params);
|
|
1223
|
+
case "get_subagent":
|
|
1224
|
+
return handleGetSubagent(supabase, userId, params);
|
|
1206
1225
|
case "create_subagent":
|
|
1207
1226
|
return handleCreateSubagent(supabase, userId, params);
|
|
1227
|
+
case "update_subagent":
|
|
1228
|
+
return handleUpdateSubagent(supabase, userId, params);
|
|
1208
1229
|
case "delete_subagent":
|
|
1209
1230
|
return handleDeleteSubagent(supabase, userId, params);
|
|
1210
1231
|
case "invalidate_tier_cache":
|
|
1211
1232
|
return handleInvalidateTierCache(userId, params);
|
|
1212
1233
|
default:
|
|
1213
|
-
return err(`Unknown action "${action}". Valid actions: list, list_subagents, create_subagent, delete_subagent, invalidate_tier_cache`);
|
|
1234
|
+
return err(`Unknown action "${action}". Valid actions: list, list_subagents, get_subagent, create_subagent, update_subagent, delete_subagent, invalidate_tier_cache`);
|
|
1214
1235
|
}
|
|
1215
1236
|
},
|
|
1216
1237
|
});
|
|
@@ -5398,6 +5419,66 @@ async function handleDeleteSubagent(supabase: SupabaseClient, userId: string, pa
|
|
|
5398
5419
|
} catch (e) { return err(e instanceof Error ? e.message : String(e)); }
|
|
5399
5420
|
}
|
|
5400
5421
|
|
|
5422
|
+
async function handleGetSubagent(
|
|
5423
|
+
supabase: SupabaseClient,
|
|
5424
|
+
userId: string,
|
|
5425
|
+
params: Record<string, unknown>,
|
|
5426
|
+
): Promise<ToolResult> {
|
|
5427
|
+
try {
|
|
5428
|
+
const subagentId = params.subagent_id as string | undefined;
|
|
5429
|
+
if (!subagentId) return err("Missing required field: subagent_id");
|
|
5430
|
+
const { data, error } = await supabase
|
|
5431
|
+
.from("agent_subagents")
|
|
5432
|
+
.select("*")
|
|
5433
|
+
.eq("user_id", userId)
|
|
5434
|
+
.eq("id", subagentId)
|
|
5435
|
+
.maybeSingle();
|
|
5436
|
+
if (error) return err(error.message);
|
|
5437
|
+
if (!data) return err("subagent_not_found_or_not_yours");
|
|
5438
|
+
return ok({ subagent: data });
|
|
5439
|
+
} catch (e) { return err(e instanceof Error ? e.message : String(e)); }
|
|
5440
|
+
}
|
|
5441
|
+
|
|
5442
|
+
async function handleUpdateSubagent(
|
|
5443
|
+
supabase: SupabaseClient,
|
|
5444
|
+
userId: string,
|
|
5445
|
+
params: Record<string, unknown>,
|
|
5446
|
+
): Promise<ToolResult> {
|
|
5447
|
+
try {
|
|
5448
|
+
const subagentId = params.subagent_id as string | undefined;
|
|
5449
|
+
if (!subagentId) return err("Missing required field: subagent_id");
|
|
5450
|
+
|
|
5451
|
+
const updates: Record<string, unknown> = {};
|
|
5452
|
+
const stringFields = [
|
|
5453
|
+
"name", "role", "codename", "color_hex",
|
|
5454
|
+
"system_prompt", "mission", "responsibilities", "instructions",
|
|
5455
|
+
"primary_model", "coding_model", "tool_call_model", "function_call_model", "vision_model",
|
|
5456
|
+
] as const;
|
|
5457
|
+
for (const key of stringFields) {
|
|
5458
|
+
if (key in params && params[key] !== undefined) updates[key] = params[key];
|
|
5459
|
+
}
|
|
5460
|
+
if ("mcp_server_ids" in params && Array.isArray(params.mcp_server_ids)) {
|
|
5461
|
+
updates.mcp_server_ids = (params.mcp_server_ids as unknown[]).filter(
|
|
5462
|
+
(x): x is string => typeof x === "string",
|
|
5463
|
+
);
|
|
5464
|
+
}
|
|
5465
|
+
if (Object.keys(updates).length === 0) {
|
|
5466
|
+
return err("no updatable fields provided");
|
|
5467
|
+
}
|
|
5468
|
+
|
|
5469
|
+
const { data, error } = await supabase
|
|
5470
|
+
.from("agent_subagents")
|
|
5471
|
+
.update(updates)
|
|
5472
|
+
.eq("id", subagentId)
|
|
5473
|
+
.eq("user_id", userId)
|
|
5474
|
+
.select()
|
|
5475
|
+
.maybeSingle();
|
|
5476
|
+
if (error) return err(error.message);
|
|
5477
|
+
if (!data) return err("subagent_not_found_or_not_yours");
|
|
5478
|
+
return ok({ subagent: data, updated_fields: Object.keys(updates) });
|
|
5479
|
+
} catch (e) { return err(e instanceof Error ? e.message : String(e)); }
|
|
5480
|
+
}
|
|
5481
|
+
|
|
5401
5482
|
async function handleSOPApplyTemplate(
|
|
5402
5483
|
supabase: SupabaseClient, userId: string,
|
|
5403
5484
|
resolveAgent: (id?: string) => Promise<string | null>,
|
|
@@ -6450,6 +6531,77 @@ function registerBrainExtractionHook(
|
|
|
6450
6531
|
|
|
6451
6532
|
api.logger.debug?.(`[ofiere-brain] agent_end identity: ctx.agentId=${ctx?.agentId || "(none)"} event.agentId=${event?.agentId || "(none)"} resolved=${resolvedAgentId}`);
|
|
6452
6533
|
|
|
6534
|
+
// ── Cycle 7b — Staff report emit ──
|
|
6535
|
+
// If this agent_end was triggered by a staff dispatch, write a chief-
|
|
6536
|
+
// scoped memory note ("Staff X reported on task Y: ...") and POST a
|
|
6537
|
+
// webhook so the dashboard can surface the report. The staff itself
|
|
6538
|
+
// gets no memory entry (req 3 — staff has no memory of its own).
|
|
6539
|
+
const staffDispatchSubagentId = readDispatchSubagentId(ctx, event);
|
|
6540
|
+
if (staffDispatchSubagentId) {
|
|
6541
|
+
(async () => {
|
|
6542
|
+
try {
|
|
6543
|
+
const staff = await loadSubagentRow(supabase, userId, staffDispatchSubagentId);
|
|
6544
|
+
if (!staff) {
|
|
6545
|
+
api.logger.warn?.(`[ofiere-staff-report] subagent ${staffDispatchSubagentId} not found — skipping report`);
|
|
6546
|
+
return;
|
|
6547
|
+
}
|
|
6548
|
+
if (staff.chief_agent_id !== resolvedAgentId) {
|
|
6549
|
+
api.logger.warn?.(`[ofiere-staff-report] chief_mismatch (subagent ${staffDispatchSubagentId} reports to ${staff.chief_agent_id}, run was on ${resolvedAgentId}) — skipping report`);
|
|
6550
|
+
return;
|
|
6551
|
+
}
|
|
6552
|
+
const taskId =
|
|
6553
|
+
ctx?.metadata?.task_id || ctx?.metadata?.dispatch?.task_id ||
|
|
6554
|
+
event?.metadata?.task_id || event?.context?.metadata?.task_id || null;
|
|
6555
|
+
const excerpt = lastAssistant.slice(0, 1500);
|
|
6556
|
+
const reportBody = taskId
|
|
6557
|
+
? `Staff ${staff.name}${staff.role ? ` (${staff.role})` : ""} reported on task ${taskId}:\n\n${excerpt}`
|
|
6558
|
+
: `Staff ${staff.name}${staff.role ? ` (${staff.role})` : ""} reported:\n\n${excerpt}`;
|
|
6559
|
+
await supabase.from("agent_memories").insert({
|
|
6560
|
+
user_id: userId,
|
|
6561
|
+
agent_id: staff.chief_agent_id,
|
|
6562
|
+
tier: "L4_episodic",
|
|
6563
|
+
content: reportBody,
|
|
6564
|
+
source: "staff_report",
|
|
6565
|
+
importance: 5,
|
|
6566
|
+
context_key: taskId ? `staff_report:${staffDispatchSubagentId}:${taskId}` : `staff_report:${staffDispatchSubagentId}`,
|
|
6567
|
+
});
|
|
6568
|
+
api.logger.info?.(`[ofiere-staff-report] memory written for chief ${staff.chief_agent_id} from staff ${staff.name}`);
|
|
6569
|
+
|
|
6570
|
+
// Best-effort webhook to dashboard (existing OPENCLAW_WEBHOOK_SECRET).
|
|
6571
|
+
const webhookUrl = process.env.OFIERE_DASHBOARD_WEBHOOK_URL || "";
|
|
6572
|
+
const webhookSecret = process.env.OPENCLAW_WEBHOOK_SECRET || "";
|
|
6573
|
+
if (webhookUrl && webhookSecret) {
|
|
6574
|
+
try {
|
|
6575
|
+
await fetch(webhookUrl, {
|
|
6576
|
+
method: "POST",
|
|
6577
|
+
headers: {
|
|
6578
|
+
"content-type": "application/json",
|
|
6579
|
+
authorization: `Bearer ${webhookSecret}`,
|
|
6580
|
+
},
|
|
6581
|
+
body: JSON.stringify({
|
|
6582
|
+
type: "staff_report",
|
|
6583
|
+
payload: {
|
|
6584
|
+
user_id: userId,
|
|
6585
|
+
chief_agent_id: staff.chief_agent_id,
|
|
6586
|
+
subagent_id: staffDispatchSubagentId,
|
|
6587
|
+
task_id: taskId,
|
|
6588
|
+
response: excerpt,
|
|
6589
|
+
},
|
|
6590
|
+
}),
|
|
6591
|
+
});
|
|
6592
|
+
} catch (wErr) {
|
|
6593
|
+
api.logger.debug?.(`[ofiere-staff-report] webhook post failed: ${wErr instanceof Error ? wErr.message : String(wErr)}`);
|
|
6594
|
+
}
|
|
6595
|
+
}
|
|
6596
|
+
} catch (sErr) {
|
|
6597
|
+
api.logger.warn?.(`[ofiere-staff-report] failed: ${sErr instanceof Error ? sErr.message : String(sErr)}`);
|
|
6598
|
+
}
|
|
6599
|
+
})();
|
|
6600
|
+
// Skip the standard chief memory writes — this run was the staff's,
|
|
6601
|
+
// not the chief's. The chief gets a single L4 report entry above.
|
|
6602
|
+
return;
|
|
6603
|
+
}
|
|
6604
|
+
|
|
6453
6605
|
// ── Fast Stream: Write L1_focus + L2_episode in parallel ──
|
|
6454
6606
|
const rawContent = `User: ${lastUser}\nAssistant: ${lastAssistant}`;
|
|
6455
6607
|
const immediateWrites: PromiseLike<any>[] = [];
|