67609c52 fixed the right thing — an ungrounded run is no longer printed as an answer (#349) —
but the guard sits at the end of the expensive path rather than in front of it.
src/task/kb/AskTask.ts:137-196: execute resolves the generation model, opens the knowledge
base, runs AiChatWithKbTask (which embeds the query and generates a complete answer), and then
checks references.length === 0 and replaces that answer with a two-sentence refusal whose
content depends on nothing but await kb.chunkCount().
Measured
Warm — weights already downloaded and cached from a previous run — against a database whose
kb_chunk is empty:
$ sec db setup && sec index # creates the three kb tables, 0 documents
$ time sec ask "What is Apple's revenue?" --no-index
Nothing is indexed, so there is no filing text to answer from.
Run `sec index` to build the index — or `sec update documents` first, if no filings have been converted yet.
EXIT=0 ELAPSED=100s
100 seconds of model load and generation to print a string that was decided before any of it
started. On a cloud model — the path the README recommends whenever an API key is present — the
same run bills for a completion nobody reads, and does so on the first question a new user asks
after sec setup.
Fix
kb.chunkCount() is already on the ungrounded path. Called first, it answers in milliseconds:
const kb = await getSecKnowledgeBase();
const chunks = await kb.chunkCount();
if (chunks === 0) {
return { answer: ungroundedAnswer(0), references: [], grounded: false, ... };
}
const model = secGenerationModel(); // only now
That covers the "nothing is indexed" arm completely. The "indexed, but nothing matched" arm
genuinely needs retrieval — but not generation: the strategy's search could run first and the
model be resolved only if it returned something above ASK_MIN_SCORE. AiChatWithKbTask does
both in one call today, so splitting them means either calling kb.search(...) here and passing
the excerpts on, or asking @workglow/ai for a retrieval-first variant.
One ordering note: the current code resolves secGenerationModel() first deliberately, "so a
machine with no usable model says so before it spends time embedding a query" (:138-140). That
reason survives — model resolution is cheap and local; it is the loading and generation that
are not — so keep the resolve where it is and move only the work.
Related
The --json branch of the ungrounded path also drops the recovery step. ask.ts:143-146 returns
inside if (isJsonOutput()) before the suggest({ command: "sec index" }) at :153, so a
--json consumer gets "nextSteps": [] where a human gets the command — the opposite of
CLAUDE.md's "suppressed by --quiet and left as data under --json". Verified:
$ sec --json ask "What is Apple's revenue?" --no-index
{ ..., "grounded": false, "nextSteps": [] }
$ sec --json status | tail -6
"nextSteps": [ { "command": "sec load download ciks", "why": "…" } ]
Found during the 2026-09-14 review. Snapshot: workglow-dev/prd → analysis/grades/2026-09-14/sec-detailed.md §4.3.
67609c52fixed the right thing — an ungrounded run is no longer printed as an answer (#349) —but the guard sits at the end of the expensive path rather than in front of it.
src/task/kb/AskTask.ts:137-196:executeresolves the generation model, opens the knowledgebase, runs
AiChatWithKbTask(which embeds the query and generates a complete answer), and thenchecks
references.length === 0and replaces that answer with a two-sentence refusal whosecontent depends on nothing but
await kb.chunkCount().Measured
Warm — weights already downloaded and cached from a previous run — against a database whose
kb_chunkis empty:100 seconds of model load and generation to print a string that was decided before any of it
started. On a cloud model — the path the README recommends whenever an API key is present — the
same run bills for a completion nobody reads, and does so on the first question a new user asks
after
sec setup.Fix
kb.chunkCount()is already on the ungrounded path. Called first, it answers in milliseconds:That covers the "nothing is indexed" arm completely. The "indexed, but nothing matched" arm
genuinely needs retrieval — but not generation: the strategy's
searchcould run first and themodel be resolved only if it returned something above
ASK_MIN_SCORE.AiChatWithKbTaskdoesboth in one call today, so splitting them means either calling
kb.search(...)here and passingthe excerpts on, or asking
@workglow/aifor a retrieval-first variant.One ordering note: the current code resolves
secGenerationModel()first deliberately, "so amachine with no usable model says so before it spends time embedding a query" (
:138-140). Thatreason survives — model resolution is cheap and local; it is the loading and generation that
are not — so keep the resolve where it is and move only the work.
Related
The
--jsonbranch of the ungrounded path also drops the recovery step.ask.ts:143-146returnsinside
if (isJsonOutput())before thesuggest({ command: "sec index" })at:153, so a--jsonconsumer gets"nextSteps": []where a human gets the command — the opposite ofCLAUDE.md's "suppressed by
--quietand left as data under--json". Verified:Found during the 2026-09-14 review. Snapshot:
workglow-dev/prd→analysis/grades/2026-09-14/sec-detailed.md§4.3.