kenn-io--agentsview
f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
229 行
8.0 KiB
Go
229 行
8.0 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// contentAnchorMeta is one match's anchor metadata: session lineage plus the
|
|
// anchor message row's classification columns (role, sidechain, embeddable).
|
|
// fillAnchorMeta resolves all of it post-truncation with one batched lookup —
|
|
// deliberately NOT in the search SQL, where extra columns would be evaluated
|
|
// for every candidate row before the LIMIT and carried through the sort.
|
|
// Rows whose message row does not exist are marked missing.
|
|
type contentAnchorMeta struct {
|
|
relationship string
|
|
parentSessionID string
|
|
role sql.NullString
|
|
sidechain sql.NullBool
|
|
embeddable sql.NullBool
|
|
missing bool
|
|
}
|
|
|
|
// deriveLexicalUnits is the shared post-scan pass for the substring, regex,
|
|
// and fts modes: it fetches each row's anchor classification and session
|
|
// lineage with one batched lookup, derives each match's conversation-unit
|
|
// range (DeriveUnitRanges issues one batched statement per seam method for
|
|
// the whole page, deduplicating duplicate probes), and assigns OrdinalRange
|
|
// plus the lineage fields. matches is the already-truncated page, so the
|
|
// pass is O(page).
|
|
func (db *DB) deriveLexicalUnits(
|
|
ctx context.Context, matches []ContentMatch,
|
|
) error {
|
|
if len(matches) == 0 {
|
|
return nil
|
|
}
|
|
metas, err := db.fillAnchorMeta(ctx, matches)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
anchors := buildContentUnitAnchors(matches, metas)
|
|
ranges, err := DeriveUnitRanges(ctx, db, anchors)
|
|
if err != nil {
|
|
return fmt.Errorf("deriving lexical units: %w", err)
|
|
}
|
|
for i := range matches {
|
|
matches[i].OrdinalRange = ranges[i]
|
|
matches[i].Relationship = metas[i].relationship
|
|
matches[i].ParentSessionID = metas[i].parentSessionID
|
|
matches[i].Sidechain = anchors[i].Sidechain
|
|
matches[i].Subordinate = anchors[i].Sidechain ||
|
|
SubordinateSession(metas[i].relationship, metas[i].parentSessionID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// buildContentUnitAnchors maps scanned sidecars to DeriveUnitRanges anchors.
|
|
// A missing anchor row keeps zero-valued classification fields, so it
|
|
// resolves to [o, o] with Sidechain false (session lineage still applies).
|
|
func buildContentUnitAnchors(
|
|
matches []ContentMatch, metas []contentAnchorMeta,
|
|
) []UnitAnchor {
|
|
anchors := make([]UnitAnchor, len(matches))
|
|
for i, m := range matches {
|
|
meta := metas[i]
|
|
anchors[i] = UnitAnchor{
|
|
SessionID: m.SessionID,
|
|
Ordinal: m.Ordinal,
|
|
Role: meta.role.String,
|
|
Sidechain: meta.sidechain.Valid && meta.sidechain.Bool,
|
|
Embeddable: meta.embeddable.Valid && meta.embeddable.Bool,
|
|
Missing: meta.missing,
|
|
}
|
|
}
|
|
return anchors
|
|
}
|
|
|
|
// fillAnchorMeta fetches anchor classification and session lineage for every
|
|
// page row via lookupAnchorMeta. Refs whose message row does not exist
|
|
// (tool_result_events orphans) are marked missing so derivation falls back
|
|
// to [o, o]; their session lineage is still populated via the sessions join.
|
|
// The result aligns 1:1 with matches.
|
|
func (db *DB) fillAnchorMeta(
|
|
ctx context.Context, matches []ContentMatch,
|
|
) ([]contentAnchorMeta, error) {
|
|
refs := make([]semanticHitKey, len(matches))
|
|
for i := range matches {
|
|
refs[i] = semanticHitKey{matches[i].SessionID, matches[i].Ordinal}
|
|
}
|
|
found, err := db.lookupAnchorMeta(ctx, refs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
metas := make([]contentAnchorMeta, len(matches))
|
|
for i, ref := range refs {
|
|
got, ok := found[ref]
|
|
if !ok {
|
|
metas[i].missing = true
|
|
continue
|
|
}
|
|
got.missing = !got.role.Valid
|
|
metas[i] = got
|
|
}
|
|
return metas, nil
|
|
}
|
|
|
|
// lookupAnchorMeta resolves anchor classification and session lineage for
|
|
// refs: one batched VALUES-CTE lookup per enrichHitsChunk distinct
|
|
// (session_id, ordinal) refs, never a per-row query. Refs whose session row
|
|
// is absent are omitted from the result map.
|
|
func (db *DB) lookupAnchorMeta(
|
|
ctx context.Context, refs []semanticHitKey,
|
|
) (map[semanticHitKey]contentAnchorMeta, error) {
|
|
seen := make(map[semanticHitKey]bool, len(refs))
|
|
distinct := make([]semanticHitKey, 0, len(refs))
|
|
for _, ref := range refs {
|
|
if !seen[ref] {
|
|
seen[ref] = true
|
|
distinct = append(distinct, ref)
|
|
}
|
|
}
|
|
found := make(map[semanticHitKey]contentAnchorMeta, len(distinct))
|
|
for start := 0; start < len(distinct); start += enrichHitsChunk {
|
|
chunk := distinct[start:min(start+enrichHitsChunk, len(distinct))]
|
|
if err := db.lookupAnchorMetaChunk(ctx, chunk, found); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return found, nil
|
|
}
|
|
|
|
// lookupAnchorMetaChunk resolves one chunk of (session_id, ordinal) refs to
|
|
// session lineage plus the anchor message row's classification columns:
|
|
// role, sidechain, and the embeddable flag (is_system = 0 AND content not
|
|
// system-prefixed — SystemPrefixSQL constrains only user rows, exactly like
|
|
// the embedding reducer's predicate). messages is LEFT JOINed so a ref whose
|
|
// message row is absent (tool_result_events orphan) still resolves lineage;
|
|
// its classification columns come back NULL.
|
|
func (db *DB) lookupAnchorMetaChunk(
|
|
ctx context.Context, refs []semanticHitKey,
|
|
out map[semanticHitKey]contentAnchorMeta,
|
|
) error {
|
|
values := make([]string, len(refs))
|
|
args := make([]any, 0, len(refs)*2)
|
|
for i, r := range refs {
|
|
values[i] = "(?, ?)"
|
|
args = append(args, r.sessionID, r.ordinal)
|
|
}
|
|
query := "WITH refs(session_id, ordinal) AS (VALUES " +
|
|
strings.Join(values, ", ") + ") " +
|
|
"SELECT r.session_id, r.ordinal, " +
|
|
"COALESCE(s.relationship_type,''), COALESCE(s.parent_session_id,''), " +
|
|
"m.role, m.is_sidechain, " +
|
|
"CASE WHEN m.is_system = 0 AND " +
|
|
SystemPrefixSQL("m.content", "m.role") + " THEN 1 ELSE 0 END " +
|
|
"FROM refs r " +
|
|
"JOIN sessions s ON s.id = r.session_id " +
|
|
"LEFT JOIN messages m ON m.session_id = r.session_id AND m.ordinal = r.ordinal"
|
|
|
|
rows, err := db.getReader().QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return fmt.Errorf("looking up match anchors: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var key semanticHitKey
|
|
var meta contentAnchorMeta
|
|
if err := rows.Scan(&key.sessionID, &key.ordinal,
|
|
&meta.relationship, &meta.parentSessionID,
|
|
&meta.role, &meta.sidechain, &meta.embeddable); err != nil {
|
|
return fmt.Errorf("scanning match anchor: %w", err)
|
|
}
|
|
out[key] = meta
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return fmt.Errorf("iterating match anchors: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// classifyUnitlessHybridHits assigns each unit-less hybrid FTS hit (the
|
|
// resolver returned no mirror unit; hits[i] for i in idxs) its structurally
|
|
// derived conversation-unit range and subordinate flag BEFORE scope filtering
|
|
// and fusion, so a unit-less sidechain (or subagent/fork-session) hit is
|
|
// excluded, included, and penalized exactly like lexical mode classifies the
|
|
// same anchor. One batched anchor lookup plus one DeriveUnitRanges pass
|
|
// covers all idxs — unit-less refs are rare (system rows or mirror lag), so
|
|
// the pre-merge leg stays cheap; mirror-unit rows keep their embedded span
|
|
// and resolver-provided subordinate flag untouched.
|
|
func (db *DB) classifyUnitlessHybridHits(
|
|
ctx context.Context, hits []hybridDisplay, idxs []int,
|
|
) error {
|
|
if len(idxs) == 0 {
|
|
return nil
|
|
}
|
|
refs := make([]semanticHitKey, len(idxs))
|
|
for k, i := range idxs {
|
|
refs[k] = semanticHitKey{hits[i].sessionID, hits[i].ordinal}
|
|
}
|
|
metas, err := db.lookupAnchorMeta(ctx, refs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
anchors := make([]UnitAnchor, len(idxs))
|
|
for k, ref := range refs {
|
|
meta := metas[ref]
|
|
anchors[k] = UnitAnchor{
|
|
SessionID: ref.sessionID,
|
|
Ordinal: ref.ordinal,
|
|
Role: meta.role.String,
|
|
Sidechain: meta.sidechain.Valid && meta.sidechain.Bool,
|
|
Embeddable: meta.embeddable.Valid && meta.embeddable.Bool,
|
|
Missing: !meta.role.Valid,
|
|
}
|
|
}
|
|
ranges, err := DeriveUnitRanges(ctx, db, anchors)
|
|
if err != nil {
|
|
return fmt.Errorf("deriving hybrid unit-less ranges: %w", err)
|
|
}
|
|
for k, i := range idxs {
|
|
hits[i].ordinalStart, hits[i].ordinalEnd = ranges[k][0], ranges[k][1]
|
|
meta := metas[refs[k]]
|
|
hits[i].subordinate = anchors[k].Sidechain ||
|
|
SubordinateSession(meta.relationship, meta.parentSessionID)
|
|
}
|
|
return nil
|
|
}
|