ofiere-openclaw-plugin 4.54.2 → 4.56.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/src/tools.ts CHANGED
@@ -23,6 +23,8 @@ import {
23
23
  } from "./attachments.js";
24
24
  import { invalidateAgentTier } from "./agent-tier.js";
25
25
  import { loadSubagentRow, readDispatchSubagentId, loadDispatchContextBySession, type DispatchContextRow } from "./staffPersona.js";
26
+ import { expandPlanToDependencyEdges, type PlanNode } from "./planExecute.js";
27
+ import { registerGateOps } from "./gateOps.js";
26
28
 
27
29
  // ─── Tool result shape (matches OpenClaw SDK) ────────────────────────────────
28
30
 
@@ -5033,7 +5035,7 @@ async function handleExecutePlan(
5033
5035
 
5034
5036
  while (queue.length > 0) {
5035
5037
  const { node, parentTaskId } = queue.shift()!;
5036
- if (node.type === "task") {
5038
+ if (node.type === "task" || node.type === "milestone") {
5037
5039
  const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
5038
5040
  const now = new Date().toISOString();
5039
5041
  const agentId = node.agentId ? await resolveAgent(node.agentId) : null;
@@ -5094,70 +5096,36 @@ async function handleExecutePlan(
5094
5096
  }
5095
5097
  }
5096
5098
 
5097
- // Step 3: Create dependencies sibling chaining + parent→child
5098
- const depsQueue = [...rootNodes];
5099
+ // Step 3: Build dependencies + manual-gate rows via shared helper.
5100
+ // The helper handles gate-skip-through (Option A), OR-join via group_id (Option B),
5101
+ // and manual gates via pm_gates rows (Option C). See dashboard/lib/pm/planExecute.ts.
5102
+ const { edges, gates: gateRows, emptyGates } = expandPlanToDependencyEdges(rootNodes as PlanNode[], idMap);
5099
5103
  let depsCreated = 0;
5104
+ let gatesCreated = 0;
5100
5105
 
5101
- // Helper: resolve a node to its nearest task ID (skip gates/milestones)
5102
- function resolveTaskId(n: any): string | undefined {
5103
- return idMap.get(n.id);
5104
- }
5105
-
5106
- // Helper: insert a dependency row into pm_dependencies
5107
- async function insertDep(predId: string, succId: string): Promise<boolean> {
5106
+ for (const e of edges) {
5108
5107
  const { error } = await supabase.from("pm_dependencies").insert({
5109
5108
  id: `dep-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
5110
- user_id: userId, predecessor_id: predId, successor_id: succId,
5111
- dependency_type: "finish_to_start", lag_days: 0,
5109
+ user_id: userId,
5110
+ predecessor_id: e.predTaskId,
5111
+ successor_id: e.succTaskId,
5112
+ dependency_type: e.dependencyType,
5113
+ lag_days: e.lagDays,
5114
+ group_id: e.groupId,
5112
5115
  });
5113
- return !error;
5114
- }
5115
-
5116
- while (depsQueue.length > 0) {
5117
- const node = depsQueue.shift()!;
5118
- const nodeRealId = resolveTaskId(node);
5119
-
5120
- // Chain siblings: sequential children of this node get chained left-to-right
5121
- if (Array.isArray(node.children) && node.children.length > 1 && !node.parallel) {
5122
- const taskChildren: string[] = [];
5123
- for (const child of node.children) {
5124
- const cid = resolveTaskId(child);
5125
- if (cid) taskChildren.push(cid);
5126
- }
5127
- for (let i = 1; i < taskChildren.length; i++) {
5128
- if (await insertDep(taskChildren[i - 1], taskChildren[i])) depsCreated++;
5129
- }
5130
- }
5131
-
5132
- // Parent → first child link (sequential only)
5133
- if (nodeRealId && !node.parallel && node.children?.length > 0) {
5134
- const firstChildId = resolveTaskId(node.children[0]);
5135
- if (firstChildId) {
5136
- if (await insertDep(nodeRealId, firstChildId)) depsCreated++;
5137
- }
5138
- }
5139
-
5140
- // Parallel fan-out: parent → each child
5141
- if (nodeRealId && node.parallel && node.children?.length > 0) {
5142
- for (const child of node.children) {
5143
- const childId = resolveTaskId(child);
5144
- if (childId) {
5145
- if (await insertDep(nodeRealId, childId)) depsCreated++;
5146
- }
5147
- }
5148
- }
5149
-
5150
- if (Array.isArray(node.children)) depsQueue.push(...node.children);
5116
+ if (!error) depsCreated++;
5151
5117
  }
5152
5118
 
5153
- // Chain root-level siblings (sequential root nodes)
5154
- const rootTaskIds: string[] = [];
5155
- for (const rn of rootNodes) {
5156
- const rid = resolveTaskId(rn);
5157
- if (rid) rootTaskIds.push(rid);
5158
- }
5159
- for (let i = 1; i < rootTaskIds.length; i++) {
5160
- if (await insertDep(rootTaskIds[i - 1], rootTaskIds[i])) depsCreated++;
5119
+ for (const g of gateRows) {
5120
+ const { error } = await supabase.from("pm_gates").insert({
5121
+ user_id: userId,
5122
+ plan_id: params.plan_id as string,
5123
+ plan_node_id: g.planNodeId,
5124
+ gate_label: g.gateLabel,
5125
+ gate_condition: g.gateCondition,
5126
+ task_id_blocked: g.taskIdBlocked,
5127
+ });
5128
+ if (!error) gatesCreated++;
5161
5129
  }
5162
5130
 
5163
5131
  // Mark plan as deployed
@@ -5167,6 +5135,8 @@ async function handleExecutePlan(
5167
5135
  message: `Plan "${data.name || plan.name}" executed successfully`,
5168
5136
  tasks_created: tasksCreated,
5169
5137
  dependencies_created: depsCreated,
5138
+ gates_created: gatesCreated,
5139
+ empty_gates: emptyGates,
5170
5140
  folder_id: folderId,
5171
5141
  space_id: spaceId,
5172
5142
  ...(folderSkipped ? { folder_skipped_reason: "No space_id provided — assign the plan to a space to enable folder creation" } : {}),
@@ -7088,6 +7058,7 @@ export function registerTools(
7088
7058
  registerTalentOps(api, supabase, userId); // 15
7089
7059
  registerFrameworkOps(api, supabase, userId, resolveAgent); // 16
7090
7060
  registerOfficeOps(api, supabase, userId, resolveAgent); // 17
7061
+ registerGateOps(api, supabase, userId, fallbackAgentId); // 18
7091
7062
 
7092
7063
  // ── Register dynamic brain context hook ──
7093
7064
  registerBrainContextHook(api, supabase, userId, fallbackAgentId);
@@ -7102,7 +7073,7 @@ export function registerTools(
7102
7073
  registerBrainExtractionHook(api, supabase, userId, fallbackAgentId);
7103
7074
 
7104
7075
  // ── Count and log ──
7105
- const toolCount = 17;
7076
+ const toolCount = 18;
7106
7077
  const callerName = getCallingAgentName(api);
7107
7078
  const agentLabel = fallbackAgentId || callerName || "auto-detect";
7108
7079
  api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);
@@ -7395,6 +7366,118 @@ function registerBrainExtractionHook(
7395
7366
  return;
7396
7367
  }
7397
7368
 
7369
+ // FIX-A2A-LATE-REPLY — direct chief↔chief A2A late reply.
7370
+ // Dashboard's relayToOpenClaw caps the synchronous wait at 45s; when
7371
+ // the responding chief takes longer (e.g. while running its own tool
7372
+ // calls), the dashboard WS closes and the eventual reply is dropped.
7373
+ // dispatch_context was pre-written with { conversation_id,
7374
+ // source_agent_id, a2a_pair_id } so we can route the late reply back:
7375
+ // 1. Insert the assistant turn into conversation_messages for the
7376
+ // A2A conversation so reloading the page shows the reply.
7377
+ // 2. POST a `chief_reply` webhook so the dashboard SSE can swap the
7378
+ // "still processing" placeholder bubble in the active chat UI.
7379
+ // Guarded by: a2a_pair_id is set AND subagent_id is null AND task_id
7380
+ // is null — so neither the staff nor the scheduled branches above
7381
+ // can collide with this path.
7382
+ const a2aPairId = dispatchCtxRow?.a2a_pair_id ?? null;
7383
+ const a2aSourceAgentId = dispatchCtxRow?.source_agent_id ?? null;
7384
+ const a2aConversationId = dispatchCtxRow?.conversation_id ?? null;
7385
+ if (
7386
+ a2aPairId &&
7387
+ !staffDispatchSubagentId &&
7388
+ !dispatchTaskIdFromCtx &&
7389
+ a2aConversationId
7390
+ ) {
7391
+ (async () => {
7392
+ try {
7393
+ const excerpt = lastAssistant.slice(0, 4000);
7394
+ if (!excerpt) {
7395
+ api.logger.warn?.(`[ofiere-a2a-reply] empty assistant text — skipping late-reply persistence`);
7396
+ return;
7397
+ }
7398
+
7399
+ // Insert assistant turn into the A2A conversation. The
7400
+ // dashboard's synchronous path also writes the assistant row
7401
+ // when the reply lands in-window — UI dedup is by content +
7402
+ // a2a_conversation_id so a duplicate from a race is benign.
7403
+ try {
7404
+ await supabase.from("conversation_messages").insert({
7405
+ conversation_id: a2aConversationId,
7406
+ user_id: userId,
7407
+ role: "assistant",
7408
+ content: excerpt,
7409
+ metadata: {
7410
+ sender_agent_id: resolvedAgentId,
7411
+ target_agent_id: a2aSourceAgentId,
7412
+ late_reply: true,
7413
+ },
7414
+ branch_id: "main",
7415
+ });
7416
+ await supabase.from("conversations")
7417
+ .update({ updated_at: new Date().toISOString() })
7418
+ .eq("id", a2aConversationId)
7419
+ .eq("user_id", userId);
7420
+ // Best-effort message-count bump. RPC may not exist on older
7421
+ // schemas — swallow the error if it 404s.
7422
+ try {
7423
+ await supabase.rpc("increment_conversation_message_count", {
7424
+ conv_id: a2aConversationId,
7425
+ });
7426
+ } catch { /* RPC missing — count drift is acceptable */ }
7427
+ } catch (cmErr) {
7428
+ api.logger.warn?.(`[ofiere-a2a-reply] conversation_messages insert failed: ${cmErr instanceof Error ? cmErr.message : String(cmErr)}`);
7429
+ }
7430
+
7431
+ // Resolve responder agent name for the webhook payload so the
7432
+ // UI bubble can be attributed correctly without another DB hit.
7433
+ let responderName: string | null = null;
7434
+ try {
7435
+ const { data: agentRow } = await supabase
7436
+ .from("agents")
7437
+ .select("name")
7438
+ .eq("id", resolvedAgentId)
7439
+ .eq("user_id", userId)
7440
+ .maybeSingle();
7441
+ responderName = (agentRow?.name as string | undefined) ?? null;
7442
+ } catch { /* fall back to agent id */ }
7443
+
7444
+ const webhookUrl = process.env.OFIERE_DASHBOARD_WEBHOOK_URL || "";
7445
+ const webhookSecret = process.env.OPENCLAW_WEBHOOK_SECRET || "";
7446
+ if (webhookUrl && webhookSecret) {
7447
+ try {
7448
+ await fetch(webhookUrl, {
7449
+ method: "POST",
7450
+ headers: {
7451
+ "content-type": "application/json",
7452
+ authorization: `Bearer ${webhookSecret}`,
7453
+ },
7454
+ body: JSON.stringify({
7455
+ type: "chief_reply",
7456
+ payload: {
7457
+ user_id: userId,
7458
+ source_agent_id: a2aSourceAgentId,
7459
+ target_agent_id: resolvedAgentId,
7460
+ target_agent_name: responderName,
7461
+ conversation_id: a2aConversationId,
7462
+ a2a_pair_id: a2aPairId,
7463
+ response: excerpt,
7464
+ },
7465
+ }),
7466
+ });
7467
+ api.logger.info?.(`[ofiere-a2a-reply] chief_reply webhook posted (conv=${a2aConversationId})`);
7468
+ } catch (wErr) {
7469
+ api.logger.debug?.(`[ofiere-a2a-reply] webhook post failed: ${wErr instanceof Error ? wErr.message : String(wErr)}`);
7470
+ }
7471
+ }
7472
+ } catch (aErr) {
7473
+ api.logger.warn?.(`[ofiere-a2a-reply] failed: ${aErr instanceof Error ? aErr.message : String(aErr)}`);
7474
+ }
7475
+ })();
7476
+ // Continue to brain extraction below — A2A late-reply is purely
7477
+ // additive bookkeeping; the responding chief still owns the L1/L2
7478
+ // memory writes for its own turn.
7479
+ }
7480
+
7398
7481
  // BUG 9 fix (BUGSHOOT-2): mark task_dispatch_log row complete for
7399
7482
  // non-staff scheduled dispatches too. Without this the log row stays
7400
7483
  // at 'dispatched' even after the chief finished the run.