8000 feat: adapt ask_user interactions across channels by qqqqqf-q · Pull Request #817 · felinics/Memoh · GitHub
[go: up one dir, main page]

Skip to content

feat: adapt ask_user interactions across channels - #817

Merged
qqqqqf-q merged 6 commits into
felinics:mainfrom
qqqqqf-q:codex/ask-user-channel-interactions
Jul 16, 2026
Merged

qqqqqf-q merged 6 commits into
felinics:mainfrom
qqqqqf-q:codex/ask-user-channel-interactions

Conversation

@qqqqqf-q
@qqqqqf-q qqqqqf-q commented Jul 16, 2026 •
Copy link
Copy Markdown
Member

Summary

ask_user was only usable on the web UI; on IM channels it degraded to a /respond slash-command hint or nothing at all. This PR makes ask_user a first-class interaction on all 13 channels, backed by one durable state machine with two input surfaces:

  • Durable interaction state — user_input_requests.interaction_json + interaction_revision (optimistic-lock CAS). All input surfaces write the same TextInteractionState; a pending ask_user survives server restarts and cannot desync across surfaces.
  • Telegram native buttons — paged questions, single-select auto-advance, multi-select ✓ toggle, "Other…"/free text via force-reply, back with answer preservation, skip, final summary. Callbacks carry the request UUID and apply structured ops (select/toggle/set_text/navigate/submit) via userinput.AdvanceInteraction; the card re-renders from persisted state.
  • Plain-text fallback — for the 12 channels without inline buttons: numbered options, label/ID matching, comma-separated multi-select, back/skip commands (en/zh/ja), per-question prompts, and a submitted summary. Gated by a new NativeUserInput channel capability.
  • DingTalk/WeCom group fix — both platforms only deliver @-mentioned/direct messages, so inbounds are marked is_mentioned; without this, group replies to ask_user were silently dropped.
  • Continuation repair — orphaned tool calls from interrupted turns are closed before the transcript reaches providers that enforce strict assistant-tool adjacency.
  • ask_user UI chrome localized through command_ui_language.

Migration

0111_user_input_text_interaction (renumbered from 0108 after rebasing onto main, which took 0108–0110): adds interaction_json JSONB / interaction_revision INTEGER with constant defaults — PG11+ fast path, no table rewrite. Idempotent, paired down migration, 0001_init.up.sql updated.

Design notes

  • One state machine, two surfaces: buttons and plain-text replies both CAS into the same row, so answering can even move between surfaces mid-request.
  • AdvanceText and AdvanceInteraction share a single persistInteraction CAS write path.
  • The only process-local state left on Telegram is the force-reply prompt binding; losing it on restart means the user re-taps one button — no answers are lost (documented in code).
  • Completed-but-unsubmitted state (crash window between CAS and submit) is inert: any interaction re-drives submission instead of mutating a complete answer set (pinned by test).

Known limitations / follow-ups

  1. Legacy respond: parsing is kept for upgrade compatibility only. The shared layer now emits a single marker action (filter keepalive) instead of fabricated per-option respond: keyboards †8000 ” the structured InteractionOp path is the only real button protocol. The Telegram-side respond: parser stays for one compat window because cards rendered by a pre-deploy server never expire; it can be deleted in a follow-up once that window passes.
  2. Future native-button channels (Discord/Feishu cards) only need a render layer — the InteractionOp state machine is channel-agnostic; render from the payload and encode structured ops as Telegram does.

Validation

  • live Telegram and WeChat end-to-end testing (including migration path on a live dev DB)
  • go test ./internal/userinput ./internal/channel/... ./internal/conversation/... — green
  • golangci-lint on all touched packages — clean
  • sqlc regenerated (mise run sqlc-generate), zero drift

@qqqqqf-q
qqqqqf-q requested review from a team July 16, 2026 08:15
@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 37f75d7399

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

w.MessageID = cb.Message.ID
}

ready, needText, toast := applyAskUserCallback(w, parsed)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind Telegram wizard callbacks to the requesting user

In group chats, any member can press these inline buttons because the wizard contains no sender identity and this path mutates/submits it solely from the callback token. A different member can therefore change the original user's selections; on a final single-select or Submit tap, their synthetic /respond can resolve the request when they have no active session (the explicit-ID resolution path permits an empty session), submitting their answers into the original user's waiting run. Even when their session causes resolution to fail, submitAskUserWizard has already deleted the shared wizard, preventing the requester from completing it.

Useful? React with 👍 / 👎.

Comment on lines +690 to +693
w := a.askUserStore().get(parsed.Token)
if w == nil {
_ = bot.Respond(cb, &tele.CallbackResponse{Text: i18n.New("").T("cmd.userInput.expired"), ShowAlert: true})
return true
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist Telegram wizard state across restarts

After a server restart or adapter reconnect, every outstanding Telegram ask_user card becomes unusable: its callback token only indexes this process-local map, so the newly created adapter returns expired at the w == nil branch. Although the pending request is durable in PostgreSQL, this native path neither restores a wizard from it nor exposes a usable fallback response identifier, leaving the agent run waiting until expiry.

Useful? React with 👍 / 👎.

qqqqqf-q added 4 commits July 16, 2026 01:52
DingTalk and WeCom smart-robot webhooks only deliver a group message when
the bot is @-mentioned, so every inbound is already bot-directed. Neither
adapter set is_mentioned, so isDirectedAtBot returned false in groups and
handlePlainTextUserInput silently dropped the answer — group ask_user hung
forever. Mark user messages (not event callbacks) as mentioned. Guarded by
a directed-group fallback regression test.
…on state

Remove the process-local ask_user wizard. Telegram inline-button callbacks
now carry the request UUID and apply structured ops (select/toggle/set_text/
navigate/submit) to the persistent TextInteractionState via a new
userinput.AdvanceInteraction — the same CAS-guarded state machine the
plain-text fallback uses. The card is re-rendered from the persisted state.

Consequences:
- A pending ask_user survives server restarts: state lives in
  user_input_requests.interaction_json, and the card binds via
  prompt_external_message_id (persisted on send through
  UpdatePromptMessage).
- Buttons and plain-text replies can no longer desync: one durable state,
  two input surfaces, optimistic-lock CAS on every write.
- Callback acks are silent (no more 'OK' toast) and same-page taps are
  reported unchanged, so the page indicator never triggers a no-op edit.
- The only process-local remainder is the force-reply prompt binding;
  losing it just means re-tapping the fill-answer button.

AdvanceText now shares persistInteraction with AdvanceInteraction so there
is exactly one CAS write path.
@qqqqqf-q
qqqqqf-q force-pushed the codex/ask-user-channel-interactions branch from 37f75d7 to a843b5e Compare July 16, 2026 10:41
qqqqqf-q added 2 commits July 16, 2026 03:49
… only

The respond:<id>[:<option>] callback namespace cannot be deleted yet: the
shared channel layer (inbound/channel.go buildUserInputActions,
pipeline/driver.go) still emits these buttons for every adapter, and
chat-flow ask_user requests never expire — a card rendered before this
deploy stays tappable indefinitely.

What this commit does remove: the bare respond:<id> dispatch path. It only
produced a '/respond <id>' hint command with no answer attached; the
callback handler already acks it as a silent no-op, so building an inbound
message for it was dead weight. buildTelegramCallbackInboundMessage now
rejects answer-less respond callbacks outright.

Full deletion of the namespace is deferred until the shared action builder
emits aui~ ops directly (cross-adapter change, separate PR).
…ck/misskey

Pay down the respond: debt at its source: the shared stream mapper no
longer fabricates per-option respond: keyboards. It now emits exactly one
marker action (Type=user_input) whose only jobs are (a) keeping the
show_tool_calls_in_im filter from dropping the pending card and (b) legacy
upgrade compat. No adapter other than Telegram ever rendered these buttons,
and Telegram replac
A289
es them with the aui structured keyboard — the fabricated
options were dead weight that could only mislead future adapters into
depending on the legacy encoding. New native-button channels should render
from the payload and encode their own structured ops, as Telegram does.

Also close the remaining plain-text fallback directedness gaps:
- slack: flag is_reply_to_bot when the thread root author is the bot
  (ParentUserId == selfUserID), so thread replies to the bot's ask_user
  prompt count as bot-directed in channels.
- misskey: flag is_reply_to_bot when the replied-to note is the bot's own,
  so answers without an explicit @-mention are not dropped.
QQ needs no change: the platform only delivers @-messages and the adapter
already marks them is_mentioned.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0