# Takopi Specification v0.22.3 [2026-03-02] This document is **normative**. The words **MUST**, **SHOULD**, and **MAY** express requirements. ## 1. Scope Takopi v0.22.3 specifies: - A **Telegram** bot bridge that runs an agent **Runner** and posts: - a throttled, edited **progress message** - a **final message** with the final answer and a resume line - **Thread continuation** via a **resume command** embedded in chat messages - **Parallel runs across different threads** - **Serialization within a thread** (no concurrent runs on the same thread) - **Automatic runner selection** among multiple engines based on ResumeLine (with a configurable default for new threads) - A Takopi-owned **normalized event model** produced by runners and consumed by renderers/bridge Out of scope for v0.22.3: - Non-Telegram clients (Slack/Discord/etc.) - Token-by-token streaming of the assistant’s final answer - Engines/runners that cannot provide **stable action IDs** within a run ## 2. Terminology - **EngineId**: string identifier of an engine (e.g., `"codex"`, `"claude"`, `"pi"`). - **Runner**: Takopi adapter that executes an engine process and yields **Takopi events**. - **Thread**: a single engine-side conversation, identified in Takopi by a **ResumeToken**. - **ResumeToken**: Takopi-owned thread identifier `{ engine: EngineId, value: str }`. - **ResumeLine**: a runner-owned string embedded in chat that represents a ResumeToken. - **Run**: a single invocation of `Runner.run(prompt, resume)`. - **TakopiEvent**: a normalized event emitted by a runner and consumed by renderers/bridge. - **Progress message**: a Telegram message that is periodically edited during a run. - **Final message**: a Telegram message that includes run status, final answer, and resume line. ## 3. Resume tokens and resume lines ### 3.1 Decision: canonical resume line is the engine CLI resume command The canonical ResumeLine embedded in chat MUST be the engine’s CLI resume command, e.g.: - `codex resume ` - `claude --resume ` - `pi --session ` ResumeLine MUST resume the interactive session when the engine offers both interactive and headless modes. It MUST NOT point to a headless/batch command that requires a new prompt (e.g., a `run` subcommand that errors without a message). Takopi MUST treat the runner as authoritative for: - formatting a ResumeToken into a ResumeLine - extracting a ResumeToken from message text ### 3.2 ResumeToken schema (Takopi-owned) ```python @dataclass(frozen=True, slots=True) class ResumeToken: engine: str # EngineId value: str ``` ### 3.3 Runner resume codec (MUST) Each runner MUST implement: * `format_resume(token: ResumeToken) -> str` * `extract_resume(text: str) -> ResumeToken | None` * `is_resume_line(line: str) -> bool` Constraints: * `format_resume()` MUST fail if `token.engine != runner.engine`. * `extract_resume()` MUST return `None` if it cannot **confidently** parse a resume line for its engine. ### 3.4 Bridge resume resolution (MUST) Given `text` (user message), optional `reply_text` (the message being replied to), and an ordered list of available runners `runners`: 1. The bridge MUST attempt to extract a resume token by polling all runners in order: 1. for each `r` in `runners`, attempt `r.extract_resume(text)` 2. choose the **first** runner that returns a non-`None` token and stop 2. If not found, it MUST repeat step (1) for `reply_text` if present. 3. If still not found, the run MUST start with `resume=None` (new thread) on the default runner (per §8, including chat-level overrides). ## 4. Normalized event model ### 4.1 Decision: events are trusted after normalization Runners are responsible for emitting well-formed Takopi events. Consumers (renderer/bridge) SHOULD assume validity and MAY fail fast on invariant violations. ### 4.2 Supported event types (minimum set) Takopi MUST support: * `started` * `action` * `completed` Minimal runner mode is supported: * A runner MAY emit only `started` and `completed`. * If `action` events are emitted, `phase="completed"` alone is valid (no requirement to emit `started`/`updated` phases). ### 4.3 Event schemas All events MUST include `engine: EngineId` and `type`. #### 4.3.1 `started` Required: * `type: "started"` * `engine: EngineId` * `resume: ResumeToken` Optional: * `title: str` * `meta: dict` #### 4.3.2 `action` Required: * `type: "action"` * `engine: EngineId` * `action: Action` * `phase: "started" | "updated" | "completed"` Optional: * `ok: bool` (typically on `phase="completed"`) * `message: str` * `level: "debug" | "info" | "warning" | "error"` Notes: * `phase="completed"` alone is valid. #### 4.3.3 `completed` Required: * `type: "completed"` * `engine: EngineId` * `ok: bool` (overall run success/failure) * `answer: str` (final assistant answer; MAY be empty) Optional: * `resume: ResumeToken` (final token; new or existing, if known) * `error: str | None` (fatal error message, if any) * `usage: dict` (telemetry/usage if available) ### 4.4 Action schema (MUST; stable IDs) Actions MUST have stable IDs within a run: ```python @dataclass(frozen=True, slots=True) class Action: id: str kind: str title: str detail: dict[str, Any] ``` Stability requirements: * Within a single run, the same underlying action MUST keep the same `Action.id` across events. * `Action.id` values MUST be unique within a run. * IDs do **not** need to be stable across different runs/resumes. Action kinds SHOULD come from an extensible stable set, e.g.: * `command`, `tool`, `file_change`, `web_search`, `subagent`, `turn`, `warning`, `telemetry`, `note` Unknown kinds MAY be rendered as `note`. `detail` is freeform; no per-kind schema is required. `ok` semantics are runner-defined. User-visible warnings/errors SHOULD be surfaced as `action` events (typically `kind="warning"` or `kind="note"`, `phase="completed"`, `ok=False`) rather than introducing new event types. ## 5. Runner protocol and concurrency ### 5.1 Runner protocol (MUST) ```python class Runner(Protocol): engine: str # EngineId def run( self, prompt: str, resume: ResumeToken | None, ) -> AsyncIterator[TakopiEvent]: ... ``` ### 5.2 Per-thread serialization (MUST; core invariant) Define: * `ThreadKey(resume) := f"{resume.engine}:{resume.value}"` Invariant: * At most **one** active run may operate on the same `ThreadKey` at a time. Rules: * Runs for different ThreadKeys MAY run in parallel. * Runs for the same ThreadKey MUST be queued and executed sequentially. * This invariant MUST be enforced by the runner implementation even if used outside the Telegram bridge. New thread rule (`resume is None`): * When the runner learns the new thread’s ResumeToken, it MUST: * acquire the per-thread lock for that token * do so **before emitting** `started(resume=token)` ### 5.3 `started` emission and ordering * If the runner obtains a ResumeToken for the run, it MUST emit exactly one `started` event containing that token. * The runner MAY emit `action` events before `started` (e.g., pre-init warnings). Consumers MUST NOT assume `started` is the first event. ### 5.4 Completion * If the run reaches `started`, and then terminates under the runner’s control (success or detected failure), the runner MUST emit exactly one `completed` event and it MUST be the last event. * If the runner never obtains a ResumeToken (e.g., fatal failure before session init), it MAY emit no `started` and no `completed`. ### 5.5 Event delivery semantics (MUST) * Events MUST be yielded in the order produced by the runner. * The runner MUST NOT spawn unbounded background tasks per event. * If the consumer stops iterating early (cancel/break/exception), the runner MUST abort the run best-effort and release any held locks/resources. ## 6. Bridge (Telegram orchestration) ### 6.1 Responsibilities (MUST) The bridge MUST: * Receive Telegram updates * Resolve resume token (per §3.4) * Schedule runs per thread (per §6.2) * Start runner execution with cancellation support * Maintain a progress message while avoiding excessive edits * Publish a final message containing status, answer, and resume line (when known) * Support `/cancel` for in-flight runs The bridge MUST NOT: * parse engine-native streams/events * embed engine-specific rules beyond calling runner resume extraction/formatting Queue depth: * There is no queue depth limit; all prompts are accepted. ### 6.2 Scheduling (MUST) Definitions: * `Job := (chat_id, user_msg_id, text, resume: ResumeToken | None)` Required behavior: * For `resume != None`, the bridge MUST enqueue jobs into `pending_by_thread[ThreadKey(resume)]`. * For each ThreadKey, exactly one worker (or equivalent mechanism) MUST drain the queue sequentially. * A worker MUST exit when its queue is empty; the bridge SHOULD avoid retaining state for inactive threads. * The implementation MUST avoid spawning one long-lived task per queued job (bounded concurrency). Runs that start as new threads: * If a job starts with `resume=None` and later yields `started(resume=token)`, the bridge MUST treat that run as the in-flight job for `ThreadKey(token)` until it completes (for scheduling and cancellation routing). ### 6.3 Progress message behavior * The bridge SHOULD send an initial progress message quickly (e.g., “Running…”). * The bridge SHOULD avoid excessive edits and respect transport constraints (implementation-defined). * The bridge SHOULD skip edits when rendered content is unchanged. * Once `started` is observed, the progress view SHOULD include the canonical ResumeLine. ### 6.4 Final message requirements (MUST) The final output MUST include: * a status line (`done` / `error` / `cancelled`) * the final `answer` (if any) * the ResumeLine if known (and MUST include it if `started` was received) ### 6.5 Cancellation `/cancel` (MUST) * The bridge MUST allow users to cancel a run in progress by sending `/cancel` in reply to the progress message (or by an equivalent mapping defined by the bridge). * Cancellation MUST terminate the runner process via **SIGTERM**. * After cancellation, the bridge MUST stop further progress edits and publish a “cancelled” status message. * The bridge SHOULD include the ResumeLine if known. * Any additional text after `/cancel` is ignored. ### 6.6 Telegram markdown + truncation (MUST) The bridge MUST: * escape/prepare Telegram markdown correctly * enforce Telegram message length limits (including after escaping) * avoid truncating away the ResumeLine (using `runner.is_resume_line()`) If truncation is required: * the bridge MUST keep the ResumeLine intact * the bridge SHOULD preserve the beginning of the content and insert an ellipsis at the truncation point ### 6.7 Crash/error handling (MUST) If the runner crashes or exits uncleanly: * the bridge MUST publish an error status message * if `started` was received, the bridge MUST include the ResumeLine in that error message ## 7. Renderer Renderers MUST: * be deterministic functions/state machines over Takopi events + internal renderer state * produce Telegram-ready markdown (or markdown + entities) * tolerate `action` events that are “completed-only” (no prior `started`/`updated`) Renderers MUST NOT: * depend on engine-native event formats * call Telegram APIs * perform blocking I/O Action update collapsing: * If multiple `action` events share the same `Action.id`, renderers SHOULD treat later `started`/`updated` events as updates (replace the prior running line rather than appending). ## 8. Configuration and engine selection Decision (v0.4.0): * Takopi MUST support configuring a **default engine** used to start new threads (`resume=None`). * If not configured, the default engine is implementation-defined (non-normative: the reference implementation defaults to `codex`). * If no engine subcommand is provided, Takopi MUST run in **auto-router** mode: * new threads use the configured default engine * resumed threads are routed based on ResumeLine extraction (per §3.4) * If an engine subcommand is provided, Takopi MUST still use the auto-router, but it overrides the configured default engine for new threads. * Resume extraction MUST poll **all** available runners (per §3.4) and route to the first matching runner. * New thread engine override (chat-level): * Users MAY prefix the first non-empty line with `/{engine}` (e.g. `/claude`, `/codex`, or `/pi`) to select the engine for a **new** thread. * The bridge MUST strip that directive from the prompt before invoking the runner. * If a ResumeToken is resolved from the message or reply, it MUST take precedence and the `/{engine}` directive MUST be ignored. * Bridges MAY persist default engine overrides per Telegram scope: * **Topic default**: forum topic (`chat_id + thread_id`) * **Chat default**: chat (`chat_id`) * When no ResumeToken is resolved, engine selection MUST follow this precedence: 1) explicit `/{engine}` directive 2) topic default (if any) 3) chat default (if any) 4) project default engine (if configured for the resolved context) 5) global default engine ### 8.1 Command menu (Telegram) Takopi SHOULD keep the bot’s slash-command menu in sync at startup by calling `setMyCommands` with the canonical list of supported commands. * The command list MUST include: * `cancel` — cancel the current run * one entry per configured engine * one entry per configured project alias that is a valid Telegram command * The command list MUST NOT include commands the bot does not support. * Command descriptions SHOULD be terse and lowercase. * The command list SHOULD be capped at 100 entries per Telegram's limit; if the config exceeds that limit, implementations SHOULD warn and truncate while still handling all commands at runtime. ## 9. Testing requirements (MUST) Tests MUST cover: 1. **Runner contract** * If a token is obtained: exactly one `started` * Action schema validity (required fields; stable unique IDs within run) * Event ordering preserved * `completed` emitted and last for controlled termination after `started` 2. **Runner serialization** * Concurrent runs for the same ResumeToken serialize * `resume=None` runs acquire the per-thread lock once token is known and before emitting `started` 3. **Bridge per-thread scheduling** * FIFO per ThreadKey * second job for same thread does not start until first completes 4. **Progress throttling** * edits not more frequent than configured interval * no edit when content unchanged * truncation preserves ResumeLine 5. **Cancellation** * `/cancel` terminates run and produces “cancelled” * ResumeLine included if known 6. **Renderer formatting** * completed-only actions render correctly * repeated events for same Action.id collapse as intended 7. **Auto-router engine selection** * resume lines for non-default engines are detected and routed correctly (poll all runners) * new threads use the configured default engine, with CLI subcommand overriding it Test tooling SHOULD include event factories, deterministic/fake time, and a script/mock runner. ## 10. Lockfile (single-instance enforcement) Takopi MUST prevent multiple instances from racing `getUpdates` offsets for the same bot token. ### 10.1 Lock file location The lock file MUST be stored at `.lock`. For the default config path, this resolves to `~/.takopi/takopi.lock`. ### 10.2 Lock file format The lock file MUST contain JSON with: * `pid: int` — the process ID holding the lock * `token_fingerprint: str` — SHA256 hash of the bot token, truncated to 10 characters ### 10.3 Lock acquisition rules * If the lock file does not exist, acquire and write the lock. * If the lock file exists and the PID is dead (not running), replace the lock. * If the lock file exists and the token fingerprint differs (different bot), replace the lock. * If the lock file exists, the PID is alive, and the fingerprint matches, fail with an error instructing the user to stop the other instance. ### 10.4 Lock release The lock file SHOULD be removed on clean shutdown. Stale locks from crashed processes are handled by the acquisition rules above. ## 11. Changelog ### v0.22.3 (2026-03-02) - No normative changes; align spec version with the v0.22.3 release. ### v0.22.2 (2026-02-24) - No normative changes; align spec version with the v0.22.2 release. ### v0.22.1 (2026-02-10) - No normative changes; align spec version with the v0.22.1 release. ### v0.22.0 (2026-02-10) - No normative changes; align spec version with the v0.22.0 release. ### v0.21.5 (2026-02-08) - No normative changes; align spec version with the v0.21.5 release. ### v0.21.4 (2026-01-22) - No normative changes; align spec version with the v0.21.4 release. ### v0.21.3 (2026-01-21) - No normative changes; align spec version with the v0.21.3 release. ### v0.21.2 (2026-01-20) - No normative changes; align spec version with the v0.21.2 release. ### v0.21.1 (2026-01-18) - No normative changes; align spec version with the v0.21.1 release. ### v0.21.0 (2026-01-16) - No normative changes; align spec version with the v0.21.0 release. ### v0.20.0 (2026-01-15) - No normative changes; align spec version with the v0.20.0 release. ### v0.19.0 (2026-01-15) - No normative changes; align spec version with the v0.19.0 release. ### v0.18.0 (2026-01-13) - No normative changes; align spec version with the v0.18.0 release. ### v0.17.1 (2026-01-12) - No normative changes; align spec version with the v0.17.1 release. ### v0.17.0 (2026-01-12) - No normative changes; align spec version with the v0.17.0 release. ### v0.16.0 (2026-01-12) - No normative changes; align spec version with the v0.16.0 release. ### v0.15.0 (2026-01-11) - No normative changes; align spec version with the v0.15.0 release. ### v0.14.1 (2026-01-10) - No normative changes; align spec version with the v0.14.1 release. ### v0.14.0 (2026-01-10) - No normative changes; align spec version with the v0.14.0 release. ### v0.13.0 (2026-01-09) - No normative changes; align spec version with the v0.13.0 release. ### v0.12.0 (2026-01-09) - No normative changes; align spec version with the v0.12.0 release. ### v0.11.0 (2026-01-08) - No normative changes; align spec version with the v0.11.0 release. ### v0.10.0 (2026-01-08) - Require Telegram command menus to include valid project aliases and warn/truncate when exceeding 100 commands. ### v0.9.0 (2026-01-07) - No normative changes; align spec version with the v0.9.0 release. ### v0.8.0 (2026-01-05) - Add `subagent` action kind for agent/task delegation tools. - Add lockfile specification for single-instance enforcement (§10). ### v0.7.0 (2026-01-04) - No normative changes; implementation migrated to structlog and msgspec schemas. ### v0.6.0 (2026-01-03) - No normative changes; added interactive onboarding and lockfile implementation. ### v0.5.0 (2026-01-02) - No normative changes; align spec version with the v0.5.0 release. ### v0.4.0 (2026-01-01) - Add auto-router engine selection by polling all runners to decode resume lines; add configurable default engine for new threads (subcommand overrides default). ### v0.3.0 (2026-01-01) - Require runners to implement explicit resume formatting/extraction/detection and treat runners as authoritative for resume tokens/lines. ### v0.2.0 (2025-12-31) - Initial minimal Takopi specification (Telegram bridge + runner protocol + normalized events + resume support).