diff --git a/mateclaw-server/src/main/java/vip/mate/approval/ApprovalWorkflowService.java b/mateclaw-server/src/main/java/vip/mate/approval/ApprovalWorkflowService.java index 3f9d342d..70fc7322 100644 --- a/mateclaw-server/src/main/java/vip/mate/approval/ApprovalWorkflowService.java +++ b/mateclaw-server/src/main/java/vip/mate/approval/ApprovalWorkflowService.java @@ -260,6 +260,78 @@ public class ApprovalWorkflowService implements ApplicationRunner { toolCallPayload, siblingToolCalls, agentId, null); } + /** + * Workflow-scoped approval request — creates a {@code mate_tool_approval} + * row keyed to a workflow run + step instead of a conversation, so an + * {@code await_approval} step is visible in the same approval inbox the + * tool-approval flow uses. Returns the row's auto-generated long id; the + * caller (typically {@code AwaitApprovalStepAdapter}) writes that id back + * onto {@code mate_workflow_run_pause.external_approval_id} so a future + * approval-resolve callback can map "approval X resolved → resume run Y". + * + *
The approval row's {@code conversationId} is set to + * {@code "workflow:run:{runId}"} as a synthetic key — that lets the + * existing {@link ApprovalService#findPendingByConversation} surface the + * workflow approval to operator UIs without needing a parallel query + * surface. {@code toolName} is set to {@code "workflow:{kind}"} so the + * inbox can group / filter workflow approvals from tool approvals. + * + *
v0 keeps the resume path through {@code WorkflowResumeController}
+ * with the pauseToken; this method does not yet wire a resolve→resume
+ * callback. The approval row's purpose for v0 is operator visibility
+ * and a stable foreign key for the pause record.
+ */
+ public Long requestWorkflowApproval(long workspaceId,
+ long runId,
+ Long stepId,
+ String approvalKind,
+ String approvalMessage,
+ java.util.List The {@code external_approval_id} column on the pause row is reserved
- * for a future integration that bridges workflow pauses into the
- * tool-approval inbox; v0 leaves it null and routes resolution through the
- * pause-token path above.
+ * The {@code external_approval_id} column on the pause row links to the
+ * {@code mate_tool_approval} row created via
+ * {@link ApprovalWorkflowService#requestWorkflowApproval} so the workflow
+ * pause is visible in the same approval inbox the tool-approval flow uses.
+ * Resolution still goes through {@code WorkflowResumeController} +
+ * pauseToken — the approval row is for operator visibility today; v1 wires
+ * the resolve→resume callback so an inbox decision can also fire the
+ * resumer.
*/
@Component
public class AwaitApprovalStepAdapter implements StepAdapter {
private final WorkflowRunPauseMapper pauseMapper;
private final WorkflowRunStepMapper stepMapper;
+ /** Optional — not all test contexts wire the approval module up. The
+ * adapter falls back to a no-op approval row when null. */
+ @Autowired(required = false)
+ private ApprovalWorkflowService approvalService;
public AwaitApprovalStepAdapter(WorkflowRunPauseMapper pauseMapper,
WorkflowRunStepMapper stepMapper) {
@@ -77,6 +87,8 @@ public class AwaitApprovalStepAdapter implements StepAdapter {
String pauseToken = UUID.randomUUID().toString();
LocalDateTime now = LocalDateTime.now();
+ // Insert the pause row first so we have a stable id to reference
+ // even if the approval-service call below fails.
WorkflowRunPauseEntity pause = new WorkflowRunPauseEntity();
pause.setRunId(context.runId());
pause.setStepId(stepRow.getId());
@@ -88,6 +100,35 @@ public class AwaitApprovalStepAdapter implements StepAdapter {
}
pauseMapper.insert(pause);
+ // Bridge into the approval inbox: create a mate_tool_approval row so
+ // the workflow pause shows up alongside tool approvals, then write
+ // the row id back as external_approval_id for the future
+ // resolve→resume callback. Failures here are non-fatal — the run
+ // is still resolvable via pauseToken + WorkflowResumeController.
+ if (approvalService != null) {
+ try {
+ Long approvalId = approvalService.requestWorkflowApproval(
+ context.workspaceId(),
+ context.runId(),
+ stepRow.getId(),
+ cfg.approvalKind(),
+ cfg.approvalMessage(),
+ cfg.approverChannels(),
+ cfg.timeoutSecs());
+ if (approvalId != null) {
+ pause.setExternalApprovalId(approvalId);
+ pauseMapper.updateById(pause);
+ }
+ } catch (Exception e) {
+ // Non-fatal — log and continue. The pause row is the
+ // canonical record for v0; the approval row is a parallel
+ // visibility surface that can rebuild later if needed.
+ org.slf4j.LoggerFactory.getLogger(AwaitApprovalStepAdapter.class)
+ .warn("await_approval failed to create approval row for run {}: {}",
+ context.runId(), e.getMessage());
+ }
+ }
+
return StepResult.paused(pauseToken,
"awaiting " + (cfg.approvalKind() == null ? "approval" : cfg.approvalKind()));
}