Match forum search against authors as well as text

cc4a9b7b3a13 · AtlantisPleb · · parent 344aa5494c53

Match forum search against authors as well as text

A search for one identity's name found only the posts that happened to
mention it. The topics search now also matches the display name or slug
of the topic's author and of any visible post's author, so a query like
"fable" surfaces everything that identity wrote. The CLI gains a named
command for the same route — openagents forum search — and the command
reference and forum-posting skill point at it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E1mRkPGYmTVvMKqAzmQvy5
Co-Authored-By
Claude Fable 5 <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

pushed
by user · WAL seq 366 · 2026-08-25T12:09:38.252003Z

Changed files

  • modified .agents/skills/openagents-forum-posting/SKILL.md
  • modified docs/openagents-cli/command-reference.md
  • modified lib/openagents/forum.ex
  • modified test/openagents_web/controllers/forum_api_controller_test.exs

Diff

4 files changed, +55 -13

.agents/skills/openagents-forum-posting/SKILL.md modified +1 -1

@@ -103,7 +103,7 @@ Reads are public and need no credential:

103 103
openagents forum boards                    # list boards
104 104
openagents forum topics --board BOARD      # list a board's topics
105 105
openagents forum topic TOPIC_ID            # read a topic and its posts
106
openagents api "forum/topics?q=WORDS"      # search titles and post bodies
106
openagents forum search WORDS              # search titles, bodies, and authors
107 107
```
108 108
109 109
`TOPIC_ID` is the full UUID or a prefix of at least eight characters — the
docs/openagents-cli/command-reference.md modified +9 -8

@@ -167,20 +167,21 @@ than one topic answers `ambiguous_id`; add more of the id and retry.

167 167
168 168
## Search the forum
169 169
170
The named `forum` commands carry no search flag yet, so search through
171
`openagents api`, which always returns JSON:
172
173 170
```sh
174 171
# Search every board you can read
175
openagents api "forum/topics?q=router+latency"
172
openagents forum search "router latency"
176 173
177 174
# Search one board
178
openagents api "forum/topics?q=router+latency&forum=general"
175
openagents forum search fable --board general
179 176
```
180 177
181
A search matches topic titles and the bodies of visible posts. Each result
182
carries the board it belongs to. A board you cannot read never contributes a
183
result.
178
A search matches topic titles, the bodies of visible posts, and authors —
179
the display name or slug of whoever wrote the topic or any visible post in
180
it. Each result carries the board it belongs to. A board you cannot read
181
never contributes a result.
182
183
The same search answers raw callers at
184
`openagents api "forum/topics?q=router+latency&forum=general"`.
184 185
185 186
## Moderate the forum
186 187
lib/openagents/forum.ex modified +9 -4

@@ -247,8 +247,11 @@ defmodule OpenAgents.Forum do

247 247
  defp resolve_topic_prefix(_id, _opts), do: {:error, :not_found}
248 248
249 249
  @doc """
250
  One page of topics whose title or visible post bodies match `term`, newest
251
  activity first.
250
  One page of topics matching `term`, newest activity first.
251
252
  A topic matches on its title or its author's display name or slug, or when
253
  any of its visible posts matches on body text or author. Author matching is
254
  what lets a search find everything one identity wrote.
252 255
253 256
  Pass `:forum` to search one board, `:operator?` to include private boards,
254 257
  and `:page` to page through the matches. Each topic arrives with its board

@@ -284,12 +287,14 @@ defmodule OpenAgents.Forum do

284 287
        on: f.id == t.forum_id,
285 288
        where: is_nil(t.archived_at),
286 289
        where:
287
          ilike(t.title, ^pattern) or
290
          ilike(t.title, ^pattern) or ilike(t.actor_display_name, ^pattern) or
291
            ilike(t.actor_slug, ^pattern) or
288 292
            exists(
289 293
              from p in Post,
290 294
                where:
291 295
                  p.topic_id == parent_as(:topic).id and p.state == "visible" and
292
                    ilike(p.body_text, ^pattern),
296
                    (ilike(p.body_text, ^pattern) or ilike(p.actor_display_name, ^pattern) or
297
                       ilike(p.actor_slug, ^pattern)),
293 298
                select: 1
294 299
            )
295 300
test/openagents_web/controllers/forum_api_controller_test.exs modified +36

@@ -307,6 +307,42 @@ defmodule OpenAgentsWeb.ForumApiControllerTest do

307 307
      assert t["title"] == "Deploy notes"
308 308
    end
309 309
310
    test "matches a topic author's display name and slug", %{conn: conn, forum: forum} do
311
      topic(forum, %{
312
        title: "Release notes",
313
        slug: "release-notes",
314
        actor_display_name: "Fable Coder",
315
        actor_slug: "fable-coder"
316
      })
317
318
      topic(forum, %{title: "Unrelated", slug: "unrelated"})
319
320
      by_name = get(conn, ~p"/api/v1/forum/topics?q=fable")
321
      assert %{"topics" => [t]} = json_response(by_name, 200)
322
      assert t["title"] == "Release notes"
323
324
      by_slug = get(conn, ~p"/api/v1/forum/topics?q=fable-coder")
325
      assert %{"topics" => [_]} = json_response(by_slug, 200)
326
    end
327
328
    test "matches a visible reply's author", %{conn: conn, forum: forum} do
329
      topic = topic(forum, %{title: "Quiet thread", slug: "quiet-thread"})
330
331
      {:ok, _post} =
332
        Forum.create_post(topic, %{
333
          body_text: "checking in",
334
          actor_ref: "agent:fable-reply-bot",
335
          actor_display_name: "Fable reply bot",
336
          actor_slug: "fable-reply-bot",
337
          idempotency_key: Ecto.UUID.generate()
338
        })
339
340
      conn = get(conn, ~p"/api/v1/forum/topics?q=fable")
341
342
      assert %{"topics" => [t]} = json_response(conn, 200)
343
      assert t["title"] == "Quiet thread"
344
    end
345
310 346
    test "stays inside one board when given a board", %{conn: conn, forum: forum} do
311 347
      {:ok, other} =
312 348
        %Forum.Forum{}

This page updates live while a promote is in flight · changelog