Skip to content

docs(adr): ADR-0020 mid-run message queue + ADR-0021 five-field cron pad (#155) - #165

Merged
chinkan merged 2 commits into
mainfrom
docs/adr-midrun-and-cron
Oct 6, 2026
Merged

chinkan merged 2 commits into
mainfrom
docs/adr-midrun-and-cron

Conversation

@chinkan

@chinkan chinkan commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Docs-only PR. It adds two Accepted ADRs and two glossary terms. No code changes.

ADR-0020: Mid-run message queue (steer, never drop)

docs/adr/0020-mid-run-message-queue.md. Decisions locked by the PO on 2026-10-06 (Q4–Q9). No issue has been filed for this yet.

What recon of main @ 318d203 found:

  • The old steer path is broken. drain_and_inject_steer has had no caller since the AgenticLoop extraction (a401063). Queued messages get "Steer queued" and are then never read. After 10 of them, the key stays "queue full" until restart.
  • Per-chat dispatcher serialisation plus an inline process_message await mean Telegram messages never steer. Each one becomes its own turn later.
  • Scheduled runs share the user's session_key and run in parallel with user turns. This causes cancel-token clobbering and loses messages sent during a scheduled run.
  • The queue path drops attachments. Voice and audio are ignored entirely.

Decision summary:

  • A per-key TurnGate (bot+user) allows one turn at a time and holds an in-memory FIFO queue, capped at 10. Crash or restart loses the queue; this is accepted.
  • The Telegram handler spawns the turn so later messages reach the gate.
  • Q4: when the queue is full, the message is refused with a reply. Nothing is dropped silently.
  • Q7: an accepted message gets a 👀 reaction via setMessageReaction in all modes, including fully silent. No ack bubble. If the reaction fails, it is only logged.
  • Messages are injected between loop iterations (never mid-tool). A second drain happens before the final answer, so one reply covers the queued content.
  • Q5: /stop cancels the turn, clears the queue, and reports how many messages were cleared.
  • Q6: after a max-iterations stop, queued messages auto-start one new turn. If that turn also hits max-iterations, the queue is kept, the PO step-limit message (with N) is sent, and the bot waits for the user's next message.
  • Q8: a scheduled run waits behind an active user turn and is never steered. User messages sent during it start the user's own turn afterwards.
  • Q9: media is preprocessed at ingress (download; voice goes through a transcription hook). It is injected through the same attachment pipeline as a fresh turn. Download failure produces a placeholder, never a drop.
  • Portal follows the same rules as slice 2: 202 queued instead of 409 chat_in_progress, amending ADR-0005.

The ADR lists four open questions for the PO: the refusal bubble on fully silent bots, /stop versus scheduled runs waiting behind the turn, the speech-to-text backend, and localisation of the step-limit message.

ADR-0021: Five-field cron, seconds pad (#155)

docs/adr/0021-five-field-cron-seconds-pad.md. Refs #155.

  • A new normalize_cron_expr pads a 5-field cron to 6 fields with second 0 (0 9 * * * becomes 0 0 9 * * *). A 6-field cron is stored unchanged apart from trimming.
  • 4-field, 7-field and @ macros are rejected with a clear invalid_cron message.
  • It is used at every write path: portal POST /api/tasks, PUT /api/tasks/{id}, the schedule_task tool, and the config crons. Only the 6-field form is stored.
  • Restore repairs and rewrites any 5-field row it finds at boot.
  • The tool description, the portal UI heuristic (CRON_6_FIELD) and the en/zh-HK hints are updated to say "5 or 6 fields". API responses echo the stored form.
  • Timezone is restated, not changed: crons are evaluated in UTC (Job::new_async → Utc). "Fires at 09:00" in the AC therefore means 09:00 UTC. This is flagged as an open question.

Glossary

Adds Mid-run queue and Steer to GLOSSARY.md.

ADR-0020: queue-full refusal is a text reply in fully_silent too; /stop keeps
waiting scheduled runs; v1 voice gets an explicit 'not supported' reply, local
STT is a follow-up; user-facing copy in code is English (locked step-limit and
voice strings), earlier Cantonese copy superseded. Open questions replaced by
non-blocking follow-ups.

ADR-0021: UTC evaluation flagged as a separate P1 timezone follow-up (default
system local timezone, config override); #155 proceeds without waiting.
@chinkan

chinkan commented Oct 6, 2026

Copy link
Copy Markdown
Owner Author

TL ACCEPT (docs).

PO Product GO (2026-10-06) is baked into both ADRs at 4996091. Five locks:

  1. ADR-0020 Q4: the queue-full refusal is a text reply in every mode, including fully_silent. Reaction-only refusal is rejected.
  2. ADR-0020 /stop: unchanged. It cancels user-initiated work and clears the user queue, and does not skip a waiting scheduled run.
  3. ADR-0020 Q9 voice: v1 keeps the placeholder and sends an explicit "Voice is not supported yet — please type instead." reply. Voice is never treated as understood. Local STT (e.g. whisper.cpp) is a follow-up requirement.
  4. ADR-0020 copy: user-facing strings in code are English. Locked Q6 step-limit and voice strings. Per-bot language / i18n is backlog. The earlier Cantonese Q6 copy is superseded.
  5. ADR-0021 timezone: UTC evaluation is a separate P1 follow-up (default = system local timezone, config override). Accept 5-field cron by padding seconds as 0 #155 proceeds without waiting.

No blocking open questions remain; both ADRs stay Accepted.

@chinkan
chinkan merged commit e4f70ad into main Oct 6, 2026
13 of 14 checks passed
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