Repository navigation
fix(ai,sharing): long ask_user options reach the user, a write-secretary invites for the owner's own events, a partly failed turn ends with a readable receipt (Refs #908) - #940
Merged
Conversation
This was referenced Nov 2, 2026
…ed tap reaches the AI once (Refs #908, #914) An option longer than Telegram's 64-byte callback_data made the whole question undeliverable. Such options are now sent as ai_opt index buttons that the tap handler maps back to the exact option text; shorter ones keep the inline form. The tap handler claims the answered message before the AI continues, releases the claim when the continuation cannot start and treats a failed ctx.answer as non-fatal, so racing or repeated taps reach the AI once. The answer is shown as one bounded line. The earlier option-length and keyboard-size caps are removed; the 100-button cap stays and an empty option is still rejected. docs/reference/telegram-limits.md and the tests record the limits.
…osters for the owner's own personal events (Refs #909) send_invitation, resend_invitation, cancel_invitation and get_invitation_status accept owner_id, so a secretary whose access covers a calendar can address an event in the owner's calendar. The delegated paths look the event up with an owner-only personal-event lookup, so a group event or an event another person organises is an explicit scope denial, and write access is checked again after the recipient lookup. Recipient identity is unchanged: invented ids are still refused. A foreign invitation id now answers exactly as a missing one, and every denial carries mutationState not_applied. The system prompt, tool schemas and sharing spec describe owner_id.
…s and was not written instead of the numbered attempt ledger (Refs #526) When one write of a turn failed, the whole reply became numbered "attempt N" lines with no event titles. The reply is now built from the write ledger: each event or invitation is named with its title and local time, a failed write is shown once as not done, a later success replaces the earlier failures of the same requested event, and an unknown outcome is reported as unconfirmed. A delegated calendar is named by its owner when readable and neutrally otherwise, group chats still hide targets, names are neutralised against markup, and a hostile start_at cannot make the receipt throw. Recurring creates are told apart through an RRULE normaliser. Messages exist in ru and en.
…record the secretary-invitation incident plan (Refs #908) AGENTS.md gains a section that makes the per-chat AI debug log, the Docker log and the host logs the first step of any problem analysis: copy the log to a private temporary file outside the checkout, read ranges rather than the whole file, treat FINAL as the intended reply rather than proof of delivery, and keep raw logs, ids and chat text out of the repository. The deploy runbook diagnostics use the same procedure with a <chatId> placeholder. The plan file for the incident (a secretary asking the bot to create an event in the delegating owner's calendar and invite three people) lists the fixed items, the review findings fixed before commit and the deferred table #910-#920.
…n the secretary, receipt and recipient-approval tests with real types (Refs #908)
…ool catalog budget and the per-chat turn gate deps (Refs #908) After rebasing onto the per-chat serialization change, the ask_user logging tests lacked the new reserveChatTurn dependency, and the four invitation tools' owner_id parameter pushed the tool catalog 151 characters over its 36000 budget. The shared owner_id description is shortened (it appears in 19 tools) and still says it is the calendar owner's ID for a secretary.
alex-mextner
force-pushed
the
fix/secretary-invites-askuser-20261031
branch
from
November 4, 2026 04:52
b711de1 to
ab104e5
Compare
3 tasks
This was referenced Nov 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #908 #909 #526 #914
A write-secretary asked the bot to create an event in the delegating owner's calendar and invite three people. Three things went wrong in that one conversation: the question the bot asked never reached the user, the bot could not invite for the owner's event and invented Telegram ids for the named invitees, and after a partly failed run the reply was a numbered "attempt" list with no event names. This PR fixes the three production paths. The incident is described without any private data in
docs/plans/2026-10-31-secretary-invitation-incident.md.What changed and why
ask_user options of any length are delivered (#908, #914). Telegram refuses
callback_dataover 64 bytes, and a 45-character Russian option is 87 bytes inai_btn:<text>, so the whole question failed to send (BUTTON_DATA_INVALID) and the model got an error it could not act on. Options that do not fit now go out asai_opt:<index>buttons and the tap handler maps the index back to the exact option text; short options keep the inline form. The answer is shown as one bounded line. Two taps, a failed acknowledgement edit or a failedctx.answer()reach the AI once: the answered message is claimed in one bounded process-wide set, released when the continuation cannot start. The earlier option-length and keyboard-size caps were guesses and are removed; an empty question or option and more than 100 buttons are still rejected.A write-secretary can invite, resend, cancel and read the roster for the owner's own personal events (#909).
send_invitation,resend_invitation,cancel_invitationandget_invitation_statusacceptowner_id. The delegated paths use an owner-only personal-event lookup, so a group event or an event somebody else organises is an explicit scope denial, and write access is checked again after the recipient lookup (a revoked secretary can no longer send). Recipient identity is unchanged: invented ids are still refused; only the requester's own id is now accepted as verified and the rejection hints for a delegated calendar no longer send the model to the picker. A foreign invitation id answers exactly like a missing one (no existence oracle), and every denial carriesmutationState: not_applied, so the receipt does not ask the user to check the calendar after a denied write. Behaviour change: a secretary can no longer act on group-owned events, only on the owner's personal events.A partly failed turn ends with a readable receipt (#526). The turn-finish guard used to replace the model's text with the numbered attempt ledger. The reply is now built from the write ledger: each event or invitation is named with its title and local time, a failed write is shown once as not done, a later success of the same requested event replaces the earlier failures, and an unknown outcome is reported as unconfirmed, never as delivered. A delegated calendar is named by its owner when readable and neutrally otherwise, group chats still hide targets, markup in names is neutralised and a hostile
start_atcannot make the receipt throw. Recurring creates are told apart through an RRULE normaliser. Messages exist in ru and en.Debugging procedure. AGENTS.md and the deploy runbook now make the per-chat AI debug log and the Docker log the first step of any problem analysis (private temporary copy, ranges rather than the whole file, FINAL is the intended reply and not proof of delivery, no raw ids or chat text in the repository).
Release.
pyproject.tomlanduv.lockgo from 0.3.1 to 0.3.2.Verification
review diff --stagedrounds before each commit (recorded under the task codes of ask_user fails with BUTTON_DATA_INVALID for options over 64 bytes of callback_data #908, A write-secretary cannot send invitations for an event in the owner's calendar; the model invented Telegram ids for named invitees #909 and Write receipts after a partly failed run list 'Попытка N' lines without event names and keep failures a later attempt already fixed #526); the review findings fixed before commit are listed in the plan file.test_local_fallback_staging.py,test_odroid_stage_qa_installer.py,test_stage_qa_browser_installer.pyandtest_odroid_stage_qa_teardown.pypass when run withpython3 -m unittest.test_local_fallback_staging.pyfails underbun testonly because bun's temporary directory name contains a dot; it was not changed for that.reserveChatTurndependency, and theowner_idparameter of the invitation tools had pushed the tool catalog 151 characters over its 36000-character budget, so the sharedowner_iddescription is shortened (it appears in 19 tools).bun install --frozen-lockfile0,bun run lint0,tsc --noEmit0,bun test: 9187 pass, 16 skip, 6 fail across 535 files, none of them in this change. Two Python installer suites cannot run there because the server has no systemgit, andtest_odroid_stage_qa_teardown.pyhits the known intermittent bash 5.3 crash. Three Chromium tests (playwright-pool,intent-catalogue,ssg-browser) failed because that run lacked the Chromium library environment; the same three files pass (4 pass, 0 fail) withLD_LIBRARY_PATHandFONTCONFIG_FILEset. So the server run is not fully green and hosted PR CI is the check of the exact head.Not covered
test/services/ai/tool-handlers/ask-user.test.ts,test/bot/handlers/ai-button-answers.test.ts,test/services/ai/tool-handlers/sharing-secretary-invitation*.test.ts,test/services/ai/write-receipt.test.ts,test/services/ai/write-outcomes.test.ts. The conversation-level acceptance (a secretary invites a named person; a long-option question is answered once) is a separate, owner-authorised live test after the deploy.Deferred findings
Tracked, not part of this change: #910 reason class of a rejected invitation, #911 stale not-deleted line after a retried delete or update, #912 chat-logging stores an ask_user answer as a turn before the handler decides, #913 unreadable-keyboard alert wording, #915 roster read for secretaries in a group, #916 receipt wording for an unreadable owner name, #917 tap handler claim ordering, #918 cancel and resend denials carry no mutation state, #919 test fixtures with production-looking values, #920 write ledger edge cases, #921 all-day events west of UTC, #922 leftover scope gaps of the secretary tools, #923 taps ignored when the question ends in a checkmark line. #526, #914 and #920 to #923 stay open after this merge.