Skip to content

sec ask on an empty index loads a model and generates a discarded answer: 100 s, warm, for a fixed refusal #369

Description

@sroussey

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/prdanalysis/grades/2026-09-14/sec-detailed.md §4.3.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions