`odek uses a layered configuration system with convention over configuration — opt-in files and environment variables, no mandatory setup.
Each layer overrides the one below it. Unset fields inherit from the layer below:
0. ~/.odek/secrets.env ← Auto-loaded into process environment on startup
1. ~/.odek/config.json ← Global defaults (shared across projects)
2. ./odek.json ← Project-specific overrides
3. ODEK_* env vars ← Runtime/environment overrides
4. CLI flags ← Explicit invocation (highest priority)
Layer 0 is unique: it does not hold config fields directly. Instead it injects
KEY=VALUE pairs into the process environment so they're available for:
- Layer 1–2
${VAR}substitution in config files - Layer 3
ODEK_*env var lookups (e.g.ODEK_API_KEY) - Legacy fallbacks like
DEEPSEEK_API_KEY/OPENAI_API_KEY
Shared across all projects:
{
"model": "deepseek-v4-flash",
"base_url": "https://api.deepseek.com/v1",
"api_key": "${ODEK_API_KEY}",
"thinking": "",
"max_iterations": 90,
"sandbox": false,
"interaction_mode": "engaging",
"no_color": false,
"no_agents": false,
"max_tool_parallel": 4,
"tool_progress": "all",
"tool_progress_cleanup": true,
"system": ""
}Same schema as global. Only set the fields you want to override:
{
"model": "gpt-4o",
"max_iterations": 30
}Security note: The following fields cannot be set in
./odek.jsonbecause a malicious repository could use them to steal secrets, poison the system prompt, disable safety policy, or redirect data to attacker-controlled backends:
base_url— use~/.odek/config.json,ODEK_BASE_URL, or--base-urlapi_key— use~/.odek/config.json,ODEK_API_KEY, or~/.odek/secrets.envsystem— use~/.odek/config.json,ODEK_SYSTEM, or--systemdangerous— use~/.odek/config.jsonembedding/memory/sessions/skills.dirs/skills.embedding/web_search— use~/.odek/config.jsontelegram— use~/.odek/config.jsonorODEK_TELEGRAM_*env varsIf any of these appear in
./odek.json, odek ignores them and prints a warning.
Both files are optional. Missing files are silently ignored. String values support ${VAR} environment variable substitution — useful for API keys without plaintext storage.
Auto-loaded on every odek invocation before any config file or env var is read.
Each KEY=VALUE line is injected into the process environment via os.Setenv.
ODEK_API_KEY=sk-...
GITHUB_TOKEN=ghp_...
Rules:
- File format:
KEY=VALUE— one per line, noexportkeyword needed - Blank lines and
#comments are skipped - Existing env vars are NOT overwritten — if
ODEK_API_KEYis already in the environment, the file is ignored for that key - Missing/unreadable file is silently ignored (not an error)
- Permissions: keep
0600(chmod 600 ~/.odek/secrets.env)
This lets you keep secrets out of config files entirely:
// ~/.odek/config.json — no plaintext secrets
{
"model": "deepseek-v4-flash",
"api_key": "${ODEK_API_KEY}" // ← resolved from secrets.env at runtime
}Every config knob has a ODEK_* counterpart:
| Variable | Maps to | Type |
|---|---|---|
ODEK_MODEL |
--model |
string |
ODEK_BASE_URL |
--base-url |
string |
ODEK_API_KEY |
config files only | string |
ODEK_THINKING |
--thinking |
string |
ODEK_MAX_ITER |
--max-iter |
int |
ODEK_SANDBOX |
--sandbox |
bool |
ODEK_INTERACTION_MODE |
--interaction-mode |
string |
ODEK_NO_COLOR |
--no-color |
bool |
ODEK_NO_AGENTS |
--no-agents |
bool |
ODEK_SYSTEM |
--system |
string |
ODEK_SKILLS_LEARN |
skills.learn |
bool |
ODEK_PROMPT_CACHING |
prompt_caching |
bool |
ODEK_TOOL_PROGRESS |
tool_progress |
string (all|new|verbose|off) |
ODEK_SANDBOX_IMAGE |
--sandbox-image |
string |
ODEK_SANDBOX_NETWORK |
--sandbox-network |
string |
ODEK_SANDBOX_READONLY |
--sandbox-readonly |
bool |
ODEK_SANDBOX_MEMORY |
--sandbox-memory |
string |
ODEK_SANDBOX_CPUS |
--sandbox-cpus |
string |
ODEK_SANDBOX_USER |
--sandbox-user |
string |
ODEK_MAX_TOOL_PARALLEL |
max_tool_parallel |
int |
ODEK_API_KEY → DEEPSEEK_API_KEY → OPENAI_API_KEY
When a model emits multiple tool calls in one response (tool_calls array with N entries), odek executes them concurrently in goroutines bounded by a semaphore.
| Field | Default | Env var | Description |
|---|---|---|---|
max_tool_parallel |
4 |
ODEK_MAX_TOOL_PARALLEL |
Max concurrent tool calls per iteration. 0 = default 4. Set to 1 for sequential execution. |
I/O-bound tools (read_file, search_files, shell) benefit most — latency drops from sum(latencies) to max(latency).
Approval gate: When an approver is configured and the LLM returns multiple tool calls, a single batch approval prompt is shown before any tool executes. If approved, all tools run in parallel. If denied, no tools run.
The skills section controls the skill system:
{
"skills": {
"max_auto_load": 3,
"max_lazy_slots": 5,
"learn": true,
"llm_learn": true,
"llm_curate": true,
"import": {
"max_size_bytes": 1048576,
"timeout_seconds": 5,
"require_https": false
},
"curation": {
"staleness_days": 90,
"auto_prune": false,
"auto_curate": true,
"skip_threshold": 1,
"skip_reset_days": 30
},
"auto_save": {
"enabled": true,
"require_llm": true,
"max_per_run": 3
}
}
}| Field | Env var | Default | Description |
|---|---|---|---|
max_auto_load |
— | 3 | Max skills injected into system prompt on start |
max_lazy_slots |
— | 5 | Max skills loaded per user input via trigger matching |
learn |
ODEK_SKILLS_LEARN |
true |
Enable skill learning mode (detects patterns, suggests skills). On by default |
llm_learn |
— | true |
Use LLM to enrich detected patterns. Template-only — set via odek init, not parsed from JSON at runtime |
llm_curate |
— | true |
Use LLM for curation quality assessment. Template-only — set via odek init, not parsed from JSON at runtime |
dirs |
— | [] | Extra skill directories beyond ~/.odek/skills and ./.odek/skills |
import.max_size_bytes |
— | 1048576 (1MB) | Max size for fetched skill content |
import.timeout_seconds |
— | 5 | HTTP timeout for skill URI fetch |
import.require_https |
— | false | Reject http:// URIs when true |
curation.staleness_days |
— | 90 | Days without use before flagging as stale |
curation.auto_prune |
— | false | Auto-delete stale skills on curate (no prompt) |
curation.auto_curate |
— | true | Run auto-curation after sessions (merge, dedup, prune) |
curation.skip_threshold |
— | 1 | Times a skill must be skipped before permanent suppression |
curation.skip_reset_days |
— | 30 | Days after which a skip expires (re-allows suggestion) |
auto_save.enabled |
— | true | Auto-save quality skill suggestions without prompting |
auto_save.require_llm |
— | true | Only auto-save if LLM enhancement was applied |
auto_save.max_per_run |
— | 3 | Max skills to auto-save per session |
embedding |
— | (inherits top-level embedding) |
Optional override of the shared embedding backend for semantic skill matching. When unset, skills inherit the top-level embedding default with the per-turn query timeout bounded to 2s. See Shared embedding backend. |
The memory section controls the persistent memory system (see docs/MEMORY.md):
{
"memory": {
"enabled": true,
"facts_limit_user": 1500,
"facts_limit_env": 2500,
"buffer_lines": 20,
"buffer_enabled": true,
"merge_on_write": true,
"consolidate_on_end": true,
"extract_on_end": true,
"extract_facts": false,
"llm_search": true,
"llm_extract": true,
"llm_consolidate": true,
"merge_threshold": 0.7,
"add_threshold": 0.3,
"auto_approve_episodes": false,
"episode_dedup_threshold": 0.92,
"max_episodes": 500,
"episode_ttl_days": 0,
"embedding": {
"provider": "http",
"base_url": "http://localhost:11434/v1",
"model": "nomic-embed-text",
"api_key": "${OPENAI_API_KEY}",
"dims": 0,
"timeout_seconds": 10
}
}
}| Field | Default | Description |
|---|---|---|
enabled |
true | Enable memory system entirely |
facts_limit_user |
1500 | Max chars for user.md fact file |
facts_limit_env |
2500 | Max chars for env.md fact file |
buffer_lines |
20 | Max turn summaries in session buffer |
buffer_enabled |
true | Enable the turn-level buffer |
merge_on_write |
true | Use go-vector RP similarity to auto-merge related entries (fast, no LLM — uses simple string merge) |
consolidate_on_end |
true | At session end, run an LLM consolidation pass over user.md and env.md in a background goroutine. This is the quality complement to merge_on_write: merge-on-write handles obvious duplicates immediately (no LLM), while consolidation handles near-duplicates and paraphrases at session end with full LLM quality. Requires llm_consolidate: true. Note: facts in the borderline similarity band (0.3–0.7 cosine) are now always added immediately and only merged by this consolidation pass — if you set consolidate_on_end: false, near-duplicate facts will accumulate rather than being merged. |
extract_on_end |
true | At session end (≥3 turns), extract a narrative episode summary via LLM for later recall |
extract_facts |
false | Opt-in. At session end (≥3 turns), auto-extract a few durable facts (stable user preferences, project invariants) into user.md/env.md. Off by default — see the security note below. Independent of extract_on_end; to disable all end-of-session LLM extraction set llm_extract: false. |
llm_search |
true | Use LLM to rerank candidates for explicit memory search calls (the memory tool). Per-turn recall (FormatEpisodeContext) always uses the cached go-vector index — no LLM call on the hot path regardless of this setting. |
llm_extract |
true | Use LLM for end-of-session fact extraction |
llm_consolidate |
true | Use LLM to merge related fact entries |
merge_threshold |
0.7 | Cosine similarity above which two fact entries are auto-merged without an LLM call (0.0–1.0). Raise it to merge less aggressively; lower it to merge more. |
add_threshold |
0.3 | Cosine similarity below which a new fact entry is auto-added without an LLM call (0.0–1.0). Between add_threshold and merge_threshold the LLM decides. Keep add_threshold < merge_threshold. |
auto_approve_episodes |
false | Security trade-off. When true, untrusted episodes (sessions that touched web/MCP/out-of-workspace content) are auto-approved at session end so they are recalled without a manual odek memory promote. Leaving it false keeps the human review gate (recommended). |
episode_dedup_threshold |
0.92 | Cosine similarity above which a newly written episode is treated as a near-duplicate of an existing one and replaces it (newest wins). An untrusted episode never replaces a trusted/approved one. 0 disables dedup. |
max_episodes |
500 | Maximum number of stored episodes. On each write, episodes beyond this count are evicted oldest-first (both the summary file and the index entry). 0 disables the cap. |
episode_ttl_days |
0 | Evict episodes older than this many days. 0 (default) disables TTL-based eviction. |
embedding |
(inherits top-level embedding) |
Optional override of the embedding backend for episode recall, dedup, the non-LLM episode ranker, and fact merge-on-write. When unset, memory inherits the shared top-level embedding default; if neither is set, local RandomProjections (lexical bag-of-words — fast, zero-cost, but no real semantics). See below. |
By default every similarity computation in memory uses go-vector
RandomProjections: a local, zero-dependency bag-of-words embedder. It is
fast but purely lexical — "fixed the auth bug" and "repaired login issue"
share no tokens and score ~0. Setting embedding.provider to "http" routes
all of those paths through any OpenAI-compatible embeddings API instead
(Ollama, llama.cpp server, LM Studio, vLLM, OpenAI, Voyage…), giving recall
that matches by meaning.
| Field | Default | Description |
|---|---|---|
provider |
"rp" |
"rp" = local RandomProjections; "http" = OpenAI-compatible embeddings API. An "http" config missing base_url or model silently falls back to "rp" so memory keeps working. |
base_url |
— | API root, e.g. http://localhost:11434/v1 (Ollama) or https://api.openai.com/v1. ${ENV_VAR} expansion supported. |
model |
— | Embedding model name, e.g. nomic-embed-text, text-embedding-3-small. |
api_key |
— | Sent as Authorization: Bearer <key> when set. ${ENV_VAR} expansion supported — keep secrets out of config files. |
dims |
0 | Expected vector dimensionality; 0 infers it from the first response (recommended). |
timeout_seconds |
10 | Per-request HTTP timeout. |
Operational notes:
- Per-turn recall stays cheap. Episode vectors live in a persisted index; a
loop turn costs at most one embedding call (the query), bounded by
timeout_seconds. If the backend is down, recall degrades to "no context" and rebuilds back off for 30s — the agent loop is never blocked. The index rebuild that follows a new episode (session-end) embeds the corpus on a fresh client off the index lock, so a slow backend never serializes concurrent recall; it is one batch call over the episode summaries. - Switching backends is safe. The persisted index records which embedding
space it was built in; changing
provider/model/dimsautomatically invalidates it and rebuilds on next use (one batch embedding call). Note: withdims: 0, if a server silently changes a model's output dimensionality (e.g. a model upgrade under the same name) the fingerprint cannot detect it; recall self-heals to "no context" on the dimension mismatch and rebuilds on the next write. Pindimsif you want such a change to force an explicit rebuild. base_urlis an egress target — point it only at a server you trust. Every episode summary and fact entry is POSTed there for embedding. The URL is used verbatim with no allowlist, so do not point it at internal/metadata endpoints (e.g. cloud metadata services) you would not otherwise expose. Prefer a local server (Ollama/llama.cpp) when episode/fact text must not leave the machine.
The same embedder that powers memory also powers semantic session search
(the session_search tool) and semantic skill matching. Set one
top-level embedding block and every subsystem inherits it — one endpoint,
consistent embedding-space semantics everywhere. Each subsystem can still
override the default with its own block. The block uses the same fields as
memory.embedding above (provider/base_url/model/api_key/dims/timeout_seconds).
{
"embedding": {
"provider": "http",
"base_url": "http://localhost:11434/v1",
"model": "nomic-embed-text"
}
}With just that block, memory recall, session_search, and skill matching all go
semantic.
| Subsystem | Inherits the shared embedding? |
Optional override |
|---|---|---|
| Memory | ✅ when memory.embedding is unset |
memory.embedding |
Sessions (session_search) |
✅ when sessions.embedding is unset |
sessions.embedding |
| Skills (lazy matching) | ✅ when skills.embedding is unset (timeout bounded) |
skills.embedding |
Each override is optional and isolated — e.g. point skills at a smaller/faster model while memory uses a higher-quality one:
{
"embedding": { "provider": "http", "base_url": "http://localhost:11434/v1", "model": "nomic-embed-text" },
"skills": { "embedding": { "provider": "http", "base_url": "http://localhost:11434/v1", "model": "all-minilm" } }
}Operational notes:
- Sessions self-heal across backend changes exactly like memory: a
vectors_meta.jsonfingerprint records the embedding space; changingprovider/model/dimsforces a one-time rebuild from the session files. A down backend degradessession_searchto its keyword fallback and backs off for 30s — it never fails a session save. - Skill matching is the hot path — it inherits, but with a bounded timeout.
Skill matching runs on every user turn, so when skills inherit the shared
default their per-turn query embed is capped at 2s (regardless of the
shared
timeout_seconds) and any slow/failed/empty result falls back to the local keyword matcher. An explicitskills.embeddingis respected verbatim — set its owntimeout_secondsif you want a different bound. Memory and sessions are not capped (they embed infrequently and persist their vectors). - The egress warning above applies to every subsystem — session transcripts
and skill text are POSTed to
base_url. Point it only at a server you trust.
When enabled, after each session of ≥3 turns odek asks the LLM to pull a few
durable facts from the conversation — stable user preferences (user.md) and
project/environment invariants (env.md) — so it learns them without you calling
the memory tool. Facts are injected into every system prompt.
Why it is off by default. Turning conversation into always-injected memory is a persistent prompt-injection surface. Several guards apply when it is on:
- It runs only for trusted sessions — a session that ingested untrusted content via tools (web, MCP, out-of-workspace file reads) writes no facts.
- The extractor is instructed to treat the conversation as data, never to act on instructions in it, and never to record "download-and-run" style content.
- A download-and-execute / pipe-to-shell filter drops the obvious exploit class, and the standard injection/credential scan, merge-on-write dedup, and char caps all still apply. A per-session count cap limits how many facts one session adds.
The residual risk these do NOT remove: the trusted-session gate only covers content the agent fetched via tools — it does not cover untrusted text that enters the conversation another way (e.g. you paste an attacker-controlled snippet into a chat that otherwise stayed trusted). Such text is summarized by the extractor and a plausible, non-command fact could still be stored and then injected into every future prompt. This cannot be fully eliminated while the feature is on.
Recommendation. Leave extract_facts: false (the default) on any host that
processes untrusted input. Enable it only in trusted, single-user setups where
you accept the trade-off, and periodically review stored facts with the memory
tool (read) — or remove a bad one with memory remove. To turn off all
end-of-session LLM extraction (episodes and facts), set llm_extract: false.
The subagent section controls task decomposition and parallel sub-agent execution (see docs/SUBAGENTS.md):
{
"subagent": {
"max_concurrency": 3,
"timeout_seconds": 120,
"max_iterations": 15
}
}| Field | Default | Description |
|---|---|---|
max_concurrency |
3 | Max sub-agents running in parallel (max 8) |
timeout_seconds |
120 | Default timeout per sub-agent (overridden by --timeout) |
max_iterations |
15 | Default max think→act cycles per sub-agent (overridden by --max-iter) |
This section is optional. Omitted fields inherit sensible defaults.
Note: The
subagentsection is currently read only fromodek.jsonby theodek subagentcommand in test code. Runtime values (max_concurrency,timeout_seconds) are hardcoded in productionodek run/odek serve. This may be wired up fully in a future release.
Connect to external MCP servers and expose their tools to the agent. Any MCP server that works with Claude Code works with odek — same config format.
{
"mcp_servers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp"]
},
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"]
}
}
}| Field | Description |
|---|---|
command |
The executable to run |
args |
Optional command-line arguments |
env |
Optional environment variable overrides (empty string removes from env) |
Tools are registered as <server_name>__<tool_name> (e.g., playwright__navigate)
and are available in odek run, odek repl, odek continue, and odek serve.
See docs/MCP.md for detailed instructions.
The telegram section configures the Telegram bot integration and the --deliver flag.
{
"telegram": {
"bot_token": "8610437446:AAElHFJ...",
"allowed_users": [8592463065],
"allowed_chats": [],
"poll_interval": 1,
"poll_timeout": 30,
"max_msg_length": 4096,
"max_download_size": 5242880,
"media_quota_per_chat": 52428800,
"session_ttl_hours": 24,
"log_level": "info",
"log_file": "",
"default_chat_id": 8592463065
}
}| Field | Env var | Default | Description |
|---|---|---|---|
bot_token |
ODEK_TELEGRAM_BOT_TOKEN |
— (required) | Telegram bot API token from @BotFather |
allowed_users |
— | all | Restrict bot to specific user IDs |
allowed_chats |
— | all | Restrict bot to specific chat IDs |
poll_interval |
— | 1 | Seconds between poll cycles |
poll_timeout |
— | 30 | Long-poll timeout (1-60 seconds) |
max_msg_length |
— | 4096 | Max characters per message |
session_ttl_hours |
— | 24 | Hours before inactive session expires |
max_download_size |
ODEK_TELEGRAM_MAX_DOWNLOAD_SIZE |
5242880 (5 MiB) | Per-file byte cap for Telegram voice/photo/document downloads. Set to -1 to disable. |
media_quota_per_chat |
ODEK_TELEGRAM_MEDIA_QUOTA_PER_CHAT |
0 (disabled) | Total bytes of downloaded media allowed per chat. 0 disables the quota. |
log_level |
— | info | Log level: debug, info, warn, error |
log_file |
— | stderr | Log file path (empty = stderr) |
default_chat_id |
— | 0 | Required for --deliver — numeric chat ID where odek run --deliver sends results. Get this from your bot's update or use a tool like @userinfobot. |
The --deliver flag on odek run sends the agent's final response to the configured
default_chat_id as a plain text message. This enables cron-based scheduled agent
workflows — no daemon needed.
# Run an agent task and deliver the result to Telegram
odek run --deliver "Check the CI pipeline status"
# Works with task text first too
odek run "Daily summary" --deliverSee docs/TELEGRAM.md for full cron setup instructions.
Configures the native in-process task scheduler (odek schedule). Job
definitions live in ~/.odek/schedules.json; this section only tunes the
engine. Every field has an ODEK_SCHEDULES_* environment override.
{
"schedules": {
"enabled": true,
"max_concurrent": 2,
"timezone": "UTC",
"catchup": false,
"allow_telegram_management": true,
"telegram_admin_chats": [123456789],
"telegram_admin_users": [987654321]
}
}| Field | Env | Default | Description |
|---|---|---|---|
enabled |
ODEK_SCHEDULES_ENABLED |
true |
Run the embedded scheduler inside odek telegram. Set false to run only a standalone odek schedule daemon. |
max_concurrent |
ODEK_SCHEDULES_MAX_CONCURRENT |
2 |
Maximum scheduled jobs running at once. |
timezone |
ODEK_SCHEDULES_TIMEZONE |
UTC |
Default timezone for jobs that don't set their own --tz. |
catchup |
ODEK_SCHEDULES_CATCHUP |
false |
Global default for the missed-run policy: run a missed fire once on startup. |
allow_telegram_management |
ODEK_SCHEDULES_ALLOW_TELEGRAM_MANAGEMENT |
true |
Allow the Telegram /schedule commands to create/remove/toggle/run jobs. When false, the bot still lists and previews jobs but mutations must go through odek schedule. |
telegram_admin_chats |
ODEK_SCHEDULES_TELEGRAM_ADMIN_CHATS |
[] |
Comma-separated list of operator chat IDs. These IDs may use mutating /schedule commands and /restart. When empty, the bot falls back to telegram.default_chat_id. Read-only commands are unaffected. |
telegram_admin_users |
ODEK_SCHEDULES_TELEGRAM_ADMIN_USERS |
[] |
Comma-separated list of operator user IDs. These IDs may use mutating /schedule commands and /restart. Read-only commands are unaffected. |
dangerous |
see below | {} |
Schedule-specific override for the dangerous-operations policy. |
Scheduled jobs run unattended, so by default the scheduler denies any class that would require an approval prompt (network_egress, system_write, code_execution, install, unknown). You can override this for cron jobs without widening the policy for interactive CLI/REPL/WebUI use.
{
"schedules": {
"dangerous": {
"classes": {
"network_egress": "allow",
"system_write": "allow"
},
"allowlist": ["curl -s https://example.com/feed.xml"]
}
}
}Environment overrides:
| Env | Format |
|---|---|
ODEK_SCHEDULES_DANGEROUS_CLASSES |
JSON object, e.g. {"network_egress":"allow","system_write":"allow"} |
ODEK_SCHEDULES_DANGEROUS_ALLOWLIST |
Comma-separated command strings |
ODEK_SCHEDULES_DANGEROUS_DENYLIST |
Comma-separated command strings |
ODEK_SCHEDULES_DANGEROUS_ACTION |
Global default action: allow, deny, or prompt |
ODEK_SCHEDULES_DANGEROUS_NON_INTERACTIVE |
allow, deny, or prompt (ignored: scheduled runs force deny) |
Safety floor that cannot be overridden:
non_interactiveis alwaysdeny(no human is present to approve).destructiveandblockedclasses are always denied.
Project-level odek.json cannot set schedules.dangerous; configure it via ~/.odek/config.json or environment variables.
Full guide: docs/SCHEDULES.md.
Controls how per-tool progress messages appear inside the Telegram bot during agent runs. Independent from interaction_mode — you can have engaging terminal output with minimal Telegram progress, or verbose terminal with rich progress bubbles.
{
"tool_progress": "all",
"tool_progress_cleanup": true
}| Value | Behavior | Use case |
|---|---|---|
"all" (default) |
Single editable progress bubble with smart previews — e.g. 📝 read_file: "main.go". Includes edit throttling (1.5s), tool dedup (×N counter for repeated same-tool), and automatic flood-control fallback |
General use — shows what the agent is doing without spamming the chat |
"new" |
Same as "all" but only updates when the tool name changes. Consecutive read_file calls produce one line; a shell call starts a new line |
Long-running agents with repetitive tool chains (e.g. reading 50 files in batch) |
"verbose" |
Raw tool arguments as separate messages. Each tool call sends a new message with full JSON args; on completion the result is sent as a new message ✅ (12ms, 2KB) including execution latency and result size |
Debugging — see exactly what the agent passes to each tool and how long it takes |
"off" |
No per-tool progress messages at all. Only the initial "🤔 Looking into that..." and final answer are shown | Privacy-sensitive contexts or users who prefer zero noise |
Default: true. Controls whether the progress message bubble is deleted after the agent's final answer arrives:
true— delete the progress bubble (clean chat, no stale tool traces)false— keep the progress bubble as a breadcrumb of what the agent did
The progress system is an evolving single message that gets edited in-place (similar to an animated status). Each tool call adds a line like:
📝 read_file: "main.go"
💻 shell: "npm test"
📝 read_file: "utils.go" (×3)
Key behaviors:
- Smart previews — instead of showing raw JSON args, the system extracts meaningful context: filename for file tools, the command text for shell, URL for browser, query text for memory/search tools, audio filename for transcribe, file path for vision, query for web_search
- Edit throttling — edits are rate-limited to one every 1.5 seconds to avoid hitting Telegram's flood control limits. Rapid tool chains don't produce 429 errors
- Tool dedup — when the same tool runs consecutively (common with parallel batch tools like
batch_read), identical lines are collapsed into a(×N)counter instead of repeating N times - Flood control fallback — if an edit message fails with "flood" or "retry after", the system automatically switches to sending new messages instead of editing. This prevents the bot from becoming unresponsive under heavy load
- Content reset — when the agent calls
send_messagemid-run to send an interim message, the progress bubble resets below that content, keeping the chat timeline in correct order
Create a config file template:
# Local project config (./odek.json)
odek init
# Global config (~/.odek/config.json)
odek init --global
# Overwrite existing file
odek init --force# Set API key via secrets.env (recommended — keeps secrets out of config files)
echo 'ODEK_API_KEY="sk-..."' >> ~/.odek/secrets.env
chmod 600 ~/.odek/secrets.env
# Global config (model and other settings only, no secrets)
echo '{"model": "deepseek-v4-flash"}' > ~/.odek/config.json
odek run "list files"
# Per-project override
echo '{"max_iterations": 30}' > ./odek.json
odek run "quick status"
# Env var override for one-off
ODEK_SANDBOX=true odek run "run untrusted script"
# Enable skill learning via env var
ODEK_SKILLS_LEARN=true odek run "set up CI"
# Sub-agent config (project-level)
echo '{"subagent": {"max_concurrency": 5, "timeout_seconds": 300}}' > ./odek.json
# CLI flag always wins
odek run --model gpt-4o --base-url https://api.openai.com/v1 "task"