Skip to content

fix(cli): explain empty search, messages, and dms results - #233

Merged
steipete merged 8 commits into
openclaw:mainfrom
rnavarro:fix/search-zero-result-hints
Sep 14, 2026
Merged

fix(cli): explain empty search, messages, and dms results#233
steipete merged 8 commits into
openclaw:mainfrom
rnavarro:fix/search-zero-result-hints

Conversation

@rnavarro

@rnavarro rnavarro commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Empty local search, messages, and dms results now explain supported causes on stderr: uncrawled or forum channels, content filters, date windows, guild mismatches, multi-term search, and missing embedding coverage. JSON suppresses the notes; stdout and exit codes stay unchanged. The command documentation explains the stderr behavior and its limits. Aggregate notes are suppressed with --author, where message and search paths use different name matching; lexical term probes retain every filter. Contradictory date bounds get combined advice. Embedding notes respect local-only DMs, empty jobs, and the archive-wide queue limit.

Rebased onto current main and fixed the three failing semantic tests: the local embedding stub now uses and verifies a synthetic test credential instead of inheriting OPENAI_API_KEY from the developer environment.

Validation: focused CLI/store tests with OPENAI_API_KEY unset, the complete local gate on the final candidate (86.0% coverage), documentation tests/build, independent P0–P2 review, and a built-CLI comparison against main on a synthetic archive. All 15 CLI cases preserve stdout and exit codes byte-for-byte, and the suggested follow-up commands return rows.

Thanks @rnavarro for the fix.

The revised full local gate passed; CI is running on the updated candidate. Review’s sole remaining report assumed date flags on search; the source has no such fields and the built CLI rejects them, so that report was rejected with evidence.

@clawsweeper

clawsweeper Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@clawsweeper clawsweeper Bot added P3 Low-risk cleanup, docs, polish, ergonomics, or speculative feature. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Sep 13, 2026
@clawsweeper

clawsweeper Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Codex review: blocked before merge. Reviewed September 13, 2026, 10:44 PM ET / September 14, 2026, 02:44 UTC (Revision 5).

ClawSweeper review

What this changes

Adds explanations and recovery suggestions on stderr for empty local search, message, and DM results, with supporting queries, documentation, and regression tests.

Merge readiness

Blocked before merge - 1 item remains

The contribution remains useful and is absent from current main and v0.15.0. The prior recovery and documentation findings are addressed, and no remaining blocking defect was found.

Priority: P3
Reviewed head: 27d9f8a3fd8b8f2d53b9ca1e3b5d63c71b2826fc

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) Useful, bounded diagnostics with real CLI evidence and the prior actionable findings resolved.
Proof confidence 🐚 platinum hermit (4/6) Sufficient (terminal): The contributor's concrete CLI output and captured built-CLI comparison exercise the local empty-result path, report preserved stdout and exit codes across 15 cases, and report successful recovery commands; the existing sufficient proof assessment remains supported.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The contributor's concrete CLI output and captured built-CLI comparison exercise the local empty-result path, report preserved stdout and exit codes across 15 cases, and report successful recovery commands; the existing sufficient proof assessment remains supported.
Evidence reviewed 8 items Repository and applicable policy: Origin verifies the target repository. No root or changed-path ancestor AGENTS.md or maintainer-notes directory exists; the discovered AGENTS.md belongs only to the unrelated autoreview skill. The checkout remained clean.
Current-main necessity: Current main returns search results directly without the new empty-result explanations. Tree searches found adjacent SQL catalog warnings, not an equivalent implementation. A bounded listing of recent PRs found no replacement for this work.
Release comparison: The v0.15.0 messages command prints the store result directly, without this diagnostic layer.
Findings None None.
Security None None.

How this fits together

Discrawl searches and lists Discord messages stored in a local SQLite archive. The new diagnostic layer examines empty query results and local archive coverage, then suggests recovery steps without changing stdout or query results.

flowchart TD
  A[CLI query and filters] --> B[Local archive query]
  C[SQLite messages and embeddings] --> B
  B --> D{Empty result?}
  D -->|No| E[Existing stdout output]
  D -->|Yes| F[Check scope and coverage]
  C --> F
  F --> G[Stderr advice unless JSON]
Loading

Before merge

  • Resolve merge risk (P1) - Existing scripts that merge stderr into stdout receive new diagnostic lines after upgrading. This deliberate compatibility change is now documented and retained by the latest repair.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production and test growth production +561 net lines; tests +1255 lines Production growth supports shared diagnostic queries and recovery advice, with CLI and store regression coverage.

Merge-risk options

Maintainer options:

  1. Retain the documented stderr behavior (recommended)
    Accept the deliberate merged-stream output change supported by the current repair and reported before/after CLI comparison.

Technical review

Best possible solution:

Retain scope-aware recovery advice, unchanged stdout and JSON behavior, and explicit documentation of the adopted stderr default.

Do we have a high-confidence way to reproduce the issue?

Not applicable as a remaining bug: this adds diagnostic behavior, and the previously identified defects are corrected in current source.

Is this the best way to solve the issue?

Yes. A shared, bounded diagnostic layer reuses existing query and recovery paths while preserving result semantics and suppressing explanations where the aggregate scope would be inaccurate.

AGENTS.md: not found in the target repository.

Codex review notes: model internal, reasoning medium; reviewed against 072bf87a9974.

Labels

Label changes:

  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The contributor's concrete CLI output and captured built-CLI comparison exercise the local empty-result path, report preserved stdout and exit codes across 15 cases, and report successful recovery commands; the existing sufficient proof assessment remains supported.
  • remove status: ⏳ waiting on author: Current PR status label is status: 👀 ready for maintainer look.
  • remove rating: 🦐 gold shrimp: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.

Label justifications:

  • P3: This improves CLI explanations without changing query results or repairing a blocked core workflow.
  • merge-risk: 🚨 compatibility: Default stderr notes deliberately change output for existing consumers using merged streams.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The contributor's concrete CLI output and captured built-CLI comparison exercise the local empty-result path, report preserved stdout and exit codes across 15 cases, and report successful recovery commands; the existing sufficient proof assessment remains supported.
  • proof: sufficient: Contributor real behavior proof is sufficient. The contributor's concrete CLI output and captured built-CLI comparison exercise the local empty-result path, report preserved stdout and exit codes across 15 cases, and report successful recovery commands; the existing sufficient proof assessment remains supported.

Evidence

What I checked:

  • Repository and applicable policy: Origin verifies the target repository. No root or changed-path ancestor AGENTS.md or maintainer-notes directory exists; the discovered AGENTS.md belongs only to the unrelated autoreview skill. The checkout remained clean. (27d9f8a3fd8b)
  • Current-main necessity: Current main returns search results directly without the new empty-result explanations. Tree searches found adjacent SQL catalog warnings, not an equivalent implementation. A bounded listing of recent PRs found no replacement for this work. (internal/cli/query_commands.go:65, 072bf87a9974)
  • Release comparison: The v0.15.0 messages command prints the store result directly, without this diagnostic layer. (internal/cli/messages.go:150, 2eb4aae89549)
  • Prior embedding findings resolved: Pending-job counts now exclude empty normalized content, deleted messages, and DMs. Scope statistics identify DMs from message rows rather than optional channel metadata. Store coverage checks these counts, and the CLI regression removes DM channel metadata before asserting that rebuild is not recommended. (internal/store/query.go:182, 27d9f8a3fd8b)
  • Query and recovery contracts: Inspected listing, lexical, multilingual, semantic, and hybrid callers together with embedding draining and rebuilding. Diagnostics preserve the different deleted-row predicates, suppress unsupported author aggregates, forward all listing window fields, and bound lexical probes to four. (internal/cli/zero_result_notes.go:109, 27d9f8a3fd8b)
  • Compatibility disposition: The command documentation now explicitly distinguishes unchanged stdout from changed merged-stream output. GitHub identifies steipete as author and committer of the latest repair, which retains these diagnostics and corrects their scope and recovery advice; this is substantive engagement beyond the contributor's earlier preference. (docs/commands/messages.md:44, 27d9f8a3fd8b)

Likely related people:

  • unknown: The claimed source-line change could not be verified from bounded local history. (role: source history unknown; confidence: low)
  • hannesrudolph: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (4 earlier review cycles)
  • reviewed 2026-09-13T03:06:20.128Z sha f720e39 :: blocked before merge. :: [P2] Pass the hours flag into the message diagnostic | [P2] Recommend an embedding recovery path that enqueues archived rows
  • reviewed 2026-09-13T04:03:26.580Z sha c5d4044 :: blocked before merge. :: [P2] Pass the hours flag into the message diagnostic | [P2] Recommend recovery that enqueues missing embedding jobs
  • reviewed 2026-09-13T05:09:36.120Z sha 5f6c7a0 :: blocked before merge. :: [P2] Exclude non-embeddable jobs from recovery selection | [P2] Handle unknown channel metadata before recommending rebuild | [P3] Document the changed output for merged-stream consumers
  • reviewed 2026-09-13T12:31:50.113Z sha 5f6c7a0 :: blocked before merge. :: [P2] Exclude non-embeddable jobs from recovery selection | [P2] Handle unknown channel metadata before recommending rebuild | [P3] Document the changed output for merged-stream consumers

@rnavarro

Copy link
Copy Markdown
Contributor Author

The date-window note said why an empty result was empty but not which channel it counted over: messages --channel 1494732128137515008 --days 1 printed "5 messages in scope but none within the last 1 days (newest: 2026-04-17T23:31:41Z); try without --days", while the channel-empty note on the same id printed "channel 1494732128137515008 (Qwen3.6-35B-A3B, kind=thread_public)". On an archive with thousands of channels that left me with the reason and a separate lookup to find out which channel it was about.

Every note that reports a count over a scope now names that scope through one helper, in the format the channel-empty note already used: "5 messages in channel 1494732128137515008 (Qwen3.6-35B-A3B, kind=thread_public) but none within the last 1 days (newest: 2026-04-17T23:31:41Z); try without --days". A guild-wide query keeps "in scope" and a resolved id that matches no channels row keeps the bare id, so neither case prints an empty parenthetical. That covers the --hours/--days/--since note, the --before note, the --include-empty note and the embedding-coverage note; the guild-scope note takes kind= from the same helper. The multi-term note is unchanged, since it reports query terms rather than a count over a scope and dms --search reaches it with no resolved channel.

Reason, timestamp and recommended command are unchanged, as are stderr-only delivery and --json suppression. The existing assertions moved to the new exact strings rather than being loosened, and two cases are new: the guild-wide wording, and an id with no channels row. Coverage 86.0% against the 85% floor.

@rnavarro

Copy link
Copy Markdown
Contributor Author

Both P2 findings are fixed, in 0fa7f8d and 5f6c7a0.

messages --channel ID --hours 6 printed nothing when every archived message predated the window. The initializer at the messages entry point supplied the resolved since but left hours zero, so no branch of explainEmptyDateWindow ran. It now passes hours, and the test drives messages --hours through Run(). I enumerated all six zeroResultWindow fields against both construction sites: dms already forwarded all six, messages was the only gap, and zeroResultScope's four fields are parameter-driven through one constructor so no site can under-fill them. --author is read by both queries and deliberately not forwarded, which TestExplainEmptyResults_DaysWindowNoteSuppressedWhenAuthorFilterExcludes pins.

The embedding note recommended discrawl embed, which drains jobs that already exist and creates none, so a scope with no pending job got a command that reported "processed": 0 and left the search empty. The note now reports which of three states the scope is in and names the command for it: pending jobs in scope get discrawl embed; no pending jobs get discrawl embed --rebuild, with its archive-wide requeue and its per-run --limit stated, because RequeueAllEmbeddingJobs is the only supported way to create missing jobs; a direct-message scope gets neither, since both the enqueue-on-write path and the rebuild filter guild_id != '@me' and no embed run gives those messages embeddings. Each of the three recommendations is executed in its own test through Run() against a stub OpenAI-compatible embedding server, with the search after it asserted to return rows.

On stderr by default, I am keeping it on and treating that as my decision rather than adding a flag. Anyone who folds stderr into stdout will see new lines after upgrading, and --json does not cover them, since it suppresses the notes only for callers who are already asking for JSON. The mitigations are --json and 2>/dev/null, both documented in the three command pages.

go build, go vet, make fmt, make lint and the full suite are clean; coverage 86.0% against the 85% floor.

rnavarro and others added 8 commits September 13, 2026 19:23
search and messages returned zero rows, exit 0, and printed nothing in
several situations where the data actually exists (never-crawled channel,
a --days/--since window that misses everything, or an FTS query whose
terms are implicitly ANDed together). Add stderr-only diagnostics for
these cases without changing query semantics; they are suppressed under
--json so stdout stays machine-parseable.
The notes explaining an empty search or messages result were derived from a
row set the query never used, so several empty results stayed silent and one
note recommended something this search path cannot do.

- The multi-term note suggested an exact phrase. normalizeFTSQuery replaces
  double quotes with spaces and wraps each whitespace-delimited unit in an
  FTS5 phrase, so quoting changes nothing and the suggestion returns the same
  zero rows. Recommend a single distinctive term narrowed by --channel or
  --author, which this path does honour.
- The same note was skipped whenever the query contained a double quote.
  Quoted and unquoted multi-term queries have identical semantics here, so
  both now get the note, and the reported term is rendered the way the index
  sees it. Terms come from store.FTSQueryTerms, the split normalizeFTSQuery
  itself uses.
- A channel holding only empty or attachment-only messages produced no note:
  the stats query filtered on deleted_at while the message and search queries
  also drop rows with empty normalized content. MessageScopeStats now reports
  both counts and the notes name --include-empty for that case.
- The --days/--since note needed a resolved --channel to fire. It now runs off
  the same guild scope the query used, so a guild-wide window explains itself.
- --before had no note at all, and with --since set only --since was named.
  Each side of the window is evaluated and reported independently.
- Resolving one channel id for the note text scanned every archived channel
  row. Added store.ChannelByID, a primary-key lookup.
- The forum hint printed a SQL statement; it now names `channels list` and
  `messages --channel`.
- A long query issued one extra probe search per word; probes are capped.
- semantic and hybrid searches now explain a scope whose messages carry no
  embeddings for the configured provider and model, which a vector query
  cannot match, and hybrid also gets the FTS AND-hint for its lexical leg.
`messages --channel ID --days 1` printed "channel ID has no messages in the
local mirror" for a channel whose messages were deleted over the Gateway,
while plain `messages --channel ID` listed those same rows. store.ListMessages
carries no deleted_at predicate and returns soft-deleted rows; the stats query
behind the note always filtered them out, so the note described a narrower row
set than the query it was explaining. MessageScopeOptions now carries
IncludeDeleted and each entry point sets it to match its own query:
`messages` counts soft-deleted rows, `search` does not.

`dms` reaches the same store.ListMessages and store.SearchMessages queries and
returned zero rows just as silently, so it now carries the window and
multi-term notes. A `--with` run gets none: --with names a person and the query
matches it against channel id and channel name alike, which a channel_id-keyed
stats query cannot reproduce. --hours is a `dms` flag `messages` does not have
and now has its own note rather than falling through to the --days wording.

Documents the notes in docs/commands/{search,messages,dms}.md: they go to
stderr, --json suppresses them, and stdout and exit codes are unchanged.

Claude-Session: https://claude.ai/code/session_019WpK9xb3Z2Zym1T9C7EGQH
`messages --guild G --channel ID` printed "channel ID has no messages in the
local mirror" when ID exists but belongs to a different guild, which the same
command without --guild contradicts by printing rows. The note now names the
channel's actual guild and the --guild value that reaches it. Covered through
both `messages` and `search`, which share the helper that prints it.

Covers --mode hybrid, whose lexical leg is the same FTS query: it takes the
multi-term note, and the embedding-coverage note only once embeddings are
configured.

Claude-Session: https://claude.ai/code/session_019WpK9xb3Z2Zym1T9C7EGQH
`messages --channel ID --days 1` printed "5 messages in scope but none within
the last 1 days (newest: ...)" where the channel-empty note on the same id
printed "channel ID (Qwen3.6-35B-A3B, kind=thread_public)". The window note
carries the accurate reason and dropped the identifying detail.

Every note that reports a count over a scope now names that scope through one
helper, in the format the channel-empty note already used: the single channel
as "channel ID (name, kind=k)", a guild-wide query as "in scope", and a
resolved id with no channels row as the bare id, so no note prints an empty
parenthetical. That covers the --hours/--days/--since note, the --before note,
the --include-empty note and the embedding-coverage note; the guild-scope note
takes kind= from the same helper. Reason, timestamp and suggested command are
unchanged, as are stderr-only delivery and --json suppression.

Claude-Session: https://claude.ai/code/session_019WpK9xb3Z2Zym1T9C7EGQH
`messages --channel ID --hours 6` printed no explanation when every
archived message in the channel predated the window. The initializer at
the messages entry point supplied the resolved `since` timestamp but left
`hours` zero, and `explainEmptyDateWindow` selects its wording from the
flag fields, so none of its branches ran and the command stayed silent.

Pass `hours` alongside the other three window flags. The new test drives
`messages --channel ID --hours 6` through `Run()`, so the flag has to
survive the entry point that was dropping it rather than being handed
straight to the helper.

Claude-Session: https://claude.ai/code/session_019WpK9xb3Z2Zym1T9C7EGQH
… search

A semantic search over archived messages with no embedding job left the
reader at `discrawl embed`, which drains jobs that already exist and
creates none, so the run reported nothing to do and the next search was
still empty. `runEmbed` creates missing jobs only under --rebuild, via
store.RequeueAllEmbeddingJobs, and that requeues every non-deleted
message outside DMs archive-wide rather than the queried scope.

The note now reports which of three states the scope is in and names the
command that applies to it. With pending jobs in scope, `discrawl embed`
embeds them. With none, the note says `discrawl embed` drains nothing and
names `discrawl embed --rebuild`, with its archive-wide scope and its
--limit per run stated, so the scope is known before the command is run.
A direct-message scope gets neither: InsertMissingEmbeddingJobs and the
enqueue-on-write path both filter guild_id '@me', so no embed run gives
those messages embeddings and the note recommends --mode fts alone.

store.MessagePendingEmbeddingJobs supplies the count, applying the three
predicates ListPendingEmbeddingJobs applies, so it counts what an embed
run would act on rather than every embedding_jobs row.

Each recommended command is executed in its own test through `Run()`
against a stub OpenAI-compatible embedding server, and the search after
it asserts rows come back. The fixture embeds the archive and then
archives one more message, which is the only shape that reaches this
note: store.SearchMessagesSemantic returns ErrNoCompatibleEmbeddings
when the archive holds no compatible embeddings at all.

Claude-Session: https://claude.ai/code/session_019WpK9xb3Z2Zym1T9C7EGQH
Preserve supported diagnostic scopes, explain combined date exclusions and archive-wide embedding limits, and isolate semantic fixtures from live credentials.

Co-authored-by: Robert Navarro <crshman@gmail.com>
@steipete
steipete force-pushed the fix/search-zero-result-hints branch from 5f6c7a0 to 27d9f8a Compare September 14, 2026 02:36
@steipete steipete changed the title fix: search, messages, and dms print nothing when a filter excludes every row fix(cli): explain empty search, messages, and dms results Sep 14, 2026
@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. labels Sep 14, 2026
@steipete
steipete merged commit 7b78c2f into openclaw:main Sep 14, 2026
20 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P3 Low-risk cleanup, docs, polish, ergonomics, or speculative feature. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants