Every terminal or actionable unscheduled waiting task transition directly starts one ordinary notification-agent job. Optional notify.on metadata tells that agent which outcomes may use the task's origin.
Spynel's communication pillar gives one human a consistent assistant-facing relationship without making Spynel itself an AI agent. The classic program routes the TUI, Telegram, WhatsApp, and plain CLI through the same application handler, while external coding harnesses supply intelligence. Slash commands, typed settings, hooks, history isolation, job tracking, stopping, and harness dispatch therefore stay consistent across transports; visual Markdown presentation remains channel-specific.
Automatic proactive notifications stop at the ordinary asynchronous job boundary. The job receives the complete bounded task plus ordinary spynel notify syntax. The agent decides whether to call that CLI and directly edits the task's progress log with its successful send, skip reason, or CLI failure. Spynel does not persist notification-agent control state, bind a special action, interpret provider output, write a result, or recover/replay the turn. The ordinary CLI still reapplies live channel authorization and uses the normal delivery path.
Multiple TUI windows can run from the same initialized workspace. One process is elected as the workspace server and alone owns Telegram, WhatsApp, continuous orchestration, shared configuration mutations, and harness session persistence. Every window and transient plain CLI command connects to that owner over a private authenticated loopback endpoint. The lease's non-secret environment ID lets a client reject a loopback endpoint known to belong to another host/container before dialing it; this detects an unsupported topology but does not provide a host-reachable API, port forwarding, or relay. Startup discovers the atomically published owner record without waiting for the election mutation lock, so a new window can join a healthy same-environment owner immediately as a secondary while its background election monitor continues polling. An interactive secondary prints Connecting to the existing Spynel primary… before entering the TUI and follows it with success or a bounded actionable error; noninteractive and redirected output remain unchanged. Missing/invalid identifiers from older owners receive at most a bounded compatibility attempt and never authorize takeover of a fresh lease. When startup finds no healthy owner, the election winner resumes the most recently updated TUI conversation; simultaneous election losers use fresh histories. A window that finds an existing owner always starts a new tui/local-<instance-id> history and shows the welcome banner. If the owner exits cleanly, another window takes over immediately; if it dies or stalls, takeover occurs after its five-second heartbeat has been stale for 30 seconds. /primary safely promotes an idle local TUI through a target-fenced handoff; it is unavailable from Telegram, WhatsApp, and plain CLI conversations.
/help is a concise topic index everywhere. /status keeps the title and abbreviated requesting/elected-primary instance IDs first, then groups live jobs, durable active tasks with waiting shown as a subset, durable active goals, orchestrator leases/dispatches, and the actual primary-owned semantic-heartbeat deadline. There is no separate heartbeat-result or notification-response status. Channel health, harness/model/filesystem/startup state, logs, and turn state follow; provider prose, prompts, origins, message bodies, recipients, theme, and conversation thread are intentionally omitted.
/jobs lists the same global live workspace executions for every authenticated Telegram, WhatsApp, TUI, CLI, and shared application/API caller; /jobs recent, /job info <number>, and /job output <number> expose the same private workspace archive after completion. The workspace-local counter advances from 1 through 9999, wraps to 1, and preserves the assigned number through recovery and archival. After reuse, inspection resolves the newest generation; private archive identities prevent older retained records from overwriting it. /job message <number> <text> sends nonterminal guidance into an active orchestrator session, and /job ping <number> requests a durable progress/blocker/next-action update before the original work continues. Creation channel, caller conversation, and notify.origin never filter inspection or supported controls; authentication, sender allow-lists, loopback security, and workspace admission remain enforced before this boundary, and archive paths cannot escape the active workspace.
/tasks and /goals are shared workspace inspections available through every authorized transport. Telegram and WhatsApp pass the complete command text to the same application handler as the TUI, including semantic views and options. External terminal programs use that handler through spynel tasks, spynel goals, or the generic spynel command; direct aliases also support the plain CLI NDJSON event contract. The inspectors read bounded allowlisted fields from the configured durable workflow folders and never invoke a harness. Bare commands show every nonterminal item. Semantic recent, active, review, waiting, done, failed, and all views provide likely workflow snapshots; failed groups failed/cancelled tasks or abandoned goals. Day, limit, and detail options compose with those views. Output is newest-first, compact, control-sanitized, and explicit about bounded read warnings.
Configuration forms replace chat only in the TUI. Text transports use the same setting keys through /config, /telegram, and /whatsapp. Telegram cannot configure Telegram from Telegram, and WhatsApp cannot configure WhatsApp from WhatsApp. /quit is TUI-only and never terminates a remote server; /restart is shared, acknowledges the issuing channel, and relaunches the complete application with saved state intact. An eligible interactive npm-launched TUI reuses the launcher's bounded startup check, refreshes it asynchronously at most hourly, and shows a yellow ⚠ beside the top-right version only while a newer semantic version is known. Registry failures do not interrupt the interface; noninteractive and automatic-startup launches make no proactive check. /update reports a bounded npm version check everywhere, while /update install is offered only when the owner is supervised by the npm launcher so package replacement happens after shutdown.
Every non-empty /jobs result places Use /job info <number> to inspect a job. immediately above its /job kill hint.
Before the normal TUI and primary-owner election start, bare spynel checks whether its canonical launch directory is uninitialized beneath an initialized workspace. In that case a required themed screen offers exactly Use parent workspace (default), Initialize here, and Exit. Up/Down, Tab/Shift+Tab, Space/Enter, Escape, and Ctrl+C follow the standard required-screen controls; the two exit keys choose the non-mutating exit path. Server and automation commands never display this screen.
The full-width header is reserved for a two-cell Spynel state logo plus customizable title, compact TG/WA connection symbols, open durable goals, open durable tasks, live jobs, and retained logs in that order. Goal and task totals use the same bounded authoritative census as /status, include waiting work, exclude terminal outcomes, and refresh in every attached TUI without a restart. Every count uses singular only for an exact value of one (1 goal) and plural otherwise, including compact large values. The header shares the active main background and starts, separates, and ends items with double half-height top ▀▀ ribbons in a muted blend of the theme's user accent, without separator padding or individual badges. The default palette keeps the identity pink and renders the ribbons restrained light blue related to the You label. The logo animates through the specified left-to-right circle sequence at full speed while the main chat agent is active and half speed for background-only work; whenever neither is active, it immediately renders the empty ○○ state and schedules no animation tick. Connected, connecting, error, and unconfigured transports use distinct geometric icons/colors.
The footer is reserved for exactly one left-aligned context-sensitive control-hint set. Bindings are compact and symbol-first (↵ send, ⇧↵ line, ⇞⇟ history, ⌃C clear/stop/quit, ␛ back/cancel/close, and corresponding picker/form glyphs). It shares the active main background and uses the same muted user-accent double half-height bottom ▄▄ ribbons before and between items and through the unused right side, without separator padding. Opening the command picker or a form replaces the ordinary composer hints rather than prepending another status string.
Chat history and form screens are unframed on one continuous semantic theme background. Each has one fixed blank row above and below its scrollable content, one left padding cell, and one padding cell before the final right-edge scrollbar column; the scrollbar's inactive │ track and proportional wide ┃ thumb are painted on that same background. Chat has no individual message rectangles. The composer and command/theme pickers retain exact-width borders whose complete cells use the page background, while compact form actions and selections use lighter semantic surfaces.
All terminal colors are semantic and loaded from .spynel/themes/*.yaml. New workspaces receive the documented ordered collection of twelve editable palettes: all six dark themes followed by all six light themes, with GitHub Colorblind Dark and Okabe-Ito Dark as the accessible dark choices and Tol Muted Light and Okabe-Ito Light as the accessible light choices. spynel and hack-the-box remain first and second, unchanged. Upgraded workspaces with no theme files expose the same built-in set; workspaces with files retain them as user-owned and can add only missing revised templates with spynel init --force. Bare /theme opens an inline list; Up/Down previews immediately, Enter saves, and Escape cancels. /theme <name> also works through Telegram and WhatsApp, while bare remote /theme lists names and descriptions.
The composer begins at one row, expands in place through ten visual rows, then scrolls only enough to keep the cursor on its last row. The first character that creates row eleven scrolls immediately even while its word is unfinished; it never waits for a following space. Composer/menu resizing keeps a bottom-anchored history viewport at the bottom so the newest message remains visible, while a viewport deliberately paged up retains its exact top offset. Enter sends; Shift+Enter inserts a newline; Alt+Enter follows the send path. Up/Down moves through logical and terminal-wrapped composer rows at the current visual column; Up from the first visual row jumps to the start of the complete input, and Down from the final visual row jumps to its final insertion position. PageUp/PageDown scrolls chat only.
Mouse reporting is disabled so Warp and other terminals can use ordinary drag selection/copying and native wheel behavior. Spynel discards stale complete or fragmented SGR mouse reports defensively, preventing escape-sequence bytes from entering the composer after a burst. Terminal-owned shortcuts such as Cmd+K do not reach the program; a ten-second cache invalidation self-heals cleared output, and Ctrl+L forces an immediate redraw.
Typing / at the beginning opens the canonical picker. Continue typing to filter, use Up/Down to select, Tab to insert, Enter to send, and Escape to close. Rows are flush-left and selection uses a filled semantic highlight rather than a marker. The picker title is only Commands; bindings live in the footer.
Ctrl+C from any non-required form, picker, fullscreen QR view, or dialog discards that surface's in-memory state and returns directly to main chat instead of stepping through its parent or unsaved-change confirmation; required setup exits. Main-chat Ctrl+C remains state-based: it clears non-empty input and transient UI, dispatches /stop for an active local turn with an empty composer, or exits idle chat. /quit exits immediately.
Chat uses equal-width You and Spy labels without a > separator; every wrapped/explicit continuation begins at the same four-column content position. Only You uses a restrained blue accent.
A harness turn displays the compact spinner sequence ⠋ ⠙ ⠸ ⠴ ⠦ ⠇ immediately after the newest response character, with no “Working” label. Multiple agent-message items are joined with one newline and retained. A local slash command sent during an active turn commits the current streaming entry, renders the command normally, and opens another Spy spinner entry for later harness output. The local response cannot stop the underlying harness animation.
Live deltas and intermediate continuation responses remain visible in the TUI. Telegram and WhatsApp translate the canonical communication-agent lifecycle into native typing and composing indicators. Neither remote transport sends progress events or transport-status handoffs; the remote conversation receives only the last non-continuing final response or terminal error. Routine remote task/goal confirmations are one brief natural result and never contain local-path Markdown links, task/goal IDs, or orchestration metadata. Exact blockers and explicitly requested technical details remain visible. Plain CLI progress remains an explicit --stream or --json choice.
Agent Markdown renders in the terminal. One source paragraph gap remains one blank row; code blocks do not acquire extra outer blank rows. Explicit URI and absolute local-file destinations use standard OSC 8 links in supporting terminals. Relative files remain plain because the renderer cannot safely infer a base directory.
Bracketed pastes of at least 1,000 characters appear as atomic [Pasted N chars] tokens but dispatch their full content. Pasted/dropped readable local file paths are copied to .spynel/attachments/ and become atomic attachment links. Left/Right jumps across a token; Backspace/Delete removes it in one action. Terminals provide dropped files as text paths and do not expose arbitrary binary image clipboard payloads.
After a persisted transcript is rendered using the terminal's real viewport dimensions, its first view starts at the newest message. This positioning happens once, including when the initial history render is asynchronous or the terminal resizes during startup. Subsequent streaming uses a separate follow rule: a viewport within one current visible page of the tail stays attached as content grows, wraps, finalizes, or is rerendered. PageUp or Shift+Up immediately suspends that follow behavior for two seconds so reading is never snapped away; it resumes after the grace period if the viewport is still tail-adjacent, or immediately when PageDown/Shift+Down reaches the bottom. Sending a message also returns to and follows the active tail. A viewport farther than one page from the tail preserves its top offset across streaming, notifications, and layout changes.
Initialization, config, Telegram, WhatsApp pairing, harness, model, and resume views share one borderless screen canvas with one cell of padding on all four sides and a final-column scrollbar. Wizard and selection titles plus optional wizard tabs stay fixed above scrolling controls; main configuration omits redundant title/subtitle copy and begins at Core settings. Wizard tabs are two rows: ordinary bold labels above one continuous heavy line, with only the segment beneath the active label using the primary highlight. Each field places its label at left and right-aligned value on the same row, followed immediately by a full-width muted description; ordinary settings have exactly one blank row between items, while labeled section rules have two blank rows above them when they follow content. Fixed choices show ‹ value ›. Actions and standalone disclosures use compact filled <label> ↵ buttons whose side spaces share the button background. Advanced settings merge their heading and disclosure into a single rule whose clickable segment reads Show Advanced Settings ↵ when collapsed and Hide Advanced Settings ↵ when expanded. Up/Down or Tab/Shift+Tab selects a control; text edits in place; Space/Enter cycles selections. Ctrl+S saves /config, /telegram, or /whatsapp and returns to chat on success; a failed save keeps the edited form open. Escape returns directly from a clean form, while a dirty form opens a centered confirmation overlay with Save, Discard, and safe-default Keep editing. Save uses the same validated persistence path; modal keys cannot edit the form behind it. Muted ←→ nav ─ ␠/↵ choose ─ ␛ cancel hints occupy the lower border below one blank interior row. The harness and model views are immediate action lists instead: every choice has its own row, the current value starts focused, and Space/Enter applies it without Ctrl+S. When either chooser is opened from config, the TUI preserves the full parent form—including unsaved edits, focus, advanced disclosure, and scroll—and restores it after Enter or Escape; directly opened choosers return to chat. A missing harness remains on the chooser with installation guidance. Main configuration groups harness/model actions, agent filesystem access, context, and startup essentials under Core settings, with all other controls collapsed under the combined Advanced settings disclosure. Bare /telegram and /whatsapp open the first wizard step directly while first-time setup is incomplete, omitting the empty status/config form. WhatsApp remains incomplete until it has at least one allowed phone number; session-database existence alone is insufficient. Once configured, both top-level forms start with one merged Telegram Status or WhatsApp Status rule, live connection state plus available detail or error text, and no redundant page heading or subtitle. They keep Enabled in that status section before configuration, put the guided wizard under Setup, keep remaining essentials under Basic settings, and open optional controls through the same combined disclosure rule. Wizard screens omit the live transport status and begin directly with setup title, tabs, instructions, and controls. They use the same keyboard navigation, keep one blank row between an editable control and its navigation buttons, render official resources as OSC 8 terminal hyperlinks, carry secret and ordinary values only in process memory between steps, and save essentials transactionally at completion. Cancel closes a directly opened first-time wizard; from an existing channel form it restores that preserved form. WhatsApp has no enable-choice step: continuing from its access screen saves mode and allowed numbers, enables the channel, and opens pairing. Its QR is never embedded in the wizard: Show QR renders it alone across the full terminal, with any key returning to the controls. Rotated QR data updates in place, while timeout returns to the wizard. Retry pairing replaces the expired session and clears its stale state; repeating setup does the same. Use pairing code provides WhatsApp's phone-number linking alternative.
The TUI loads only a fixed newest display tail from its selected history. An ownerless-startup winner continues the latest TUI history; a new secondary uses tui/local-<instance-id> and always receives the welcome. Automatic onboarding is a non-interactive, non-persisted banner above an otherwise empty chat, using the exact revised five-row open-circle logo in the theme primary color and a concise, conversational introduction while leaving the composer active. Spynel and Spy use strong Markdown rendered in the theme primary color. The intro's final two sentences are adjacent; a separate hint list always includes /help and includes Telegram or WhatsApp setup only while that transport is disconnected. Manual /welcome instead appends the same content as a persisted assistant message at the current bottom and renders its logo with that same primary accent. Persisted spynel-logo fences are semantic markers whose body is replaced with the current canonical logo during rendering. Telegram and WhatsApp omit the logo, transport hints, and TUI-only harness warning; they receive only the introduction and /help with native strong formatting. /resume lists disk-backed histories and copies a selected one to tui/resume-<short-id> before switching chat to that independent branch. /clear removes only the current TUI conversation, its harness session, and any visible automatic welcome; it does not reopen onboarding in the same running window.
Telegram uses the HTTPS Bot API directly. Polling clears a prior webhook without dropping pending updates and then long-polls. Webhook mode binds the configured local listener, registers a private derived path below the public URL, optionally verifies Telegram's secret header, and processes a bounded queue. Both modes call getMe for mention/reply group policy.
The allow-list accepts positive numeric user IDs and case-insensitive ASCII usernames made from letters, digits, and underscores, with or without a leading @, and must contain at least one valid entry before Telegram can be enabled. The adapter resolves the current live list before startup, every inbound update, provider API call, webhook bind, and proactive delivery. Missing, malformed, or revoked authorization publishes a persistent connection error, attempts no provider or listener side effect, and is not retried until channel configuration changes. Every inbound update repeats this check before identity persistence, media, typing, command or agent dispatch, replies, attachments, and hooks. When an authenticated private inbound update matches a username entry, Spynel stores a minimal verified username-to-numeric-ID mapping under private runtime state so restart-safe proactive notifications can use the stable TG-<user-id> origin. Removing the username (and any matching numeric ID) revokes delivery immediately even if that mapping remains on disk. The form and wizard link to the third-party @userinfobot helper for discovering numeric IDs. Telegram's verified getMe identity is carried with live connection status, and /telegram renders the bot's @username as a clickable t.me link. In groups, mention responds to an @bot mention or reply, all handles every eligible message, and off ignores groups. New-member welcome messages, notices, and retention cleanup are optional advanced controls.
Text, captions, documents, photos, video, audio, and voice are accepted. Media is streamed to .spynel/attachments/telegram/; voice retains its link even when transcription is disabled or fails. Agent Markdown becomes Telegram's supported HTML subset and is split within the 4096-character limit. A final-response [Send attachment](</absolute/path/to/file>) line uploads a native Telegram document, while [Send photo](</absolute/path/to/image.png>) uploads a native photo. Group commands such as /help@bot commands normalize to the same shared command while keeping arguments.
An ordinary accepted Telegram message starts typing immediately, including during media download and voice transcription for that message, matching the TUI's immediate user-visible communication-turn activity. A leading slash delays activation until the application identifies an agent-backed command, so framework-only commands never flash typing. Background orchestration, proactive outbox sends, and final response or attachment delivery do not activate typing. Telegram documents that a chat action lasts no more than five seconds, so Spynel refreshes every four seconds to leave one second for ordinary scheduler and network jitter. The best-effort requests are individually time-bounded and serialized by one worker per chat; transient failures retry on later ticks without accumulating requests. Each response stream owns one idempotent reference, and the TUI animation and Telegram indicator both retain activity until every overlapping main-agent reference has ended. A released or completed turn therefore cannot silence a newer turn in the same chat or affect another chat, even if its terminal event arrives late. The main-agent terminal event stops activity before final delivery; handler failure, cancellation, panic unwinding, and channel shutdown also clear it. A transport emitter handoff releases the old reference only after the replacement has started, avoiding an ownership gap.
Private history/session identity is TG-<numeric-user-id> and group identity is TG-group-<chat-id>, so bot restarts reuse the same conversation.
WhatsApp uses whatsmeow with a private CGO-free SQLite multi-device store. If no device exists, /whatsapp opens its pairing controls. Show QR uses a chrome-free full-terminal render, Retry pairing starts a fresh session after timeout, and Use pairing code generates WhatsApp's official phone-number linking alternative. A stored session reconnects automatically. The configured interval checks live connection health.
Pairing timeout and terminal error events self-recover after a short bounded delay by replacing the websocket and QR session. The manual retry action is only an immediate override.
In self-chat mode, only messages sent by the linked user to their own chat are accepted and sent-message IDs are suppressed to prevent loops. In dedicated mode, messages sent by the linked account are ignored. At least one valid canonical number is mandatory before the adapter opens its session database, subscribes to events, connects, or pairs. Number allow-lists normalize punctuation and international prefixes but reject letters, normalization-empty values, and values beyond 15 digits. The adapter resolves the current live list again for every event and queued message before media, composing presence, command or agent dispatch, reply, attachment, hook, pairing-code, and provider-delivery work. Missing or revoked authorization publishes a persistent error and suppresses downstream callbacks without a reconnect loop; stored session state and self-chat identity never grant access by themselves. Enabled groups still require a mention or reply to the linked account.
Text, captions, documents, images, video, audio, and stickers are accepted. The SDK decrypts directly into .spynel/attachments/whatsapp/ without holding the whole file in memory. Agent Markdown becomes WhatsApp-native bold, italic, strike, monospace, quote, and list syntax and is split into bounded messages. The same [Send attachment](</absolute/path/to/file>) and [Send photo](</absolute/path/to/image.png>) final-response lines upload encrypted native documents and images through WhatsApp.
Outbound directives must occupy their own line. Spynel strips them from the accompanying response text, accepts multiple directives, resolves the paths and any symlinks, and permits readable regular files outside the active workspace. It rejects relative paths, directories, unreadable or unresolved files, non-images marked as photos, and files larger than workspace.attachment_max_mb. Ordinary Markdown links and inbound [Attachment filename](<path>) references never trigger delivery.
An admitted communication-agent turn starts WhatsApp's composing presence and refreshes it every four seconds. Media preparation, framework-only commands, duplicate/eventless intake, notifications, and other background work remain silent. Activity is reference-counted per chat so an older completion cannot silence an overlapping agent turn; after the final reference ends, Spynel synchronously sends paused before the terminal response or handler error is delivered.
Direct identity is WA-<normalized-number> and group identity is WA-group-<group-id>.
whatsmeow implements the unofficial WhatsApp Web protocol. Protect the database like a login credential and evaluate account risk before production use.
Speech is enabled with English Parakeet in new workspaces; selecting auto or one of the other 24 supported language codes switches to multilingual Parakeet. Both remote transports append the absolute audio attachment link and an explicit [Generated voice transcription — may contain errors] block. A single shared worker serializes voice work, securely provisions checksum-pinned INT8 model files in a coordinated versioned OS per-user cache shared by compatible workspaces, decodes WAV/FLAC/MP3 with miniaudio, and detects and decodes Telegram/WhatsApp Ogg/Opus voice notes in process. Attachments remain workspace-owned. Every path produces bounded mono 16 kHz PCM chunks for sherpa-onnx. No FFmpeg or Python process is used.
Every stable external identity maps to a sanitized append-only JSONL file containing timestamp, role, sender, and content. The complete path is linked in the harness prompt, but only the newest window allowed by both workspace.history_max_messages and workspace.history_char_limit is read from disk. /clear erases only the issuing remote conversation and resets only its harness session. Remote transports cannot /resume, because they cannot replace their native visible history; their files remain browsable and branchable from the TUI.