ofiere-openclaw-plugin 4.47.0 → 4.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.47.0",
3
+ "version": "4.49.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"],
@@ -15,7 +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, loadChiefStaffDefaults, loadDispatchContextBySession, resolveStaffModels, type SubagentRow, type DispatchContextRow } from "./staffPersona.js";
18
+ import { loadSubagentRow, buildStaffPersonaBlock, readDispatchSubagentId, loadChiefStaffDefaults, loadChiefAgentConfig, loadDispatchContextBySession, resolveStaffModels, type SubagentRow, type DispatchContextRow } from "./staffPersona.js";
19
19
 
20
20
  interface ToolResult {
21
21
  content: Array<{ type: "text"; text: string }>;
@@ -453,8 +453,11 @@ export function registerAttachmentContextHook(args: {
453
453
  if (subagentId) {
454
454
  staffRow = await loadSubagentRow(supabase, userId, subagentId);
455
455
  if (staffRow && staffRow.chief_agent_id === resolvedAgentId) {
456
- const chiefDefaults = await loadChiefStaffDefaults(supabase, userId, staffRow.chief_agent_id).catch(() => null);
457
- const resolved = resolveStaffModels(staffRow, chiefDefaults);
456
+ const [chiefDefaults, chiefConfig] = await Promise.all([
457
+ loadChiefStaffDefaults(supabase, userId, staffRow.chief_agent_id).catch(() => null),
458
+ loadChiefAgentConfig(supabase, userId, staffRow.chief_agent_id).catch(() => null),
459
+ ]);
460
+ const resolved = resolveStaffModels(staffRow, chiefDefaults, chiefConfig);
458
461
  staffPrefix = buildStaffPersonaBlock(staffRow, resolved) + "\n\n---\n\n";
459
462
  api.logger?.debug?.(
460
463
  `[ofiere-staff] subagent ${subagentId} (${staffRow.name}) reporting to ${resolvedAgentId} — persona injected; resolved models: ${JSON.stringify(resolved)}`,
@@ -127,7 +127,7 @@ export interface ResolvedStaffModels {
127
127
  tool_call_model: string | null;
128
128
  function_call_model: string | null;
129
129
  vision_model: string | null;
130
- source: Record<StaffModelSlot, "staff" | "chief_default" | "chief_fallback" | null>;
130
+ source: Record<StaffModelSlot, "staff" | "chief_default" | "chief_primary" | "chief_fallback" | null>;
131
131
  }
132
132
 
133
133
  const SLOTS: StaffModelSlot[] = [
@@ -138,6 +138,17 @@ const SLOTS: StaffModelSlot[] = [
138
138
  "vision_model",
139
139
  ];
140
140
 
141
+ // Cycle 7b BUGSHOOT-3 — chief's own ofie_agent_config row, used as the third
142
+ // fallback tier when neither the staff slot nor the chief STAFF MODEL slot is
143
+ // set. Mirrors the dispatcher's loadStaffPrimaryModel chain.
144
+ export interface ChiefAgentConfigRow {
145
+ agent_id: string;
146
+ primary_model: string | null;
147
+ coding_model: string | null;
148
+ tool_call_model: string | null;
149
+ vision_model: string | null;
150
+ }
151
+
141
152
  export async function loadChiefStaffDefaults(
142
153
  supabase: SupabaseClient,
143
154
  userId: string,
@@ -155,19 +166,42 @@ export async function loadChiefStaffDefaults(
155
166
  return data as ChiefStaffDefaultsRow;
156
167
  }
157
168
 
169
+ export async function loadChiefAgentConfig(
170
+ supabase: SupabaseClient,
171
+ userId: string,
172
+ chiefAgentId: string,
173
+ ): Promise<ChiefAgentConfigRow | null> {
174
+ const { data, error } = await supabase
175
+ .from("ofie_agent_config")
176
+ .select("agent_id, primary_model, coding_model, tool_call_model, vision_model")
177
+ .eq("user_id", userId)
178
+ .eq("agent_id", chiefAgentId)
179
+ .maybeSingle();
180
+ if (error || !data) return null;
181
+ return data as ChiefAgentConfigRow;
182
+ }
183
+
158
184
  /**
159
- * Resolution chain at delegate_to_staff time:
160
- * staff.<slot> (per-staff override on agent_subagents)
161
- * ?? chief_staff_defaults.<slot> (chief-level "STAFF MODEL")
162
- * ?? null (caller falls back to the chief's own model — that lookup
163
- * happens in the OpenClaw runtime, not here).
185
+ * Resolution chain at delegate_to_staff time (BUGSHOOT-3):
186
+ * 1. staff.<slot> (per-staff override)
187
+ * 2. chief_staff_defaults.<slot> (chief-level STAFF MODEL)
188
+ * 3. ofie_agent_config.<slot> (chief's OWN slot, NEW)
189
+ * 4. ofie_agent_config.primary_model (chief's primary, last-resort sub)
190
+ * 5. null (gateway picks gateway default)
191
+ *
192
+ * Tier 3 was added in BUGSHOOT-3 because the prior chain stopped at tier 2 and
193
+ * relied on an implicit "gateway default" — which is opaque and led to BUG 10
194
+ * when the staff override held a model id the gateway rejected. With tier 3,
195
+ * an empty staff slot deterministically inherits the chief's actual model.
164
196
  *
165
- * The chief's own model is intentionally not touched. Empty staff slot +
166
- * empty chief STAFF MODEL = inherit chief's primary_model (gateway default).
197
+ * Tier 4 (slot primary_model fallback) handles the common case where chief
198
+ * has only a primary_model set and no per-slot routing — a staff with
199
+ * coding_model blank still inherits chief's primary_model rather than null.
167
200
  */
168
201
  export function resolveStaffModels(
169
202
  staff: SubagentRow,
170
203
  chiefDefaults: ChiefStaffDefaultsRow | null,
204
+ chiefConfig?: ChiefAgentConfigRow | null,
171
205
  ): ResolvedStaffModels {
172
206
  const out: ResolvedStaffModels = {
173
207
  primary_model: null,
@@ -183,15 +217,26 @@ export function resolveStaffModels(
183
217
  vision_model: null,
184
218
  },
185
219
  };
220
+ const chiefPrimary = typeof chiefConfig?.primary_model === "string" && chiefConfig.primary_model.trim()
221
+ ? chiefConfig.primary_model.trim()
222
+ : null;
186
223
  for (const slot of SLOTS) {
187
224
  const staffVal = (staff as any)[slot] as string | null | undefined;
188
225
  const chiefVal = chiefDefaults ? (chiefDefaults as any)[slot] as string | null | undefined : null;
226
+ const chiefSlotVal = chiefConfig ? (chiefConfig as any)[slot] as string | null | undefined : null;
189
227
  if (staffVal && staffVal.trim()) {
190
228
  out[slot] = staffVal;
191
229
  out.source[slot] = "staff";
192
230
  } else if (chiefVal && chiefVal.trim()) {
193
231
  out[slot] = chiefVal;
194
232
  out.source[slot] = "chief_default";
233
+ } else if (chiefSlotVal && chiefSlotVal.trim()) {
234
+ out[slot] = chiefSlotVal;
235
+ out.source[slot] = "chief_primary";
236
+ } else if (chiefPrimary) {
237
+ // Sub-fallback: chief has a primary_model but no per-slot override.
238
+ out[slot] = chiefPrimary;
239
+ out.source[slot] = "chief_primary";
195
240
  } else {
196
241
  out[slot] = null;
197
242
  out.source[slot] = "chief_fallback";
@@ -225,8 +270,11 @@ export function buildStaffPersonaBlock(
225
270
  lines.push(``, `## Operating Instructions`, staff.instructions.trim());
226
271
  }
227
272
  if (resolved && (resolved.primary_model || resolved.coding_model || resolved.tool_call_model || resolved.function_call_model || resolved.vision_model)) {
228
- const sourceLabel = (s: "staff" | "chief_default" | "chief_fallback" | null) =>
229
- s === "staff" ? "per-staff override" : s === "chief_default" ? "chief STAFF MODEL" : "inherits chief";
273
+ const sourceLabel = (s: "staff" | "chief_default" | "chief_primary" | "chief_fallback" | null) =>
274
+ s === "staff" ? "per-staff override"
275
+ : s === "chief_default" ? "chief STAFF MODEL"
276
+ : s === "chief_primary" ? "chief's own model"
277
+ : "inherits chief (gateway default)";
230
278
  const rows: string[] = [];
231
279
  if (resolved.primary_model) rows.push(`- primary: \`${resolved.primary_model}\` (${sourceLabel(resolved.source.primary_model)})`);
232
280
  if (resolved.coding_model) rows.push(`- coding: \`${resolved.coding_model}\` (${sourceLabel(resolved.source.coding_model)})`);
package/src/tools.ts CHANGED
@@ -6562,10 +6562,20 @@ function registerBrainExtractionHook(
6562
6562
  ): void {
6563
6563
  try {
6564
6564
  api.on("agent_end", async (event: any, ctx: any) => {
6565
+ // Cycle 7b BUGSHOOT-4 — entry log is load-bearing: 4 prior cycles spent
6566
+ // chasing downstream symptoms because no log existed to prove the hook
6567
+ // even fired. If you're debugging staff-dispatch persistence and DO NOT
6568
+ // see this line in the gateway log, the hook never ran — investigate
6569
+ // hook registration / agent scoping before anything else.
6570
+ const _entrySessionKey = ctx?.sessionKey || event?.sessionKey || event?.context?.sessionKey || "(none)";
6571
+ api.logger.info?.(`[ofiere-brain] agent_end fired (sessionKey=${_entrySessionKey})`);
6565
6572
  try {
6566
6573
  // Extract messages from event — agent_end provides the conversation
6567
6574
  const messages: any[] = event?.messages || event?.context?.messages || [];
6568
- if (!messages || messages.length < 2) return;
6575
+ if (!messages || messages.length < 2) {
6576
+ api.logger.debug?.(`[ofiere-brain] agent_end early-exit: messages.length=${messages?.length ?? 0} (<2)`);
6577
+ return;
6578
+ }
6569
6579
 
6570
6580
  // Find last user + last assistant message
6571
6581
  let lastUser = "";
@@ -6589,8 +6599,15 @@ function registerBrainExtractionHook(
6589
6599
  if (lastUser && lastAssistant) break;
6590
6600
  }
6591
6601
 
6592
- // Skip trivial exchanges
6593
- if (lastUser.length < 20 || lastAssistant.length < 30) return;
6602
+ // Cycle 7b BUGSHOOT-4 — REMOVED early-return on short messages here.
6603
+ // The prior gate (`if lastUser<20 || lastAssistant<30 return`) sat
6604
+ // BEFORE the staff_report / task_dispatch_log / conversation_messages
6605
+ // writes, which silently killed every staff dispatch whose output was
6606
+ // shorter than 30 chars (e.g. smoke tests outputting "SMOKE-OK" = 8).
6607
+ // The trivial-skip is now applied just before the brain L1/L2/L3/L4
6608
+ // extraction below — those are noise filters, not bookkeeping.
6609
+ // Bookkeeping (dispatch_log completion, conversation messages,
6610
+ // staff_report) MUST run regardless of message length.
6594
6611
 
6595
6612
  // ── Agent identity resolution (FIX) ──────────────────────────
6596
6613
  // Priority: ctx.agentId (from PluginHookAgentContext, the CORRECT source)
@@ -6776,6 +6793,16 @@ function registerBrainExtractionHook(
6776
6793
  }
6777
6794
  }
6778
6795
 
6796
+ // Cycle 7b BUGSHOOT-4 — trivial-skip moved here from line ~6593.
6797
+ // Brain L1/L2/L3/L4 extraction skips trivial chit-chat to reduce
6798
+ // memory noise. This MUST sit AFTER staff_report + task_dispatch_log
6799
+ // bookkeeping above, otherwise short-output staff dispatches silently
6800
+ // fail to persist (root cause of cycles 7b BUGSHOOT 1-4).
6801
+ if (lastUser.length < 20 || lastAssistant.length < 30) {
6802
+ api.logger.debug?.(`[ofiere-brain] skip brain extraction (trivial: user=${lastUser.length}c assistant=${lastAssistant.length}c)`);
6803
+ return;
6804
+ }
6805
+
6779
6806
  // ── Fast Stream: Write L1_focus + L2_episode in parallel ──
6780
6807
  const rawContent = `User: ${lastUser}\nAssistant: ${lastAssistant}`;
6781
6808
  const immediateWrites: PromiseLike<any>[] = [];