Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -934,14 +934,23 @@ export async function recallForClients(o = {}) {
_out: out,
})

// Capture the trace BEFORE the filters. recallTrace rides on the returned
// array as a non-enumerable property, and Array.prototype.filter builds a new
// array — so reading it after any filter yields undefined and the response
// carries trace_id: null. The trace itself was persisted either way, which is
// what made this quiet: rows accumulate in recall_traces that no caller can
// ever name. Every real caller filters (min_importance and level are how the
// hooks call this), so in practice trace_id was null whenever it mattered and
// present only in the unfiltered case nobody uses.
const trace = memories.recallTrace

if (minImportance > 0) memories = memories.filter(m => (m.importance || 0) >= minImportance)
if (levels.length > 0) memories = memories.filter(m => levels.includes(m.memory_level))
// requireVec: keep only rows with vector evidence, BEFORE the slice. Chinese
// char-level FTS OR-matches flood the RRF pool and evict semantic hits; inject
// callers need the vec rows to survive. With embedding down this yields 0 rows —
// fail-closed is correct for inject paths.
if (o.requireVec) memories = memories.filter(m => typeof m.vec_distance === 'number')
const trace = memories.recallTrace
memories = memories.slice(0, limit)

// hit_count is the raw pool; final_hit_count is what survived filtering and
Expand Down
27 changes: 27 additions & 0 deletions recall-endpoint.integration.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,33 @@ const post = async (body, init = {}) => {
check('capacity signals ride along', typeof j.requested_limit === 'number' && typeof j.effective_limit === 'number' && 'capped' in j)
}

// ── the trace id survives the filters ──
//
// recallTrace rides on the returned array as a non-enumerable property, and
// Array.prototype.filter builds a new array. Reading it after the filters
// yielded undefined, so every filtered call answered trace_id: null — while
// still persisting the trace. Rows piled up in recall_traces that no caller
// could name, and get_recall_trace / validate_memory_references had nothing to
// be called with.
//
// It stayed quiet because the unfiltered path, which nobody uses, worked fine.
// Both hooks send min_importance and level on every call.
{
const bare = await post({ query: 'omega calibration', limit: 3, source: 'trace-bare' })
check('unfiltered call returns a trace id', typeof bare.j?.trace_id === 'string' && bare.j.trace_id.length > 0,
JSON.stringify(bare.j?.trace_id))

for (const [label, body] of [
['min_importance', { query: 'omega calibration', limit: 3, min_importance: 6, source: 'trace-imp' }],
['level', { query: 'omega calibration', limit: 3, level: 'meta_knowledge,semi_abstract', source: 'trace-lvl' }],
['the shape both hooks send', { query: 'omega calibration', limit: 3, min_importance: 6, level: 'meta_knowledge,semi_abstract', require_vec: false, source: 'trace-hook' }],
]) {
const { j: r } = await post(body)
check(`trace id survives ${label}`, typeof r?.trace_id === 'string' && r.trace_id.length > 0,
`trace_id=${JSON.stringify(r?.trace_id)} hits=${r?.count}`)
}
}

// ── shape parity with the CLI: same query, same ids, same order ──
{
const args = ['index.mjs', '--recall', 'omega calibration', '--format', 'json',
Expand Down
Loading