ofiere-openclaw-plugin 4.45.0 → 4.46.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.45.0",
3
+ "version": "4.46.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, resolveStaffModels, type SubagentRow } from "./staffPersona.js";
18
+ import { loadSubagentRow, buildStaffPersonaBlock, readDispatchSubagentId, loadChiefStaffDefaults, loadDispatchContextBySession, resolveStaffModels, type SubagentRow, type DispatchContextRow } from "./staffPersona.js";
19
19
 
20
20
  interface ToolResult {
21
21
  content: Array<{ type: "text"; text: string }>;
@@ -427,7 +427,27 @@ export function registerAttachmentContextHook(args: {
427
427
  // Cycle 7b — staff persona injection. Only fires when subagent_id is
428
428
  // present in dispatch metadata (task-dispatcher / scheduler / explicit
429
429
  // dispatch params). Plain user chats with the chief never persona-swap.
430
- const subagentId = readDispatchSubagentId(ctx);
430
+ //
431
+ // Cycle 7b BUGSHOOT-1 (BUG 2) — OpenClaw gateway core rejects unknown
432
+ // root key `metadata` on chat.send, so the dispatcher can't ride
433
+ // metadata through the wire. After the metadata path comes up empty,
434
+ // try the dispatch_context table keyed by sessionKey. The metadata
435
+ // path is preferred so a future gateway upgrade keeps working.
436
+ const sessionKey: string | null =
437
+ ctx?.sessionKey || ctx?.session_key ||
438
+ (typeof _event === "object" ? (_event?.sessionKey || _event?.context?.sessionKey || null) : null) ||
439
+ null;
440
+ let dispatchCtxRow: DispatchContextRow | null = null;
441
+ let subagentId = readDispatchSubagentId(ctx);
442
+ if (!subagentId && sessionKey) {
443
+ dispatchCtxRow = await loadDispatchContextBySession(supabase, userId, sessionKey).catch(() => null);
444
+ if (dispatchCtxRow?.subagent_id) {
445
+ subagentId = dispatchCtxRow.subagent_id;
446
+ api.logger?.debug?.(
447
+ `[ofiere-staff] subagent_id resolved via dispatch_context (session=${sessionKey})`,
448
+ );
449
+ }
450
+ }
431
451
  let staffPrefix = "";
432
452
  let staffRow: SubagentRow | null = null;
433
453
  if (subagentId) {
@@ -460,7 +480,23 @@ export function registerAttachmentContextHook(args: {
460
480
  // can stash explicit `attached_sop_ids` / `attached_framework_ids` on the
461
481
  // chat.send frame's metadata. When present, prefer them over the
462
482
  // most-recent-conversation lookup and bypass cache (per-dispatch ids).
463
- const dispatchIds = readDispatchAttachmentIds(ctx);
483
+ let dispatchIds = readDispatchAttachmentIds(ctx);
484
+ // Cycle 7b BUGSHOOT-1 (BUG 2) — DB-backed dispatch-context fallback for
485
+ // attachment ids. Same rationale as the subagent_id fallback above.
486
+ if (!dispatchIds.sopIds.length && !dispatchIds.fwIds.length && sessionKey) {
487
+ if (!dispatchCtxRow) {
488
+ dispatchCtxRow = await loadDispatchContextBySession(supabase, userId, sessionKey).catch(() => null);
489
+ }
490
+ if (dispatchCtxRow && (dispatchCtxRow.attached_sop_ids.length || dispatchCtxRow.attached_framework_ids.length)) {
491
+ dispatchIds = {
492
+ sopIds: dispatchCtxRow.attached_sop_ids,
493
+ fwIds: dispatchCtxRow.attached_framework_ids,
494
+ };
495
+ api.logger?.debug?.(
496
+ `[ofiere-attach] dispatch ids via dispatch_context (session=${sessionKey}) sops=${dispatchIds.sopIds.length} fws=${dispatchIds.fwIds.length}`,
497
+ );
498
+ }
499
+ }
464
500
  if (dispatchIds.sopIds.length || dispatchIds.fwIds.length) {
465
501
  api.logger?.debug?.(
466
502
  `[ofiere-attach] dispatch ids agent=${resolvedAgentId} sops=${dispatchIds.sopIds.length} fws=${dispatchIds.fwIds.length}`,
@@ -59,6 +59,50 @@ export function readDispatchSubagentId(ctx: any, event?: any): string | null {
59
59
  return null;
60
60
  }
61
61
 
62
+ // Cycle 7b BUGSHOOT-1 (BUG 2) — DB-backed dispatch context fallback.
63
+ // OpenClaw gateway core rejects unknown root key `metadata` on chat.send, so
64
+ // the task-dispatcher edge function can't ride dispatch metadata through the
65
+ // wire. The dispatcher writes a row to public.dispatch_context keyed by
66
+ // (user_id, session_key) before chat.send fires; this loader reads it back
67
+ // during before_prompt_build when the metadata path comes up empty.
68
+ //
69
+ // Returns the dispatch context for the most recent matching row, or null if
70
+ // none exists. The metadata path is preferred so a future gateway upgrade
71
+ // that natively accepts metadata keeps working with no flag day.
72
+ export interface DispatchContextRow {
73
+ subagent_id: string | null;
74
+ attached_sop_ids: string[];
75
+ attached_framework_ids: string[];
76
+ task_id: string | null;
77
+ }
78
+
79
+ export async function loadDispatchContextBySession(
80
+ supabase: SupabaseClient,
81
+ userId: string,
82
+ sessionKey: string,
83
+ ): Promise<DispatchContextRow | null> {
84
+ if (!sessionKey) return null;
85
+ const { data, error } = await supabase
86
+ .from("dispatch_context")
87
+ .select("subagent_id, attached_sop_ids, attached_framework_ids, task_id")
88
+ .eq("user_id", userId)
89
+ .eq("session_key", sessionKey)
90
+ .order("created_at", { ascending: false })
91
+ .limit(1)
92
+ .maybeSingle();
93
+ if (error || !data) return null;
94
+ return {
95
+ subagent_id: (data.subagent_id as string | null) ?? null,
96
+ attached_sop_ids: Array.isArray(data.attached_sop_ids)
97
+ ? (data.attached_sop_ids as unknown[]).filter((x): x is string => typeof x === "string")
98
+ : [],
99
+ attached_framework_ids: Array.isArray(data.attached_framework_ids)
100
+ ? (data.attached_framework_ids as unknown[]).filter((x): x is string => typeof x === "string")
101
+ : [],
102
+ task_id: (data.task_id as string | null) ?? null,
103
+ };
104
+ }
105
+
62
106
  export type StaffModelSlot =
63
107
  | "primary_model"
64
108
  | "coding_model"
package/src/tools.ts CHANGED
@@ -653,6 +653,12 @@ async function handleCreateTask(
653
653
  if (!isNaN(parsedDate.getTime())) {
654
654
  // Valid date — check if it includes a meaningful time component
655
655
  const hasTimeInfo = /[T ]\d{2}:\d{2}/.test(startDate);
656
+ // BUG 4 fix (cycle 7b BUGSHOOT-1): if the input string already carries
657
+ // an explicit UTC marker (Z, +00, +0000, +00:00, UTC), the time
658
+ // component is NOT user-local — skip the local→UTC subtraction.
659
+ // Without this, a UTC-Z input like "2026-05-10T16:25:00Z" got mistaken
660
+ // for local 16:25 and stored as 09:25Z (off by TZ_OFFSET_HOURS).
661
+ const isUtcInput = /Z$|[+-]00:?00$|UTC$/i.test(startDate.trim());
656
662
 
657
663
  if (explicitScheduledTime) {
658
664
  // Agent explicitly passed a scheduled_time — treat as user’s local time
@@ -666,6 +672,15 @@ async function handleCreateTask(
666
672
  scheduledDateFinal = dateStr;
667
673
  // Normalize start_date to ISO UTC for consistent downstream parsing
668
674
  insertData.start_date = dt.toISOString();
675
+ } else if (isUtcInput) {
676
+ // Input is already UTC — no local→UTC conversion. Use parsedDate
677
+ // directly. scheduled_time is documented as user-local, so add the
678
+ // offset to recover the local-clock value.
679
+ nextRunAtEpoch = Math.floor(parsedDate.getTime() / 1000);
680
+ const localHour = (parsedDate.getUTCHours() + TZ_OFFSET_HOURS + 24) % 24;
681
+ scheduledTimeFinal = `${String(localHour).padStart(2, "0")}:${String(parsedDate.getUTCMinutes()).padStart(2, "0")}`;
682
+ scheduledDateFinal = parsedDate.toISOString().split("T")[0];
683
+ insertData.start_date = parsedDate.toISOString();
669
684
  } else if (hasTimeInfo) {
670
685
  // start_date already contains time — assume it’s in the user’s local timezone
671
686
  // Extract the local hour:minute from the string, NOT from UTC parsing
@@ -5387,14 +5402,36 @@ async function handleCreateSubagent(supabase: SupabaseClient, userId: string, pa
5387
5402
  return err(`Maximum ${MAX_SUBAGENTS_PER_CHIEF} subagents per department chief`);
5388
5403
  }
5389
5404
 
5390
- const role = (params.role as string) || "Staff";
5391
- const codename = (params.codename as string) ?? null;
5392
- const colorHex = (params.color_hex as string) || "#64748b";
5405
+ // BUG 1 fix (cycle 7b BUGSHOOT-1): persist persona/model/MCP fields on
5406
+ // create. Schema accepts them (see SUBAGENT_INPUT_SCHEMA above) but the
5407
+ // previous insert dropped everything except identity/UI fields, forcing a
5408
+ // follow-up update_subagent. Mirror the field whitelist used in
5409
+ // handleUpdateSubagent so create/update stay in lockstep.
5410
+ const insertRow: Record<string, unknown> = {
5411
+ user_id: userId,
5412
+ chief_agent_id: chiefAgentId,
5413
+ name,
5414
+ role: (params.role as string) || "Staff",
5415
+ codename: (params.codename as string) ?? null,
5416
+ color_hex: (params.color_hex as string) || "#64748b",
5417
+ };
5418
+ const personaFields = [
5419
+ "system_prompt", "mission", "responsibilities", "instructions",
5420
+ "primary_model", "coding_model", "tool_call_model", "function_call_model", "vision_model",
5421
+ ] as const;
5422
+ for (const key of personaFields) {
5423
+ if (key in params && params[key] !== undefined) insertRow[key] = params[key];
5424
+ }
5425
+ if ("mcp_server_ids" in params && Array.isArray(params.mcp_server_ids)) {
5426
+ insertRow.mcp_server_ids = (params.mcp_server_ids as unknown[]).filter(
5427
+ (x): x is string => typeof x === "string",
5428
+ );
5429
+ }
5393
5430
 
5394
5431
  const { data, error } = await supabase
5395
5432
  .from("agent_subagents")
5396
- .insert({ user_id: userId, chief_agent_id: chiefAgentId, name, role, codename, color_hex: colorHex })
5397
- .select("id, user_id, chief_agent_id, name, role, codename, color_hex, created_at")
5433
+ .insert(insertRow)
5434
+ .select()
5398
5435
  .single();
5399
5436
  if (error) return err(error.message);
5400
5437
  return ok({ subagent: data, message: `Subagent "${name}" created under chief ${chiefAgentId}` });