59 Commits

Author SHA1 Message Date
EugeneTes
f42b00cb1c fix(a2a-client): watches no longer hang on a silently dead task stream
Hour-long remote tasks were never delivered: a subscribe watch held one SSE stream for up to a day, the remote
sent nothing while its agent worked without text, and once the quiet connection died on the way (no FIN/RST) the
watch waited on it with no error and no log line.

- A subscription lasts at most 5 minutes (SubscriptionSlice), then GetTask and a new subscription: a pause, not a
  failure. A slice ended mid-read counts as ours even when it surfaces as an I/O error.
- The shared handler connects with TCP keepalive (60 s, every 15 s, 4 probes) and HTTP/2 pings.
- Log lines when a watch starts or is restored, when a slice ends on a running task, when a result is delivered or
  kept; the Telegram host's console log carries a UTC timestamp.
- action=status on a settled task ends its watch, so the result isn't delivered a second time.
- A "context busy" refusal of send says nothing was sent, names the watched task in that context and that its
  result will arrive by itself; refusals never touch a watch.
2026-10-16 13:48:32 +00:00
EugeneTes
644f72068c feat(skills): session-start and turn-end hooks and protected paths declared in SKILL.md
A skill's SKILL.md frontmatter may declare session-start (output into
<skill_session_context>, at session start, /new, resume and after compaction),
turn-end (after every run, in the background; output or a failure reaches the
next run as a hidden notice, repeats suppressed until a clean run) and
protected-paths (write/edit refused inside them). Hooks run from the skill
directory with VOLCANIC_WORKSPACE, VOLCANIC_SKILL_DIR and the chat's TZ; waits
are bounded by each hook's timeout and failures never break a run. /skills
shows declared hooks and broken fields.
2026-10-16 12:27:44 +00:00
EugeneTes
d51438e665 feat(telegram): several admins via ADMIN_USER_IDS, each impersonating independently
ADMIN_USER_IDS (Telegram:AdminUserIds) takes a comma separated list; the older
ADMIN_USER_ID / Telegram:AdminUserId still works, also takes a list and is merged.
Empty entries and duplicates are ignored, non-numeric entries fail validation.
Impersonation is now per admin: one admin per target, an impersonating admin
can't be impersonated, and every admin's chat menu lists /impersonate.
2026-10-15 14:02:59 +00:00
EugeneTes
7fef3ce94e feat(telegram): ElevenLabs text to speech for the voice tool
ElevenLabs speaks for the voice tool when ELEVENLABS_API_KEY and ELEVENLABS_VOICE_ID
are set (TTS_PROVIDER=elevenlabs|openai picks, the other is the fallback). Opus output
(opus_48000_64) goes to Telegram as is; voice names resolve through /v2/voices;
instructions become audio tags on eleven_v3 and are ignored on other models. The key
is redacted from tool output and never reaches agent processes.
2026-10-14 13:07:36 +00:00
EugeneTes
6e3241fed9 feat(scheduler): scheduled jobs inside skills (.volcanic/skills/<skill>/jobs/<job>.md)
Skill directories may now declare prompt or watcher jobs as files (cron or one-off at).
The file is the source of truth: the scheduler re-reads every chat's jobs/ every minute,
around agent runs and before /jobs and /skills, and schedules, re-plans or removes them.
Ids are <skill>/<job>; run state lives in .volcanic/scheduler/skill-jobs-state.json, never
in jobs.json. Watchers run from the skill directory with VOLCANIC_WORKSPACE and
VOLCANIC_SKILL_DIR. Broken files are shown with the reason in /jobs, /skills, schedule list
and the new <skill_jobs> prompt section; cancel answers with the file to edit. Every part of
a skill directory is optional; file reading and frontmatter parsing move to Abstractions.
2026-10-14 12:54:40 +00:00
EugeneTes
0cae6f600a feat(telegram): pass GOOGLE_CLIENT_SECRET to agent processes, still redacted
GOOGLE_CLIENT_SECRET is no longer denied by name in the core defaults (the
*_SECRET glob still removes it); the Telegram host allows it by default so
workspace skills (gmail, gdrive) can use the Google OAuth client. Its value
stays in SecretValues: redacted from tool output, and aliases with the same
value are still removed.
2026-10-12 15:10:49 +00:00
EugeneTes
bb1c534a7a feat(core): always-on INSTRUCTIONS.md in skill directories
A skill directory may hold INSTRUCTIONS.md, with or without SKILL.md. Its body
(frontmatter cut) goes into every system prompt in <skill_instructions>, re-read
on every run: 16 KB per file, 64 KB in total, truncation marked with the file's
path. /skills marks skills with instructions and lists instruction-only dirs.
2026-10-12 10:15:05 +00:00
EugeneTes
c3095c69ca feat(core): per-workspace Agent Skills listed in the system prompt, /skills in Telegram
Skills live in <cwd>/.claude/skills/<name>/SKILL.md (frontmatter name + description).
SkillCatalog re-reads them on every run and the prompt lists name, description and
path; the model reads the full SKILL.md with the read tool when a task matches.
2026-10-10 14:13:42 +00:00
EugeneTes
891a644590 feat(mcp): remote MCP servers per chat with sealed keys and owner-only commands
Streamable HTTP/SSE servers from the workspace's .volcanic/mcp/servers.json.
Headers are sealed with AES-GCM; the chat and the visible fields are the
associated data, so only /mcp_add|remove|tools change servers. Tools
<server>__<tool> go live in the session. An unreachable server is skipped
and reported in the system prompt. Keys are redacted from tool results, and
Telegram deletes messages that carry one.
2026-10-09 21:11:01 +00:00
EugeneTes
87240238cc feat(telegram): voice tool sends spoken replies as Telegram voice notes
OpenAI gpt-4o-mini-tts (female voice nova by default; TTS_MODEL/TTS_VOICE) renders OGG/Opus,
sent with sendVoice in order with rendered output. Up to 4500 characters per call, split
into 1500-character parts at paragraph/sentence/word boundaries; all parts are synthesized
before any is sent, and API errors come back to the agent as text with the key hidden.
On whenever OPENAI_API_KEY is set.
2026-10-09 10:09:46 +00:00
EugeneTes
923a6e04ac docs(a2a): client exposures, URLs without queries, README/architecture 2026-10-08 14:42:18 +00:00
EugeneTes
8eea09baba fix(a2a-client): delivery set before polling, flushes respect stop, restore re-arms first 2026-10-08 14:28:31 +00:00
EugeneTes
4a20ff4635 feat(telegram): A2A client per chat behind A2AClient__Enabled, watch results as follow-ups, /a2a lists agents 2026-10-08 14:21:34 +00:00
EugeneTes
b53c339bf3 chore(docker): aspnet runtime image, port 8080 and A2A settings 2026-10-07 14:08:11 +00:00
EugeneTes
372c600503 fix(telegram): clean exit on A2A bind failure, log listen URLs, /a2a polish 2026-10-07 14:02:49 +00:00
EugeneTes
a3aee939d4 feat(telegram): /a2a lists a chat's A2A callers and recent tasks 2026-10-07 13:53:47 +00:00
EugeneTes
c1a8744eb7 feat(telegram): serve A2A from the bot, callers per chat workspace, notify_owner into the chat 2026-10-07 13:50:54 +00:00
EugeneTes
3a07d00eb2 feat(telegram): log trigger changes and the trigger count at startup 2026-10-05 18:43:30 +00:00
EugeneTes
be3c77b4d0 feat(telegram): watch groups silently and route Jev topic triggers to the owner's chat 2026-10-05 16:26:16 +00:00
EugeneTes
4ce71201b0 feat(telegram): trigger engine with Jev classification, debounce and snooze batches 2026-10-05 16:21:41 +00:00
EugeneTes
50d0e81aac feat(telegram): group registry, trigger store and snooze queue 2026-10-05 16:17:17 +00:00
EugeneTes
af714cc50b feat(telegram): trigger options and Jev client for OpenRouter systemone 2026-10-05 16:15:06 +00:00
EugeneTes
92a0061fdd feat(telegram): ADMIN_USER_ID may /impersonate another user
While on, the admin's updates are routed as the target user's (allow-list,
chat, workspace, session, settings, jobs) and the target chat's output goes
to the admin's chat. The target's own updates are dropped with a notice that
an admin is working with their account. In memory; /impersonate 0 stops.
2026-10-05 07:24:07 +00:00
EugeneTes
64ea6f7fa1 feat(telegram): retry transient speech-to-text failures up to 3 attempts 2026-10-05 07:02:23 +00:00
EugeneTes
39e7849039 fix(telegram): cut the voice transcript echo to 1024 characters 2026-10-02 09:51:57 +00:00
EugeneTes
0cb791d529 fix(telegram): break the DI cycle through CalendarScriptEnvironment
ChatRegistry -> JobScheduler -> IScriptEnvironment -> ChatRegistry made the host
fail at startup. CalendarScriptEnvironment now takes TelegramOptions and computes
the workspace with the new static ChatRegistry.WorkspaceFor(options, chatId).
2026-10-02 08:45:48 +00:00
EugeneTes
d2fd7ffa02 feat(google-calendar): encrypt chat tokens and give watchers a short-lived access token
token.json is sealed with AES-256-GCM (key derived from the OAuth client secret,
bound to the chat), so the agent's processes can read the file but not the
refresh token. Plain files, including VolcanicSecretary's refresh_token format,
are rewritten sealed on first read.

The scheduler gains IScriptEnvironment: extra per-run variables set after the
process environment filter and redacted from the run's output. The Telegram
host uses it to pass GOOGLE_ACCESS_TOKEN, refreshed to cover the job's timeout;
the refresh token and client secret never reach the script.
2026-10-02 08:31:24 +00:00
EugeneTes
28bbfbc759 feat(scheduler): store jobs per chat and import VolcanicSecretary job files
Each chat's jobs live in <workspace>/.volcanic/scheduler/jobs.json. On start the
scheduler loads every chat's file, moves the retired shared
<agentDir>/scheduler/jobs.json into them and renames it .migrated-<ts>.
A VolcanicSecretary jobs.json (bare array) in a chat's folder is converted
in place for that chat; the original is kept as .secretary-<ts>.

Also adds the VolcanicSecretary data migration plan.
2026-10-02 07:29:48 +00:00
EugeneTes
8014a0e770 refactor(scheduler): drop the reminder job kind
Jobs are prompts or watchers. A reminder is a prompt the agent passes on, so
it lands in the transcript. Stored reminder jobs load as prompts
("Remind me: <text>"). Also records the Coolify deploy target in CLAUDE.md.
2026-09-29 09:30:40 +00:00
EugeneTes
fd6b9027b6 fix(scheduler): address review findings
- JobScheduler: a stored job whose zone lookup throws no longer fails StartAsync; timer re-armed under the lock;
  a delivery cut off by shutdown isn't recorded (one-offs fire again after restart); CancelJobAsync in one step
- Schedules.ParseAt: ISO 8601 with a date only (no machine-clock date for a bare 09:00)
- core: a FollowUp prompt that loses the race to a starting run joins it as a follow-up, not steering
- Telegram: TimeZoneFor tolerates non-numeric targets; RunPromptAsync waits only when queued; one agent dir
- ScriptRunner: bad workspace paths are Failed, not exceptions; zone id helpers share TimeZoneIds.ToIana's rule
- tests: no wall-clock sleeps in scheduler tests; transcript-entry, disabled-scheduler and failing-prompt coverage
- docs: spec notes for the above; README row for the scheduler
2026-09-27 10:56:45 +02:00
EugeneTes
8dd874a83f feat(telegram): scheduler delivery, hosted service, /jobs and /cancel
Spec: /jobs and /cancel explain when scheduling is off; Scheduler options validation.
2026-09-27 10:33:38 +02:00
EugeneTes
5b780fac36 feat(telegram): per-chat time zones with /timezone, set_timezone and Telegram:DefaultTimeZone
ChatSettingsStore keeps each chat's IANA zone in <AgentDir>/telegram/chat-settings.json;
ChatTimeZoneService is the one path for a change (store, live session, scheduler re-evaluation,
reply text). ChatRegistry.TimeZoneFor now reads the store and new sessions start in the chat's zone.
/timezone, the set_timezone tool (ChatSettingsExtension), /status zone line, DEFAULT_TIMEZONE.

The store updates its cache under the write gate so the last file written is the newest state.
2026-09-27 10:29:29 +02:00
EugeneTes
6ab98a8155 fix(telegram): address review findings
- A speech or download timeout (TaskCanceledException nobody asked for) is a failure
  with the user notice, not a silent shutdown; OpenAiSpeechToText maps it to
  "the request timed out."; UpdateRouter only treats the caller's cancellation as shutdown.
- send: WorkspacePaths resolves the real path component by component (realpath),
  closing the directory-link-inside-target bypass; `.` reports a directory.
- Image documents are attached inline only when their magic bytes are JPEG/PNG/GIF/WebP,
  under the detected type.
- Audio goes to STT as an ASCII audio.<ext> name.
- MCP Browser section mentions send only when it is active.
- RunInOrderAsync takes a cancellation token; queued work is skipped once disposal gives up;
  SendReplyAsync stays private behind ReplyInOrderAsync.
- Speech HttpClient handler recycles pooled connections; BuiltInTools.Send();
  IExtensionHost.Channel is abstract; WorkspacePaths in its own file.
- Tests: timeouts, symlink chains (Linux), sniffing, channel delivery failures,
  queued-work cancellation, gated voice ordering. Spec and docs updated.
2026-09-26 22:35:20 +02:00
EugeneTes
f90731baf0 feat(telegram): transcribe voice notes and audio with an echo
Content step 3 (InboundMedia.AddSpeechAsync): voice and audio go through ISpeechToText, the transcript is echoed as '๐ŸŽค โ€ฆ' and prompted as '[Voice message] โ€ฆ'. Spec ยง4 edited: audio is transcribed from the file step 2 saved (one download); an oversize or undownloadable voice note sends its notice and isn't prompted.
2026-09-26 22:13:10 +02:00
EugeneTes
a0cd2975b8 feat(telegram): save documents, videos and audio to the chat inbox
Content step 2 (InboundMedia.AddFilesAsync, InboxFiles): files land in <cwd>/inbox/<date>-<safe name>[-N], image documents within Telegram:InlineImageMaxBytes are attached inline, oversize files and failed downloads get a notice and no run. Documents, videos, audio and voice are accepted; the unsupported reply is now 'I can read text, photos, files and voice messages.'

Spec: Updates helpers are named DocumentMessage/VideoMessage/VoiceMessage/AudioMessage; the getFile 'file is too big' notice reads 'over 20 MB'; the too-large inline note is a parenthesised suffix after the size; size formats spelled out.
2026-09-26 22:09:16 +02:00
EugeneTes
8758f4b3d1 feat(telegram): OpenAI speech to text service and speech options
Spec 5: the HttpClient registration is named as AddHttpClient<ISpeechToText, OpenAiSpeechToText>() (package Microsoft.Extensions.Http), which is how Program.cs registers it.
2026-09-26 22:04:27 +02:00
EugeneTes
ee3049e47a feat(telegram): TelegramChatChannel delivers send files and scheduler texts in order
Add TelegramLimits, TelegramChatChannel (every delivery through
ChatRenderer.RunInOrderAsync, auto photo falls back to a document on a 400)
and wire it into ChatRegistry as AgentSessionOptions.Channel and
ChatContext.Channel. Spec: SendTextAsync names its failure text
("Telegram did not accept the message.") and throws when any chunk fails.
2026-09-26 22:00:16 +02:00
EugeneTes
1eed03e851 feat(telegram): ordered renderer work items and file uploads 2026-09-26 21:56:05 +02:00
EugeneTes
0b28b9627d Merge branch 'feat/google-calendar' 2026-09-26 21:24:34 +02:00
EugeneTes
4cfe3cec47 Merge branch 'feat/mcp-client-playwright' 2026-09-26 21:20:52 +02:00
EugeneTes
4fb8f39ab4 feat(telegram): MCP servers and the Playwright preset in the bot 2026-09-26 21:05:16 +02:00
EugeneTes
881e7f0be9 fix(google-calendar): address review findings
- log an unknown stored zone once even when the tool description reads it first
- serialise the flow's token write with /calendar_disconnect under the flow gate
- a cancelled or expired pending flow is cleared, not re-sent or shown
- chat keys anchor with \z so a trailing newline is hashed
- a timeMax date includes that whole day (spec updated)
- dispose the flow's CancellationTokenSource, drop ConnectFlow.Token, Begin is a plain write
- GoogleCalendarOptions.IsConfigured replaces the host's copy of the rule; StoredToken is internal
- test BotSetupService's menu through DI, tokens absent from tool results
- Telegram.Tests csproj reference placed as in telegram-message-context; ChatHost passes calendar by name
- docs: per-event timeZone, both config sources for /calendar_*
2026-09-26 20:30:56 +02:00
EugeneTes
a635e9761b feat(telegram): Google Calendar per chat (config, secret value, menu, help)
Spec ยง8: extension added when either value is set, menu and /help only when both are; host code in GoogleCalendarSetup.cs.
2026-09-26 20:18:00 +02:00
EugeneTes
2ec941bf2a fix(telegram): address review findings
- /resume discards the inbox once the id names one conversation, before any await
- /stop discards input still waiting in the window
- a forwarded message is never a command or a pending answer
- ChatInbox.DrainAsync is an internal test helper
- tests: /resume and /stop drop buffered text, forwarded /new is content,
  a failing flush doesn't stop later batches; no wall-clock sleep in the drain test
- spec and user docs: timestamp exception for extension commands, command
  ordering, InputDebounce=0 wording
2026-09-26 20:15:57 +02:00
EugeneTes
98faaf3a12 feat(telegram): send ๐ŸŸข Online. to the owner once after bot setup 2026-09-26 20:07:58 +02:00
EugeneTes
c3f1bbd463 feat(telegram): react to steering messages, discard the inbox on /new and /resume
A flushed batch that joins a running request gets a ๐Ÿ‘€ reaction on each message. /new and a successful /resume drop buffered and in-flight inbox parts. A forwarded message never answers a pending TelegramUi question. /help mentions replies and quotes.
2026-09-26 20:06:43 +02:00
EugeneTes
7bd3bb340c feat(telegram): route messages through the chat inbox with context headers
Spec: unhandled /commands bypass the inbox verbatim; the header zone comes from ChatRegistry.TimeZoneFor (session zone until ChatSettingsStore); only text and photos until the attachments spec.
2026-09-26 20:04:06 +02:00
EugeneTes
5a8c4e6b4b feat(telegram): inbound composer with the text and photo content step 2026-09-26 20:00:23 +02:00
EugeneTes
7eb9f84faa feat(telegram): InputDebounce option and message reactions 2026-09-26 19:58:55 +02:00
EugeneTes
8e31dc61b9 feat(telegram): per-chat inbox debouncer with ordered leases
Spec: ChatInbox takes an optional ILogger (dispose count, detached leases, flush failures).
2026-09-26 19:57:23 +02:00