Files
kandev/apps/backend/internal/orchestrator/messagequeue/merge.go
T
2026-09-14 19:19:09 +01:00

121 lines
4.4 KiB
Go

package messagequeue
import (
"errors"
"fmt"
"github.com/kandev/kandev/internal/entityrefs"
"github.com/kandev/kandev/internal/task/plancomments"
apiv1 "github.com/kandev/kandev/pkg/api/v1"
)
// joinMergeContent concatenates the source content below the target content,
// separated by a blank line. When the target content is empty the source is
// used verbatim so there is no leading blank line.
func joinMergeContent(targetContent, sourceContent string) string {
if targetContent == "" {
return sourceContent
}
return targetContent + "\n\n" + sourceContent
}
// ErrMergeReferenceOverflow reports a merge whose combined entity-reference
// lists would exceed entityrefs' per-message cap. The merge is rejected
// atomically — neither row is touched — rather than silently dropping
// references that were already persisted.
var ErrMergeReferenceOverflow = errors.New("merge would exceed the per-message entity reference limit")
// mergeAllowed reports whether the source entry may be folded into the target
// entry under the caller identity queuedBy. User entries merge only into user
// entries owned by the caller; agent entries merge only into agent entries
// produced by the same sender task; workflow/server/system sources and
// reserved in-flight targets are never mergeable.
func mergeAllowed(source, target *QueuedMessage, queuedBy string) bool {
if target == nil || target.IsReservedInFlight() ||
hasPlanCommentAdmission(source.Metadata) || hasPlanCommentAdmission(target.Metadata) {
return false
}
if source.QueuedBy == QueuedByAgent {
if target.QueuedBy != QueuedByAgent {
return false
}
sourceSender := metadataString(source.Metadata, MetadataSenderTaskID)
return sourceSender != "" && sourceSender == metadataString(target.Metadata, MetadataSenderTaskID)
}
if IsReservedQueuedBy(source.QueuedBy) {
return false
}
return queuedBy != "" && !IsReservedQueuedBy(queuedBy) &&
source.QueuedBy == queuedBy && target.QueuedBy == queuedBy
}
func hasPlanCommentAdmission(metadata map[string]interface{}) bool {
for _, key := range []string{
plancomments.MetadataRefs,
plancomments.MetadataRequestFingerprint,
plancomments.MetadataClientQueueID,
} {
if _, ok := metadata[key]; ok {
return true
}
}
return false
}
// mergeEntryMetadata returns a copy of the target metadata with additive
// entity references and ordinary admission provenance replaced by their
// normalized, deduplicated unions. Empty union keys are removed. Returns
// ErrMergeReferenceOverflow when the entity-reference union exceeds its cap.
func mergeEntryMetadata(target, source map[string]interface{}) (map[string]interface{}, error) {
merged := make(map[string]interface{}, len(target)+1)
for key, value := range target {
merged[key] = value
}
union, err := unionEntityReferences(target, source)
if err != nil {
return nil, err
}
if len(union) == 0 {
delete(merged, MetadataEntityReferences)
} else {
merged[MetadataEntityReferences] = union
}
admissionIDs, ok := unionQueueAdmissionIDs(target, source)
if !ok {
return nil, ErrInvalidQueueAdmissionIDs
}
if len(admissionIDs) == 0 {
delete(merged, MetadataQueueAdmissionIDs)
} else {
merged[MetadataQueueAdmissionIDs] = admissionIDs
}
return merged, nil
}
// unionEntityReferences returns the deduplicated union of two entries' entity
// reference lists, normalized from either their Go-struct or JSON-roundtripped
// storage form. When the union would exceed the per-message limit entityrefs
// enforces (MaxReferencesPerMessage), it returns ErrMergeReferenceOverflow so
// the caller can reject the merge atomically instead of silently dropping
// references that were already persisted.
func unionEntityReferences(targetMetadata, sourceMetadata map[string]interface{}) ([]apiv1.EntityReference, error) {
seen := make(map[string]struct{})
var union []apiv1.EntityReference
for _, references := range [][]apiv1.EntityReference{
entityrefs.NormalizePersisted(targetMetadata[MetadataEntityReferences]),
entityrefs.NormalizePersisted(sourceMetadata[MetadataEntityReferences]),
} {
for _, ref := range references {
if _, duplicate := seen[ref.Ref]; duplicate {
continue
}
seen[ref.Ref] = struct{}{}
union = append(union, ref)
}
}
if len(union) > entityrefs.MaxReferencesPerMessage {
return nil, fmt.Errorf("%w: at most %d references are allowed", ErrMergeReferenceOverflow, entityrefs.MaxReferencesPerMessage)
}
return union, nil
}