Files
viettranx e62c027b7d feat(vault): task + delegation auto-linking in enrich worker
- Add enrich_auto_linking.go: deterministic auto-link logic for task/delegation contexts
- Add team_task_siblings.go (PG + SQLite): find all tasks in same delegation for batch link
- Update enrich_worker.go: call enrich_auto_linking Phase 2 hook + skip re-embed for binary
- Update team_store.go: add TaskSiblings interface method + TeamStore binding
- Add context_keys.go + test: define ContextDelegationIDKey + ContextTaskIDKey for propagation
- Update vault_interceptor.go: extract + inject delegation ID from request
- Update run_context.go: propagate DelegationID from request
- Update loop_context.go: inject DelegationID from run context
- Update teams_tasks.go: DeleteTask(s) → cleanup auto-links via vault_source_cleanup
- Update teams_tasks_activity.go: DetachFileFromTask → cleanup related auto-links
- Update vault_documents_enrichment.go: populate summary from media_summary
- Update cmd/gateway.go: wire TeamStore to handler bootstrap
2026-04-11 21:22:23 +07:00

120 lines
2.9 KiB
Go

//go:build sqlite || sqliteonly
package sqlitestore
import (
"context"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// BatchGetTaskSiblingsByBasenames — SQLite mirror of the PG implementation.
// Uses dynamic `?,?,?` placeholders (no array type in sqlite) and the same
// PARTITION BY (source_base, task_id) cap semantics.
func (s *SQLiteTeamStore) BatchGetTaskSiblingsByBasenames(
ctx context.Context,
tenantID uuid.UUID,
basenames []string,
limit int,
) (map[string][]store.TaskSibling, error) {
if len(basenames) == 0 {
return nil, nil
}
if limit <= 0 {
limit = 9
}
// Lowercase + dedup defensively.
seen := make(map[string]bool, len(basenames))
clean := make([]string, 0, len(basenames))
for _, b := range basenames {
lb := strings.ToLower(b)
if lb == "" || seen[lb] {
continue
}
seen[lb] = true
clean = append(clean, lb)
}
out := make(map[string][]store.TaskSibling, len(clean))
const chunkSize = 500
for start := 0; start < len(clean); start += chunkSize {
end := start + chunkSize
if end > len(clean) {
end = len(clean)
}
chunk := clean[start:end]
ph := make([]string, len(chunk))
args := []any{tenantID.String()}
for i, b := range chunk {
ph[i] = "?"
args = append(args, b)
}
args = append(args, limit)
q := fmt.Sprintf(`
WITH target_tasks AS (
SELECT DISTINCT tta.base_name AS source_base, tta.task_id
FROM team_task_attachments tta
WHERE tta.tenant_id = ? AND tta.base_name IN (%s)
),
ranked AS (
SELECT
tt.source_base,
tta2.task_id,
vd.id AS doc_id,
tta2.base_name,
tta2.created_at,
ROW_NUMBER() OVER (
PARTITION BY tt.source_base, tta2.task_id
ORDER BY tta2.created_at DESC, vd.id DESC
) AS rn
FROM target_tasks tt
JOIN team_task_attachments tta2 ON tta2.task_id = tt.task_id
JOIN vault_documents vd
ON vd.tenant_id = tta2.tenant_id
AND vd.team_id = tta2.team_id
AND vd.path_basename = tta2.base_name
WHERE tta2.base_name != tt.source_base
)
SELECT source_base, task_id, doc_id, base_name, created_at
FROM ranked
WHERE rn <= ?
ORDER BY source_base, task_id, created_at DESC
`, strings.Join(ph, ","))
rows, err := s.db.QueryContext(ctx, q, args...)
if err != nil {
return nil, fmt.Errorf("batch task siblings: %w", err)
}
for rows.Next() {
var sourceBase string
var taskIDStr, docIDStr string
var sib store.TaskSibling
ca := &sqliteTime{}
if err := rows.Scan(&sourceBase, &taskIDStr, &docIDStr, &sib.BaseName, ca); err != nil {
rows.Close()
return nil, err
}
if tid, perr := uuid.Parse(taskIDStr); perr == nil {
sib.TaskID = tid
}
if did, perr := uuid.Parse(docIDStr); perr == nil {
sib.DocID = did
}
sib.AttachmentTime = ca.Time
out[sourceBase] = append(out[sourceBase], sib)
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, err
}
}
return out, nil
}