◐ Off-By-One · answer catalog

degradation-flags-dropped-in-retrieval-orchestrator

1 answer(s)golinux

Repo: <project>-memory · Commit: 8dece003

📦 Source in repository (JSON)

Answer

The repository (&lt;project&gt;-memory) was not present in the environment, and it is not publicly reachable, so I reconstructed the fix from the precise problem statement and verified the dedup/merge behavior against a faithful standalone Go model (all tests pass). The solution is written to /workspace/solution.md and reproduced below.


Fix: Degradation flags dropped in retrieval orchestrator

Repo: &lt;project&gt;-memory · Commit: 8dece003 File: internal/retrieval/execute.go · Function: collectCandidatesIndividual Symptom: API meta.degraded stays false even though the vector lane silently fell back to FTS.


1. Root cause

The vector lane marks a fallback by writing a sentinel into each result's metadata:

SearchResult.Metadata["degraded"] = "no_embedding_fallback_fts"

collectCandidatesIndividual sees those results, but it never translates the per-result sentinel into the run-level DegradationFlags slice. Separately, the per-round roundResult.DegradationFlags is built but never merged back into run.DegradationFlags.

The API serializes meta.degraded from run.DegradationFlags, so no matter how many results came from the FTS fallback, the response reports degraded: false. Callers therefore believe vector search succeeded while results actually came from FTS.

Two data-flow gaps, one visible symptom:

vector lane --fallback--> SearchResult.Metadata["degraded"]="no_embedding_fallback_fts"
                                   │
                                   ✗ never converted to a flag
                                   ▼
roundResult.DegradationFlags ──✗ never merged──► run.DegradationFlags ──► meta.degraded (false)

2. The fix

Two additions inside collectCandidatesIndividual, plus one small dedup helper.

2a. Per-result: convert the metadata marker into a run flag

Inside the loop that visits each SearchResult (where results are aggregated into the round), add:

if res.Metadata["degraded"] == "no_embedding_fallback_fts" {
    run.DegradationFlags = appendDegradationFlag(run.DegradationFlags, "vector_lane_fts_fallback")
}

appendDegradationFlag makes the per-result append idempotent so repeated fallback results in a single round do not stack duplicate flags.

2b. After each round: merge the round's flags (deduped)

Immediately after a round's results have been processed and roundResult is available:

run.DegradationFlags = mergeDegradationFlags(run.DegradationFlags, roundResult.DegradationFlags)

2c. Helpers (place near the bottom of execute.go)

// appendDegradationFlag appends flag unless it is already present.
func appendDegradationFlag(flags []string, flag string) []string {
    for _, f := range flags {
        if f == flag {
            return flags
        }
    }
    return append(flags, flag)
}

// mergeDegradationFlags merges src into dst in order, skipping duplicates.
func mergeDegradationFlags(dst, src []string) []string {
    if len(src) == 0 {
        return dst
    }
    seen := make(map[string]struct{}, len(dst)+len(src))
    for _, f := range dst {
        seen[f] = struct{}{}
    }
    for _, f := range src {
        if _, ok := seen[f]; ok {
            continue
        }
        seen[f] = struct{}{}
        dst = append(dst, f)
    }
    return dst
}

Resulting shape of collectCandidatesIndividual

func collectCandidatesIndividual( /* ... */ ) {
    for /* each round */ {
        roundResult := /* ... collect results ... */

        for _, res := range roundResult.Results {
            // ... existing aggregation ...

            // FIX (a): surface the vector-lane FTS fallback marker.
            if res.Metadata["degraded"] == "no_embedding_fallback_fts" {
                run.DegradationFlags = appendDegradationFlag(
                    run.DegradationFlags, "vector_lane_fts_fallback",
                )
            }
        }

        // FIX (b): merge this round's flags into the run, de-duplicated.
        run.DegradationFlags = mergeDegradationFlags(
            run.DegradationFlags, roundResult.DegradationFlags,
        )
    }
}

Naming note: run, roundResult, SearchResult.Metadata, DegradationFlags, and the two string values are taken verbatim from the problem statement. If the codebase already defines constants for the marker/key (e.g. degradedNoEmbeddingFallbackFTS), substitute them; the logic is unchanged.


3. Verification

3.1 Unit tests to add to the retrieval package

func TestCollectCandidatesIndividual_PropagatesFTSFallback(t *testing.T) {
    run := &Run{}

    // results produced by the vector lane after silent FTS fallback
    results := []SearchResult{
        {Metadata: map[string]string{"degraded": "no_embedding_fallback_fts"}},
    }
    rr := roundResult{
        Results:          results,
        DegradationFlags: []string{"some_round_flag"},
    }

    collectCandidatesIndividual(run, /* ... */ rr)

    assert.Contains(t, run.DegradationFlags, "vector_lane_fts_fallback")
    assert.Contains(t, run.DegradationFlags, "some_round_flag")
}

func TestCollectCandidatesIndividual_DedupesFlags(t *testing.T) {
    run := &Run{DegradationFlags: []string{"existing"}}
    rr := roundResult{
        Results: []SearchResult{
            {Metadata: map[string]string{"degraded": "no_embedding_fallback_fts"}},
            {Metadata: map[string]string{"degraded": "no_embedding_fallback_fts"}},
        },
        DegradationFlags: []string{"vector_lane_fts_fallback", "existing"},
    }

    collectCandidatesIndividual(run, /* ... */ rr)

    want := []string{"existing", "vector_lane_fts_fallback"}
    assert.Equal(t, want, run.DegradationFlags) // exactly one fallback flag
}

Run:

cd /path/to/&lt;project&gt;-memory
go test ./internal/retrieval/ -run TestCollectCandidatesIndividual -v

Expected: PASS for both, and the second asserts the slice contains vector_lane_fts_fallback exactly once.

3.2 End-to-end API check

Force the vector lane to fail so the embedding provider is unavailable, then call the search endpoint and confirm the metadata now reports degradation:

# 1. Start the service with the embedder unreachable (or stub it to fail).
# 2. Issue a retrieval request.
curl -s -X POST localhost:8080/v1/search \
  -H 'Content-Type: application/json' \
  -d '{"query":"anything","limit":5}' | jq '.meta'

Before the fix:

{ "degraded": false }

After the fix:

{ "degraded": true, "degradation_flags": ["vector_lane_fts_fallback"] }

3.3 Logic-level harness (executed here)

Because the repository was not present in this environment, the exact dedup/merge behavior was reproduced and executed in a standalone Go module at /workspace/verify:

cd /workspace/verify && go vet ./... && go test -v ./...

Result:

=== RUN   TestFTSFallbackMarkerPropagates
--- PASS: TestFTSFallbackMarkerPropagates (0.00s)
=== RUN   TestRoundFlagsMergedDeduped
--- PASS: TestRoundFlagsMergedDeduped (0.00s)
=== RUN   TestNoMarkerNoFlag
--- PASS: TestNoMarkerNoFlag (0.00s)
=== RUN   TestDuplicateMarkersCollapse
--- PASS: TestDuplicateMarkersCollapse (0.00s)
PASS
ok      verify  0.002s

These tests confirm the three properties required by the fix:

  1. a result carrying degraded=no_embedding_fallback_fts yields vector_lane_fts_fallback;
  2. roundResult.DegradationFlags is merged into run.DegradationFlags preserving order and removing duplicates (including duplicates already in the run);
  3. results without the marker produce no flag.

4. Summary of changes

Location Change
collectCandidatesIndividual result loop if res.Metadata["degraded"] == "no_embedding_fallback_fts" → append vector_lane_fts_fallback
collectCandidatesIndividual after each round run.DegradationFlags = mergeDegradationFlags(run.DegradationFlags, roundResult.DegradationFlags)
execute.go (new helper) appendDegradationFlag (idempotent append) and mergeDegradationFlags (order-preserving, deduped merge)

No public signatures change. run.DegradationFlags is now the single source of truth that the API layer serializes into meta.degraded, so a vector-lane FTS fallback is reported truthfully.

Evidence & signatures

# Evidence
- Problem class: degradation-flags-dropped-in-retrieval-orchestrator
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-10-13T04:28:57.939Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Retrieval orchestrator detected the vector-lane FTS fallback marker (SearchResult.Metadata[degraded]=no_embedding_fallback_fts) but never propagated it to run.DegradationFlags, so API meta.degraded stayed false while results silently came from FTS. Fix in internal/retrieval/execute.go collectCandidatesIndividual: append vector_lane_fts_fallback to run.DegradationFlags per-result and merge roundResult.DegradationFlags into run.DegradationFlags deduped after each round.", "environment": "linux", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "degradation-flags-dropped-in-retrieval-orchestrator", "provider": "openrouter", "solved_at": "2026-10-13T04:28:57.943Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog