diff --git a/internal/platform/canvas_ai_apply.go b/internal/platform/canvas_ai_apply.go index 2fbf9f3..ba71516 100644 --- a/internal/platform/canvas_ai_apply.go +++ b/internal/platform/canvas_ai_apply.go @@ -136,6 +136,7 @@ func (s *service) finishCanvasStepOutput(ctx context.Context, run canvasQueueRun eventBoard = step.BoardID } var appliedBoard any + var appliedEvent response var output any var version any if code == "" && started { @@ -158,9 +159,6 @@ func (s *service) finishCanvasStepOutput(ctx context.Context, run canvasQueueRun patch["text"] = generated.Text } - if err = appendAIEvent(ctx, tx, run.Project, run.Workspace, run.ID, "step.output", response{"step_id": step.ID, "board_id": eventBoard, "origin_board_id": step.BoardID, "canonical_root": step.CanonicalRoot, "target_incarnation": step.TargetIncarnation, "node_id": step.NodeID, "output": output}); err != nil { - return "", err - } _, role, permissionErr := lockCanvasAIRunRoute(ctx, tx, run.Project, run.Workspace, eventBoard, run.Account) if permissionErr != nil && permissionErr != sql.ErrNoRows { return "", permissionErr @@ -196,13 +194,29 @@ func (s *service) finishCanvasStepOutput(ctx context.Context, run canvasQueueRun appliedBoard = eventBoard applied := response{"step_id": step.ID, "board_id": eventBoard, "node_id": step.NodeID, "patch": patch, "canvas_version": change.Snapshot.Version} change.addTargetContext(applied, step) - if err = appendAIEvent(ctx, tx, run.Project, run.Workspace, run.ID, "step.applied", applied); err != nil { - return "", err - } + appliedEvent = applied } } } } + if output != nil { + event := response{"step_id": step.ID, "board_id": eventBoard, "origin_board_id": step.BoardID, "canonical_root": step.CanonicalRoot, "target_incarnation": step.TargetIncarnation, "node_id": step.NodeID, "output": output} + if status == "conflict" && identityOK && step.CanonicalRoot != "" && eventBoard != step.BoardID { + model, e := loadCanvasModel(ctx, tx, run.Project, run.Workspace, step.CanonicalRoot) + if e != nil { + return "", e + } + event["model"], event["document_format_version"], event["canvas_version"] = model.view(nil), canvasModelFormat, model.Versions[eventBoard] + } + if err = appendAIEvent(ctx, tx, run.Project, run.Workspace, run.ID, "step.output", event); err != nil { + return "", err + } + } + if appliedEvent != nil { + if err = appendAIEvent(ctx, tx, run.Project, run.Workspace, run.ID, "step.applied", appliedEvent); err != nil { + return "", err + } + } var outputJSON any if output != nil { encoded, _ := json.Marshal(output) diff --git a/internal/platform/canvas_ai_config.go b/internal/platform/canvas_ai_config.go index ae6bfe2..0066ed2 100644 --- a/internal/platform/canvas_ai_config.go +++ b/internal/platform/canvas_ai_config.go @@ -43,7 +43,9 @@ func validCanvasAI(value any, kind string) bool { } // Fingerprints exclude geometry and visual styles so a result can be applied -// after a concurrent drag. Content/configuration edits invalidate the result. +// after a concurrent drag. The board is the immutable plan origin, not the +// current canonical owner; canonical target identity is checked separately. +// Content/configuration edits invalidate the result. func canvasTargetFingerprint(board, node string, data map[string]any) string { target := map[string]any{"board_id": board, "node_id": node, "kind": data["kind"], "ai": data["ai"]} if canvasChoice(data["kind"], "image", "plain-image") {