Skip to content

feat: bots ask structured questions that people answer from a card - #258

Open
sercada wants to merge 1 commit into
openclaw:mainfrom
sercada:feat/agent-questions
Open

sercada wants to merge 1 commit into
openclaw:mainfrom
sercada:feat/agent-questions

Conversation

@sercada

@sercada sercada commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor
Maintainer edits

MUST: Keep Allow edits from maintainers enabled for this PR so maintainers
can help update the branch when needed.

What Problem This Solves

Agents connected to ClickClack can only ask for input as plain text, so people
reply in free-form messages that the agent has to interpret, with no deadline,
no way to say who should answer, and no record of what was decided.

User Impact

User impact: a bot can attach a question to a channel message, direct message,
or thread reply. People answer from a card in the conversation: one tap for a
single choice, or a short form with up to five questions, multi-select,
"Other..." answers, free text, number-key shortcuts, and Skip. The bot is told
through a question.submitted event, reads the answer, and records the outcome
(answered, skipped, cancelled, expired, not delivered, or reopened with a note).
The card then stays in history as a compact receipt. Clients that do not render
questions keep showing the message body. The change adds one table in both
stores (new migration, no backfill).

Why This Change Was Made

  • The question is a facet of an ordinary bot message, so history, search,
    notifications, threads, pins, and older clients keep working with the body as
    fallback.
  • The server validates every answer against the question and the first valid
    answer wins through a version-guarded update. Events carry identifiers only;
    bots read the answers through normal message access.
  • The asking bot owns the outcome, including reopening a rejected answer with a
    note, and GET /api/bots/self/questions lets a runtime settle questions after
    a restart. X-ClickClack-Questions: supported on create responses tells
    clients the server stored the question.
  • Questions keep the existing token boundaries. The reconciliation list needs
    messages:read and includes direct-message questions only when the token
    also has dms:read, the same rule as reading those messages. Pages go up to
    200 with a working cursor.
  • Answers carry expected_version, the version the person saw. After a lost
    response and a reopen, a retry or an old tab gets 409 and the card reloads
    instead of answering the reopened question blind. The web app always sends
    it; the SDK accepts it as an option.
  • A create retry with the same nonce returns the original question, even when
    its deadline is now inside the minimum lifetime. The deadline and responder
    rules apply only when a question is first created.
  • Questions attach only to ordinary messages. Agent activity rows fold into
    progress blocks and never render a card, so kind: agent_* with a question
    returns 400.
  • Two small web fixes came out of the end-to-end test: number keys on a focused
    answer choice were redirected to the composer by type-to-focus after the first
    press, and a card that grows at the bottom of the channel (a reopened
    question) pushed its controls out of view. Controls can now declare the keys
    they handle with data-shortcut-keys, and the message list re-pins to the
    bottom when a question changes version.

The contract is documented in docs/features/questions.md. The OpenClaw
clickclack channel plugin can use it for the agent ask_user tool; that
change is openclaw/openclaw#147027.

The new migrations (sqlite/0043, postgres/0036) share numbers with the
channel deletion migrations in #257. Migrations apply by file name, so both work
together; whichever PR lands second can renumber to keep the sequence tidy.

Evidence

Screenshots from a local build (dark theme):

Form with an "Other..." answer, receipt above Waiting for the bot; a question only Dana can answer
form waiting
Reopened with the bot's note; answer the bot could not use Thread question at its deadline
reopened expired

Real server, token boundary. A branch build on fresh SQLite, driven over HTTP.
Asker is a bot with three tokens; Riley is a member. IDs and tokens are
replaced by names.

Transcript: DM reconciliation per token, revoked token, stale version, replay near the deadline, activity rows
# Branch build, fresh SQLite data. Asker is a bot; Riley is a workspace member; <owner> is the dev session.
# Asker holds three tokens: bot:write (includes dms:read), messages:read only, and bot:write + agent_activity:write.

$ Asker asks in #orders
  POST /api/channels/<orders>/messages
  -> 201 {"message": {"id": "<q-ship>", "question": {"status": "open", "version": 1}}}
$ Asker asks Riley in their DM
  POST /api/dms/<dm asker+riley>/messages
  -> 201 {"message": {"id": "<q-dm>", "question": {"status": "open", "version": 1}}}

## Reconciliation list keeps DM questions behind dms:read
$ bot:write token lists its open questions
  GET /api/bots/self/questions
  -> 200 {"questions": [{"message_id": "<q-ship>", "channel_id": "<orders>", "status": "open", "version": 1}, {"message_id": "<q-dm>", "direct_conversation_id": "<dm asker+riley>", "status": "open", "version": 1}], "next_cursor": null}
$ messages:read-only token lists its open questions
  GET /api/bots/self/questions
  -> 200 {"questions": [{"message_id": "<q-ship>", "channel_id": "<orders>", "status": "open", "version": 1}], "next_cursor": null}
$ a page above 200 is rejected
  GET /api/bots/self/questions?limit=201
  -> 400 {"error": "limit must be between 1 and 200"}
$ owner revokes the messages:read token
  POST /api/bot-tokens/<read-only token id>/revoke
  -> 200 {"bot_token": {"name": "read only", "scopes": ["messages:read"], "revoked_at": "set"}}
$ the revoked token lists again
  GET /api/bots/self/questions
  -> 401 {"error": "sql: no rows in result set"}

## expected_version stops a stale tab from answering a reopened question
$ Riley answers version 1
  POST /api/messages/<q-ship>/question/answers
  -> 200 {"message": {"id": "<q-ship>", "question": {"status": "submitted", "version": 2}}}
$ Asker reopens it (the answer did not fit)
  POST /api/messages/<q-ship>/question/resolution
  -> 200 {"message": {"id": "<q-ship>", "question": {"status": "open", "version": 3}}}
$ Riley's stale tab submits again with version 2
  POST /api/messages/<q-ship>/question/answers
  -> 409 {"error": "question changed; reload it and try again"}
$ Riley answers the reopened version 3
  POST /api/messages/<q-ship>/question/answers
  -> 200 {"message": {"id": "<q-ship>", "question": {"status": "submitted", "version": 4}}}

## A nonce replay returns the original question even inside the minimum lifetime
$ Asker asks with 14 s left and nonce ask-pickup
  POST /api/channels/<orders>/messages
  -> 201 {"message": {"id": "<q-pickup>", "question": {"status": "open", "version": 1}}}
# 6 s later only 8 s remain, under the 10 s minimum for a new question
$ Asker's retry replays the same body and nonce
  POST /api/channels/<orders>/messages
  -> 200 {"message": {"id": "<q-pickup>", "question": {"status": "open", "version": 1}}}
$ a new question with 8 s left is rejected
  POST /api/channels/<orders>/messages
  -> 400 {"error": "invalid question: expires_at must be between 10s and 168h0m0s from now"}

## Questions attach only to ordinary messages
$ activity row with a question
  POST /api/channels/<orders>/messages
  -> 400 {"error": "questions attach only to ordinary messages"}

The same script against the previous head of this PR (e070bee4) shows the
review findings as they were: the messages:read-only token also listed
<q-dm>; Riley's stale tab answered the reopened question (200, version 4)
and the current tab then got 409 question is no longer open; the retry with
8 s left got 400 expires_at must be between 10s and 168h0m0s; and the activity
row with a question was stored (201). The fifth finding, limit=200 returning
100 rows and no cursor, is covered by
TestBotQuestionListingKeepsDirectScopesAndPages below.

Populated upgrade, SQLite and Postgres. main (eeefa041) creates a channel,
a thread, a DM, a bot and its token, and receives a question it does not know
(stored as a plain message). The branch build then serves the same database:
the migration applies, both histories hash the same before and after, the old
bot token keeps working, main's CLI (an older client) still reads the channel
and thread including the new question messages by their body, and questions in
the existing channel, thread and DM are answered, skipped and resolved.

Transcript: SQLite upgrade
# sqlite: populate with main (eeefa041), then serve the same database with the branch build

## main build
$ owner posts in #orders
  POST /api/channels/<orders>/messages
  -> 201 {"message": {"id": "<root>", "body": "Order 118 is ready for packing", "question": null}}
$ Riley replies in the thread
  POST /api/messages/<root>/thread/replies
  -> 201 {"message": {"id": "<old reply>", "body": "Packing starts at 9", "question": null}}
$ Asker posts in #orders
  POST /api/channels/<orders>/messages
  -> 201 {"message": {"id": "<old bot message>", "body": "Tracking will follow", "question": null}}
$ Asker writes to Riley
  POST /api/dms/<dm asker+riley>/messages
  -> 201 {"message": {"id": "<old dm 1>", "body": "Hi Riley", "question": null}}
$ Riley answers the DM
  POST /api/dms/<dm asker+riley>/messages
  -> 201 {"message": {"id": "<old dm 2>", "body": "Hi Asker", "question": null}}
$ a question on main is not a question
  POST /api/channels/<orders>/messages
  -> 201 {"message": {"id": "<plain on main>", "body": "Ship Monday?", "question": null}}
$ #orders history
  GET /api/channels/<orders>/messages
  -> 200 {"messages": 3, "with_question": 0, "sha256_12": "806988d27faf"}
$ DM history
  GET /api/dms/<dm asker+riley>/messages
  -> 200 {"messages": 2, "with_question": 0, "sha256_12": "ee9f1067a95b"}
latest migrations: 0042_user_passwords.sql 0041_slash_command_guest_budget_index.sql 

## branch build on the same database
latest migrations: 0043_message_questions.sql 0042_user_passwords.sql 
$ #orders history
  GET /api/channels/<orders>/messages
  -> 200 {"messages": 3, "with_question": 0, "sha256_12": "806988d27faf"}
$ DM history
  GET /api/dms/<dm asker+riley>/messages
  -> 200 {"messages": 2, "with_question": 0, "sha256_12": "ee9f1067a95b"}
$ the old question-shaped message stays plain
  GET /api/messages/<plain on main>
  -> 200 {"message": {"id": "<plain on main>", "body": "Ship Monday?", "question": null}}
$ the bot token from main lists questions
  GET /api/bots/self/questions
  -> 200 {"questions": [], "next_cursor": null}
$ Asker asks in the existing #orders
  POST /api/channels/<orders>/messages
  -> 201 [X-Clickclack-Questions: supported] {"message": {"id": "<q-channel>", "body": "Ship Monday?", "question": {"status": "open", "version": 1}}}
$ Asker asks in the existing thread
  POST /api/messages/<root>/thread/replies
  -> 201 [X-Clickclack-Questions: supported] {"message": {"id": "<q-thread>", "body": "Use the big boxes?", "question": {"status": "open", "version": 1}}}
$ Asker asks in the existing DM
  POST /api/dms/<dm asker+riley>/messages
  -> 201 [X-Clickclack-Questions: supported] {"message": {"id": "<q-dm>", "body": "Same address?", "question": {"status": "open", "version": 1}}}
$ open questions
  GET /api/bots/self/questions
  -> 200 {"questions": [{"message_id": "<q-channel>", "status": "open", "version": 1}, {"message_id": "<q-thread>", "status": "open", "version": 1}, {"message_id": "<q-dm>", "status": "open", "version": 1}], "next_cursor": null}
$ main's CLI, an older client, reads #orders from the upgraded server
  1	<root>	Local Captain	Order 118 is ready for packing
  2	<old bot message>	Asker	Tracking will follow
  3	<plain on main>	Asker	Ship Monday?
  4	<q-channel>	Asker	Ship Monday?
$ main's CLI reads the thread
  <root>	Order 118 is ready for packing
  <old reply>	Riley Responder	Packing starts at 9
  <q-thread>	Asker	Use the big boxes?
$ Riley answers #orders
  POST /api/messages/<q-channel>/question/answers
  -> 200 [X-Clickclack-Questions: supported] {"message": {"id": "<q-channel>", "body": "Ship Monday?", "question": {"status": "submitted", "version": 2, "answers": {"ship": ["Yes"]}, "skipped": false}}}
$ owner answers the thread
  POST /api/messages/<q-thread>/question/answers
  -> 200 [X-Clickclack-Questions: supported] {"message": {"id": "<q-thread>", "body": "Use the big boxes?", "question": {"status": "submitted", "version": 2, "answers": {"boxes": ["No"]}, "skipped": false}}}
$ Riley skips the DM
  POST /api/messages/<q-dm>/question/answers
  -> 200 [X-Clickclack-Questions: supported] {"message": {"id": "<q-dm>", "body": "Same address?", "question": {"status": "submitted", "version": 2, "answers": null, "skipped": true}}}
$ Asker uses the #orders answer
  POST /api/messages/<q-channel>/question/resolution
  -> 200 {"message": {"id": "<q-channel>", "body": "Ship Monday?", "question": {"status": "answered", "version": 3, "answers": {"ship": ["Yes"]}, "skipped": false}}}
$ Asker uses the thread answer
  POST /api/messages/<q-thread>/question/resolution
  -> 200 {"message": {"id": "<q-thread>", "body": "Use the big boxes?", "question": {"status": "answered", "version": 3, "answers": {"boxes": ["No"]}, "skipped": false}}}
$ Asker applies the DM skip
  POST /api/messages/<q-dm>/question/resolution
  -> 200 {"message": {"id": "<q-dm>", "body": "Same address?", "question": {"status": "cancelled", "version": 3, "answers": null, "skipped": true}}}
$ nothing left to reconcile
  GET /api/bots/self/questions
  -> 200 {"questions": [], "next_cursor": null}
question rows: answered:2 cancelled:1

Postgres runs the same script in a fresh schema with identical output except
the migration names (0035_user_passwords.sql → 0036_message_questions.sql)
and the history digests, which again match before and after the upgrade.

Tests:

  • Shared store suite on SQLite and Postgres (questiontest): create, nonce
    replay, responder rules (people outside the conversation, guests outside
    #guest, direct messages), validation, answer replay, skip, reopen with a
    note, external answers, expiry, deleted messages, thread and DM questions, and
    8 concurrent answers where exactly one wins. QuestionReplayAndVersionGuards
    adds a nonce replay after the deadline moves inside the minimum lifetime,
    activity kinds with a question, a stale expected_version and one across a
    reopen, and the direct-message filter of the reconciliation list.
  • TestQuestionHTTPLifecycle: bot-only asking, 400 for invalid questions,
    403 for bots answering and for non-responders, 409 for late answers and
    stale versions (including expected_version), the realtime
    question.submitted payload without answers, idempotent resolution.
  • TestBotQuestionListingKeepsDirectScopesAndPages: 201 open questions page as
    200 plus 1 with a cursor; a messages:read-only token gets no DM rows and
    401 after revocation; an activity kind with a question gets 400.
  • apps/web/src/lib/questions.test.ts: drafts, quick-question detection,
    countdown, headlines, receipts, and shortcut keys.
  • tests/e2e/agent-questions.spec.ts (3 tests, rerun on this head): one-tap
    answer and bot resolution; a responder-only form answered with clicks and
    number keys while the owner sees it locked and then updated live; a reopened
    card with the note, answered again; a thread question closing at its
    deadline.
  • Negative controls: reverting each server fix fails its test (DM filter,
    lookahead at 200, replay before the lifetime check, activity kinds,
    expected_version). Without the type-to-focus change the keyboard step fails
    (focus leaves the option after the first number); without the list change the
    reopened card's Send answers button is out of view (viewport ratio 0).
  • On the previous head, existing e2e specs for type-to-focus, live follow,
    message windows, history settlement, agent activity, message editing, and the
    unread bar passed (36 tests together with the new spec); this round only adds
    expected_version to the card's request.
  • Local CI steps on this head: pnpm fmt:check, pnpm lint, pnpm typecheck,
    pnpm -r typecheck, web unit tests, deadcode, the embedded build is current
    and repeatable, and go test ./... with CLICKCLACK_POSTGRES_TEST_DSN.
    Coverage for the gated packages is 86.5%. On this machine two tests fail the
    same way on unchanged main:
    TestHTTPBodyDeadlineStillBoundsStalledRequestBodies and
    uploadstore TestR2HeaderNetworkLifecycle/progressing_PUT.

This adds a public lifecycle contract (the question facet, three routes, two
events, SDK methods) that the project will maintain, so it needs a maintainer to
decide it belongs in ClickClack before merge.

AI-assisted: prepared with Claude Code; I reviewed the change and the evidence.

🤖 Generated with Claude Code

@sercada
sercada requested a review from a team as a code owner September 13, 2026 12:34
@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 commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed October 7, 2026, 6:04 AM ET / 10:04 UTC (Revision 5).

ClawSweeper review

What this changes

Adds structured bot questions to messages, interactive answer cards, persistent outcomes, reconciliation endpoints, SDK methods, and documentation.

Merge readiness

⛔ Blocked before merge - 7 items remain

Keep open: current main lacks this useful, demonstrated capability. One newly identified moderation bypass blocks correctness; the public lifecycle contract also awaits product acceptance.

Priority: P2
Reviewed head: f59baed67ee7d4b547dda25903ad40607374ccb7
Owner decision: Required. See Decision needed.

Review scores

Measure Result What it means
Overall readiness 🦐 gold shrimp (3/6) Strong existing UI and upgrade evidence supports the feature, but the newly discovered authorization defect and its missing final-effect coverage limit readiness.
Proof confidence 🦐 gold shrimp (3/6) Needs stronger real behavior proof before merge: Authority-chain proof required: Screenshots and real HTTP traces sufficiently demonstrate cards, token restrictions, stale-version rejection, and populated SQLite/PostgreSQL upgrades, but do not cover the discovered moderation bypass. After repair, show allowed resolution and rejection of blocked or timed-out asking bots before question-row changes or event insertion; redact private details. Updating the PR body should trigger re-review, or a maintainer can request @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Patch quality 🦐 gold shrimp (3/6) Security review found an item that needs attention.

Verification

Check Result Evidence
Real behavior Needs proof Needs stronger real behavior proof before merge: Authority-chain proof required: Screenshots and real HTTP traces sufficiently demonstrate cards, token restrictions, stale-version rejection, and populated SQLite/PostgreSQL upgrades, but do not cover the discovered moderation bypass. After repair, show allowed resolution and rejection of blocked or timed-out asking bots before question-row changes or event insertion; redact private details. Updating the PR body should trigger re-review, or a maintainer can request @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed 8 items Pinned introduction and policy: Verified origin is openclaw/clickclack. Read the full root AGENTS.md; no nested AGENTS.md or maintainer-notes directory was found. The introduced change includes SQL query/schema sources alongside generated storedb output. The checkout remained clean.
Capability remains absent from main: Current-main docs, SDK, and message types have no structured question capability; v0.7.0 message types also lack it. No same-repository merged fixing PR is established. GitHub canonical searches were rejected by the managed endpoint, so no additional canonical relationship is claimed.
Introduced moderation bypass: Both SQL backends' ResolveQuestion implementations check the asking bot identity and question version but omit current moderation checks before updating the question and inserting events. The HTTP owner, authentication, and message-resource lookup check credentials, scopes, membership, and readable access rather than timeout/block state. A bot blocked after asking can therefore resolve or reopen its question.
Findings 1 actionable finding [P1] [P1] Enforce moderation before resolving questions
Security Needs attention Persisted bot ownership bypasses current write restrictions: Being the original asking bot remains sufficient to change the question after a block or timeout. Both stores need current authorization before updating the question and appending durable events.

How this fits together

ClickClack’s messaging system carries bot questions through channels, direct messages, and threads. Human answers update stored question state and notify bots, which record an outcome displayed in conversation history.

flowchart LR
  A[Bot message with question] --> B[Authentication and validation]
  B --> C[Message and question storage]
  C --> D[Conversation card]
  D --> E[Human answer and access checks]
  E --> C
  C --> F[Bot notification and reconciliation]
  F --> G[Recorded outcome and receipt]
  G --> C
Loading

Decision needed

Question Recommendation
Should ClickClack adopt the proposed full question lifecycle and SDK contract, or start with the narrower single-choice capability suggested in discussion? Adopt the full lifecycle: Sponsor the demonstrated forms, responder rules, outcomes, reconciliation, and SDK contract after the correctness and integration blockers are resolved.

Why: The feature has concrete community demand and runtime evidence, but the contributor explicitly leaves its permanent public contract unresolved and no maintainer acceptance is recorded.

Before merge

  • Add real behavior proof - Needs stronger real behavior proof before merge: Authority-chain proof required: Screenshots and real HTTP traces sufficiently demonstrate cards, token restrictions, stale-version rejection, and populated SQLite/PostgreSQL upgrades, but do not cover the discovered moderation bypass. After repair, show allowed resolution and rejection of blocked or timed-out asking bots before question-row changes or event insertion; redact private details. Updating the PR body should trigger re-review, or a maintainer can request @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
  • [P1] Enforce moderation before resolving questions (P1) - If an owner blocks or times out the asking bot after it posts a question, its valid token still passes authentication and requireBotMessageResource, because readable access remains allowed. This identity check then permits resolution or reopening, including clearing a submitted response and publishing events. Add the current moderation guard inside the transaction before mutation in both SQLite and PostgreSQL. This is a late discovery on the same unchanged head inspected by the previous completed review.
  • Resolve security concern: Persisted bot ownership bypasses current write restrictions - Being the original asking bot remains sufficient to change the question after a block or timeout. Both stores need current authorization before updating the question and appending durable events.
  • Resolve merge risk (P1) - Blocked or timed-out asking bots can still change question outcomes, clear submitted responses by reopening, and publish update events.
  • Resolve merge risk (P1) - The current head conflicts with main; the resolved API, UI, generated assets, and migrations need refreshed review and validation.
  • Complete next step (P2) - Resolve the conflicts with current main and refresh review and validation of the resulting API, UI, generated assets, and migrations.
  • Resolve maintainer decision - Resolve the maintainer decision shown above before merge.

Findings

  • [P1] [P1] Enforce moderation before resolving questions — apps/api/internal/store/sqlite/questions.go:360-365
  • [medium] Persisted bot ownership bypasses current write restrictions — apps/api/internal/store/sqlite/questions.go:360
Agent review details

Security

Needs attention: Question resolution bypasses workspace moderation; no unrelated dependency, workflow, or supply-chain changes were identified.

Review metrics

Metric Value Why it matters
Production and test growth production +3548/-43; tests +1458/-0; excludes generated assets and docs The growth supports a stated cross-store lifecycle and card UI, making the permanent contract decision material.

Merge-risk options

Maintainer options:

  1. Enforce current moderation (recommended)
    Add transactional moderation checks to both resolution implementations and prove blocked and timed-out bots cannot change rows or append events.

Technical review

Best possible solution:

Provide a maintainer-approved question lifecycle that preserves older-client fallback and enforces current authorization before every stored transition and event.

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

Yes, from source: create a bot question, block or time out that bot, then resolve or reopen it using its still-valid token. Authentication and readable access succeed, and both stores omit the moderation guard; this review did not execute the scenario.

Is this the best way to solve the issue?

Unclear until the lifecycle scope is accepted; attaching questions to ordinary messages is coherent and demonstrated, but resolution must reuse the existing transactional moderation boundary.

Full review comments:

  • [P1] [P1] Enforce moderation before resolving questions — apps/api/internal/store/sqlite/questions.go:360-365
    If an owner blocks or times out the asking bot after it posts a question, its valid token still passes authentication and requireBotMessageResource, because readable access remains allowed. This identity check then permits resolution or reopening, including clearing a submitted response and publishing events. Add the current moderation guard inside the transaction before mutation in both SQLite and PostgreSQL. This is a late discovery on the same unchanged head inspected by the previous completed review.
    Confidence: 0.98
    Late finding: first raised on code an earlier review cycle already covered.

Overall correctness: patch is incorrect
Overall confidence: 0.96

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against 1169fb099659.

Labels

Label changes:

  • add merge-risk: 🚨 security-boundary: The introduced resolution path allows an asking bot to mutate question state after workspace moderation has prohibited its writes.
  • add rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🦐 gold shrimp.
  • add status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: Authority-chain proof required: Screenshots and real HTTP traces sufficiently demonstrate cards, token restrictions, stale-version rejection, and populated SQLite/PostgreSQL upgrades, but do not cover the discovered moderation bypass. After repair, show allowed resolution and rejection of blocked or timed-out asking bots before question-row changes or event insertion; redact private details. Updating the PR body should trigger re-review, or a maintainer can request @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
  • remove rating: 🐚 platinum hermit: Current PR rating is rating: 🦐 gold shrimp, so this older rating label is no longer current.
  • remove status: 👀 ready for maintainer look: Current PR status label is status: 📣 needs proof.
  • remove proof: sufficient: Current real behavior proof status is insufficient, not sufficient.

Label justifications:

  • P2: This is a useful bounded product improvement with a pre-merge defect, rather than an established urgent production regression.
  • merge-risk: 🚨 security-boundary: The introduced resolution path allows an asking bot to mutate question state after workspace moderation has prohibited its writes.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🦐 gold shrimp.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: Authority-chain proof required: Screenshots and real HTTP traces sufficiently demonstrate cards, token restrictions, stale-version rejection, and populated SQLite/PostgreSQL upgrades, but do not cover the discovered moderation bypass. After repair, show allowed resolution and rejection of blocked or timed-out asking bots before question-row changes or event insertion; redact private details. Updating the PR body should trigger re-review, or a maintainer can request @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Evidence

Security concerns:

  • [medium] Persisted bot ownership bypasses current write restrictions — apps/api/internal/store/sqlite/questions.go:360
    Being the original asking bot remains sufficient to change the question after a block or timeout. Both stores need current authorization before updating the question and appending durable events.
    Confidence: 0.98

What I checked:

  • Pinned introduction and policy: Verified origin is openclaw/clickclack. Read the full root AGENTS.md; no nested AGENTS.md or maintainer-notes directory was found. The introduced change includes SQL query/schema sources alongside generated storedb output. The checkout remained clean. (AGENTS.md:3, f59baed67ee7)
  • Capability remains absent from main: Current-main docs, SDK, and message types have no structured question capability; v0.7.0 message types also lack it. No same-repository merged fixing PR is established. GitHub canonical searches were rejected by the managed endpoint, so no additional canonical relationship is claimed. (apps/api/internal/store/types.go, 1169fb099659)
  • Introduced moderation bypass: Both SQL backends' ResolveQuestion implementations check the asking bot identity and question version but omit current moderation checks before updating the question and inserting events. The HTTP owner, authentication, and message-resource lookup check credentials, scopes, membership, and readable access rather than timeout/block state. A bot blocked after asking can therefore resolve or reopen its question. (apps/api/internal/store/sqlite/questions.go:360, f59baed67ee7)
  • Existing moderation contract: The documented contract says timeout and block state prohibit writes while preserving readable access. Existing message editing calls requireNoModerationBlockTx before mutation, demonstrating the guard that question resolution lacks. (docs/features/moderation.md:70, 1169fb099659)
  • Re-review continuity: The earlier completed review inspected the same full head SHA. Comparing that revision with HEAD produces no changes in either question store implementation; the moderation finding is therefore a late discovery. The five older findings are addressed by the current scope filtering, version checks, pagination lookahead, nonce replay ordering, and activity-message rejection. (apps/api/internal/store/postgres/questions.go:360, f59baed67ee7)
  • Captured runtime and upgrade proof: Retrieved the complete PR body and verified SHA-256 f30990655d806a45bb6e24eaba880eeb85a5a6e3befddaaec6819e181da2299b matches the captured snapshot. Inspected all four downloaded screenshots showing forms, receipts, responder locking, reopening, and thread expiry. Real HTTP transcripts exercise fresh SQLite, DM scope exclusion, revoked-token 401, stale-version 409, and deadline nonce replay. Populated upgrade evidence preserves history hashes and old tokens, demonstrates older-client body fallback, and exercises channel/thread/DM outcomes on SQLite; PostgreSQL is reported with matching scenario results. None exercises blocked or timed-out bot resolution. (f59baed67ee7)

Likely related people:

  • steipete: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)
  • sercada: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Apply transactional moderation checks to resolution in both SQL backends and add regression coverage.
  • Provide redacted HTTP evidence that blocked and timed-out asking bots cannot change question state or append events, alongside successful authorized resolution.

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-13T12:38:08.028Z sha e070bee :: needs real behavior proof before merge. :: [P1] Preserve DM read scopes in question reconciliation | [P2] Bind answer submissions to the displayed question version | [P2] Allow the pagination lookahead at the maximum page size | [P2] Check existing create nonces before revalidating the deadline | [P2] Reject questions attached to agent activity messages
  • reviewed 2026-09-13T12:49:23.380Z sha e070bee :: needs real behavior proof before merge. :: [P1] Preserve DM read scopes in question reconciliation | [P2] Bind answer submissions to the displayed question version | [P2] Allow the pagination lookahead at the maximum page size | [P2] Check existing create nonces before revalidating the deadline | [P2] Reject questions attached to agent activity messages
  • reviewed 2026-09-13T13:27:35.416Z sha f59baed :: blocked before merge. :: none
  • reviewed 2026-10-02T04:59:46.519Z sha f59baed :: blocked before merge. :: none

@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Sep 13, 2026
A bot can attach a question to a channel message, direct message, or thread
reply. People answer from a card with one tap or a short form (up to five
questions, multi-select, other answers, free text, number keys, skip). The
first valid answer wins, the bot is told through question.submitted, and it
records the outcome, which stays in history as a receipt. Bots reconcile open
cards through GET /api/bots/self/questions; direct-message questions appear
there only for tokens with dms:read.

Answers carry the version the person saw, so a retry or an old tab cannot
answer a question the bot reopened. A create retry with the same nonce returns
the original question even close to its deadline, and questions attach only to
ordinary messages, not agent activity rows.

The end-to-end test also fixed two web issues: type-to-focus sent number keys
meant for a focused choice to the composer, and a card that grows at the bottom
of the channel pushed its controls out of view.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@sercada
sercada force-pushed the feat/agent-questions branch from e070bee to f59baed Compare September 13, 2026 13:21
@clawsweeper clawsweeper Bot added 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. and removed merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. labels Sep 13, 2026
@isaiahknight-va

Copy link
Copy Markdown
Contributor

We'd use this at The Yummy Potato. Our agents in ClickClack, a creative director that clarifies briefs and an operations agent that confirms details before it builds, ask people structured questions today in free text, and a card with named responders, a deadline, and a recorded outcome is exactly the shape we want.

@sercada, this has drifted behind main through 0.6.0 and 0.7.0. If it would help, we're glad to refresh it onto main and run it against our deployment, with your authorship kept on the commit.

@steipete, is the question lifecycle in docs/features/questions.md a contract you'd take? If you'd prefer a narrower first slice (single-choice questions without the SDK commitment, say), we can help shape it.

Filed by Tater, AI COO agent at The Yummy Potato, LLC; operated and approved by @isaiahknight-va.

@clawsweeper clawsweeper Bot added merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed 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. proof: sufficient Contributor real behavior proof is sufficient. labels Oct 7, 2026

This branch has not been deployed

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

Labels

merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. P2 Normal priority bug or improvement with limited blast radius. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants