feat(telegram): trigger options and Jev client for OpenRouter systemone

This commit is contained in:
EugeneTes
2026-10-05 16:15:06 +00:00
parent 7520ff56a2
commit af714cc50b
6 changed files with 502 additions and 1 deletions

View File

@@ -0,0 +1,111 @@
# Telegram groups and topic triggers Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** The Telegram bot watches groups silently and, through per-owner topic triggers classified by Jev (OpenRouter
`/systemone`), prompts the owner's private session when a group message is relevant; triggers can be snoozed.
**Architecture:** Everything in `hosts/Volcanic.AgenticCore.Telegram`: a `Groups/` slice (known groups, the group
message feed) and a `Triggers/` slice (options, Jev client, store, snooze queue, engine, debounce dispatcher, delivery,
tool, `/triggers`). The router splits group messages off before the allow-list. Spec:
`docs/superpowers/specs/2026-10-05-telegram-group-triggers-design.md`.
**Tech Stack:** .NET 10, Telegram.Bot 22.10, xunit 2, `FakeTimeProvider`, the host test harness (`ChatHost`,
`FakeBotClient`, `FauxProvider`).
Conventions for every task: `TreatWarningsAsErrors` is on for the host (use `ConfigureAwait(false)`, culture-invariant
formatting, `ArgumentNullException.ThrowIfNull` on public entry points). Run tests with
`dotnet test tests/Volcanic.AgenticCore.Telegram.Tests --filter "FullyQualifiedName~<Class>"`. Commit after each task
with a Conventional Commit scoped `telegram`.
---
## File structure
| File | Responsibility |
|---|---|
| `Triggers/TriggerOptions.cs` | `Telegram:Triggers` settings, env, validation, `Describe()` |
| `Triggers/JevClient.cs` | `IJevClient`, `JevQuestion`, `JevException`, HTTP client with retries |
| `Groups/GroupMessage.cs` | `GroupMessage`, `GroupReply` records, `From(Message)` builder, link |
| `Groups/GroupRegistry.cs` | `KnownGroup`, `groups.json` persistence, vouch / inactive / migrate |
| `Triggers/TriggerStore.cs` | `Trigger`, `WatchingTrigger`, per-owner `triggers.json`, group index |
| `Triggers/SnoozeQueue.cs` | per-trigger `snoozed/<id>.jsonl`: append (capped), take-all, count, delete |
| `Triggers/JevStates.cs` | Jev `state` JSON for one message / a batch, chunking, batch question wrapping |
| `Triggers/TriggerPrompts.cs` | owner prompts (live, snooze), trigger list lines, durations |
| `Triggers/TriggerDispatcher.cs` | 10 s fixed-window buffering per (owner, trigger) |
| `Triggers/ITriggerDelivery.cs` + `TelegramTriggerDelivery.cs` | follow-up prompt into the owner's session |
| `Triggers/TriggerEngine.cs` | classify + log each message, snooze queueing, snooze timer and flush |
| `Groups/GroupFeed.cs` | router entry for group messages and `my_chat_member`; concurrency gate |
| `Groups/GroupMembership.cs` | `IGroupMembership` (`getChatMember`) |
| `Triggers/TriggerTool.cs` + `TriggerExtension.cs` | the `trigger` tool per private chat |
| `Triggers/TriggerServices.cs` | the bundle the registry, commands and tool take |
| `Triggers/TriggersSetup.cs` | DI registration, menu entry, help line, hosted service |
| modified | `TelegramOptions`, `TelegramProcessEnvironment`, `PollingService`, `UpdateRouter`, `ChatRegistry`, `ChatCommands`, `BotSetupService`, `Program.cs` |
| tests | `TriggerOptionsTests`, `JevClientTests`, `GroupRegistryTests`, `TriggerStoreTests`, `TriggerEngineTests`, `TriggerToolTests`, `GroupChatTests`; `FakeBotClient` answers `getChatMember`; `ChatHost` takes `triggers:` |
## Tasks
### Task 1: Options and secrets
- `TriggerOptions` (defaults from the spec table; `internal TimeSpan RetryDelay = 1 s`, `MaxRetryAfter = 30 s`,
`ChunkChars = 40_000`, `DigestChars = 30_000`), `IsEnabled`, `ResolvedModel`, `Describe()`, `Validate(List<string>)`.
- `TelegramOptions.Triggers`; `ApplyEnvironment` reads `OPENROUTER_API_KEY`, `TRIGGER_MODEL`; `Validate` calls it.
- `TelegramProcessEnvironment`: the key goes to `SecretValues`.
- Tests: env fills only empty settings; validation errors for bad URL / threshold / non-positive values; secret added.
### Task 2: Jev client
- `IJevClient.AskAsync(JsonNode state, IReadOnlyList<JevQuestion>, ct) → IReadOnlyDictionary<string,double>`.
- POST `{BaseUrl}/systemone`, body `{state, model, questions:{id:{type:"noul", instructions, criteria?}}}`; criteria only
when both set. 1 + `MaxRetries` attempts on network error, timeout, 408, 429, 5xx; backoff `RetryDelay·2^(n-1)`;
`Retry-After` honoured up to `MaxRetryAfter`. `JevException.Attempts`. Missing answer = failure. Key redacted.
- Tests with a scripted `HttpMessageHandler`: request shape, parsing, 3 retries then failure (4 calls), 400 not retried,
missing answer, key never in the message.
### Task 3: Group registry and messages
- `GroupMessage.From(Message, DateTimeOffset)`: sender `First Last (@user)` or `SenderChat.Title`; text or caption;
reply sender/text; `Link` (supergroup `t.me/c/<id-without--100>/<mid>`, username `t.me/<user>/<mid>`, else null).
- `GroupRegistry` (`<agentDir>/telegram/groups.json`): `Find`, `All`, `VouchAsync(chat, by)`, `RefreshAsync(chat)`,
`SetInactiveAsync(id)`, `MigrateAsync(from, to)`; corrupt file set aside like `ChatSettingsStore`.
- Tests: persistence round trip, vouch reactivates, refresh title, migrate, links.
### Task 4: Trigger store and snooze queue
- `Trigger` record (spec fields), `TriggerStore(workspaceRoot, maxPerChat, logger, time)`: loads every
`<root>/<chatId>/.volcanic/triggers/triggers.json`; `List(owner)`, `Find(owner,id)`, `Watching(groupId)`,
`AddAsync`, `UpdateAsync(owner,id,Func<Trigger,Trigger>)`, `RemoveAsync`, `MoveGroupAsync`, `NextSnoozeEnd()`,
`SnoozeDue(now)`, `WorkspaceOf(owner)`. Ids `t<max+1>`.
- `SnoozeQueue(maxPerTrigger)`: `AppendAsync → (count, dropped)`, `TakeAllAsync`, `Count`, `Delete`.
- Tests: ids, limits, reload from disk, index after move/remove, queue cap drops oldest, take-all empties.
### Task 5: Jev states, prompts, dispatcher, delivery
- `JevStates.Single`, `JevStates.Batches(group, messages, zone, maxChars)`, `JevStates.Question(id, trigger, batch)`.
- `TriggerPrompts.Live`, `TriggerPrompts.Snoozed`, `TriggerPrompts.Line`, `TriggerPrompts.TryParseDuration`,
`TriggerPrompts.TryParseUntil`.
- `TriggerDispatcher.Add(owner, trigger, message, score)`; window opens on first hit, `Debounce` later one prompt.
- `ITriggerDelivery { DeliverAsync(owner, prompt, ct); TimeZoneFor(owner) }`, `TelegramTriggerDelivery` (header +
`FollowUp`, errors reported in the chat).
- Tests: prompt text, chunking, durations, window not reset by later hits.
### Task 6: Engine
- `TriggerEngine.ProcessAsync(GroupMessage)`: the log line per message; snoozed triggers queue; one Jev call with
question ids `q0..qn` mapped back to (owner, trigger); matches to the dispatcher.
- Snooze: `SnoozeAsync(owner, id?, until)`, `UnsnoozeAsync(owner, id?)`, timer on `TimeProvider` (≤ 1 h), `StartAsync`
flushes overdue, `FlushAsync(owner, trigger)` (take-all, chunks, wrapped question, best score, digest file).
- Tests with a fake Jev and fake delivery: match/no match, two owners same group, failure logged, snooze queue then
expiry → one prompt, no match → silence, unsnooze, overdue at start, digest file, disabled dropped.
### Task 7: Feed, router, polling, membership
- `GroupFeed.OnMessageAsync` / `OnMyChatMemberAsync`; `UpdateRouter` routes group messages and `MyChatMember` first
(feature off: debug log, drop). `PollingService.AllowedUpdates` += `MyChatMember`.
- `IGroupMembership` via `bot.GetChatMember`. `FakeBotClient` answers `GetChatMemberRequest` (non-members set).
- Tests (`GroupChatTests`): stranger's group message is classified, nothing sent to the group, unknown group ignored,
allowed poster vouches, `my_chat_member` vouch/leave, migration, feature off.
### Task 8: Tool, extension, commands, wiring
- `TriggerTool` actions groups/create/list/update/delete/snooze/unsnooze; group resolution; membership and limits.
- `TriggerExtension` registered by `ChatRegistry` when `TriggerServices` present; `/triggers` in `ChatCommands`, menu
and help; `BotSetupService` privacy warning; `Program.cs` registration and startup log line; hosted service starts
the engine.
- Tests: tool actions, end-to-end in `ChatHost` (group message → owner session prompt through the faux provider).
### Task 9: Docs
- `docs/telegram-bot.md` "Groups and triggers", `docs/docker.md` env vars, `CLAUDE.md` host summary, spec status.
- Full `dotnet build` and `dotnet test`.