Preserve relocation context for conflicted AI outputs
Foundry Go checks / check (push) Successful in 3m13s
Foundry Go checks / check (pull_request) Successful in 2m57s

This commit is contained in:
2026-11-08 01:20:06 +08:00
parent 2d4aefbdc0
commit c9bb38454d
2 changed files with 23 additions and 7 deletions
+20 -6
View File
@@ -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)
+3 -1
View File
@@ -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") {