Skip to content

Operations

Running milk day to day: memory, observability, loop detection, task tracking, config reload, and graceful degradation.


Memory

milk keeps a Percept store — small remembered facts that survive across sessions, separate from conversation history. Percepts are reinforced when relevant, decay when not, and get promoted to long-term ("core") status over time (NREM consolidation, run at session end).

Commands

Command Action
/learn <statement> Explicitly remember a fact
/memory [global\|session\|<pattern>] List percepts, optionally scoped or filtered
/memory show <pattern or #id> Show full detail for matching percepts
/forget <pattern or #id> Delete matching percepts
/panel memory Toggle the right-side memory panel (open by default)

The #id form accepts a short hex prefix (4–64 chars); the # is optional. The primary agent can also call get_memory, list_memory, and forget_memory directly (same short-ID resolution).

The memory panel is a 34-column right-side panel with SESSION / GLOBAL / GLOBAL (core) sections, polling every 5s. Each percept shows a #<6hex> ID, content wrapped to 2 lines, and a right-aligned weight; percepts updated in the last 60s are highlighted.

Tuning

The knobs that control injection limits, re-injection cadence, and per-agent overrides live in docs/providers.md — Memory configuration and Context budget configuration, since they're usually set per-agent alongside the rest of that agent's config.


Observability

milk exports OpenTelemetry-shaped signals to JSONL files under ~/.milk/otel/ — no external backend required.

Command Action
/metrics Latest value for each metric+label combination
/otel File sizes, record counts, timestamp bounds
/otel trim Archive current files, recreate empty ones
search_signals (tool) Case-insensitive search over the raw JSONL
/usage Token usage report — cumulative, this session, since start — broken down by agent role and model

Config

{
  "otel": {
    "enabled": true,
    "log_level": "DEBUG",
    "log_context": true,
    "traces": true,
    "metrics": true,
    "warn_mb": 50,
    "max_mb": 0,
    "metrics_flush_minutes": 5
  }
}

otel.log_context: true logs the full content of every request payload at DEBUG level to ~/.milk/otel/logs.jsonl (requires log_level: "DEBUG") — covers the claude-cli static/dynamic context files and prompt, the full local/Bedrock inference request body, and subprocess agents' context/prompt temp files.

Raw debug logs

Separate from OTel, three flags capture raw protocol traffic verbatim:

Field Default Writes to
debug_claude_code false ~/.milk/claude_debug.ndjson — every raw NDJSON line from the Claude CLI subprocess
debug_local false ~/.milk/local_debug.log — every raw SSE line from the local/Bedrock agent's HTTP stream, including unparsed/blank lines
debug_subprocess false ~/.milk/subprocess_debug.log — every raw stdout line from subprocess agents (aider, smolagents)

milk otel debug enable turns all of the above on in one command (and prints the paths); milk otel debug disable reverts them.


Loop detection

milk monitors agent output for signs of looping — repeating the same phrase, tool call, or response pattern — to prevent runaway token consumption when nobody notices and interrupts manually. Four complementary systems work together:

Agent-internal detectors (internal/agent/local/)

Operate inside the local agent's tool iteration loop. All share the same recovery flow: crop looping messages from context, inject an escalating recovery nudge (mild → strong), then terminate after max attempts.

Detector File Catches How
Streak tracker loop_streak.go Same reasoning hash or tool-call signature across consecutive iterations SHA-256 of normalised reasoning (truncated to 500 chars, leading phrases stripped)
Streaming n-gram reasoning_ngram.go Periodic reasoning repetition during streaming ("I'm done → let me check → I'm done") Sliding 500-token window, detects blocks of 4+ tokens repeating 10+ times consecutively. Cuts the stream immediately to save tokens.
Text-loop tracker loop_streak.go Same output text across consecutive steps Normalised text (200 chars, leading phrases stripped) compared across steps
Duplicate tool calls local.go Model re-issues a tool call already executed with identical arguments Exact match on tool name + arguments. Nudges first (matching MiMo-Code's approach); terminates after max recovery.

TUI-level detector (internal/loop/detector.go)

Operates at the streaming/TUI layer. Catches patterns the agent-internal trackers can't see:

Signal Scope Catches Default threshold
chunk_repetition Intra-turn Same text repeating consecutively in streaming output 5 occurrences / 50-chunk window
chunk_repetition (scattered) Intra-turn The same chunk recurring within the window without needing to be back-to-back Chunks ≥ 40 runes only, to avoid flagging short boilerplate phrases
reasoning_chunk_flood Intra-turn Too many reasoning chunks without content output 5000 chunks
token_velocity Cross-turn Rapid token consumption without progress 300k tokens / 60s
silent_burn Per-turn High input tokens, near-zero output 20k input tokens
turn_flood Session Excessive turns without user input 10 consecutive non-user turns

Note: consecutive reasoning chunk repetition (SignalReasoningChunkRepetition) was removed from the TUI detector — it is now handled more effectively by the streaming n-gram detector, which cuts the stream immediately instead of just warning.

Try-best detector (internal/loop/try_best.go)

Operates at the tool-execution layer. Catches the most common real-world loops:

Signal Catches Default threshold
edit_repeat Near-identical edits to the same file (Jaccard similarity on normalized diffs) 0.8 similarity × 2 prior matches in window of 12
bash_retry Same failing bash command retried without success 3 consecutive failures
action_streak Non-progressing actions of the same kind (edit or verify) 4 consecutive failures

Intra-turn (the primary case): every streaming chunk passes through a ring buffer of the last 50 chunks, checked two ways — consecutive identical chunks, and (for longer chunks only) the same chunk recurring anywhere in the window without needing adjacency. Either fires at high confidence and auto-interrupts the turn. Cross-turn: after each turn, token velocity, silent burn, and turn count are checked. Tool-level: after each tool call, edit similarity, bash retries, and action streaks are checked.

Status bar shows ⚠ loop — auto-interrupting (high confidence) or ⚠ <signal> (warning); the transcript logs [⚠ loop detected: <signal> (confidence N%)]. A user turn resets all warnings and the turn-flood counter. Works identically across every provider — the intra-turn monitor sits at the TUI layer, not inside any specific agent driver.

{
  "loop_detection": {
    "enabled": true,
    "chunk_repetition_threshold": 5,
    "chunk_window_size": 50,
    "chunk_repetition_min_scattered_length": 40,
    "reasoning_chunk_flood_threshold": 5000,
    "token_velocity_window_seconds": 60,
    "token_velocity_threshold": 300000,
    "max_silent_burn_tokens": 20000,
    "max_consecutive_turns_without_user": 10,
    "auto_interrupt": false
  }
}

Default: detection on, auto_interrupt off (warn only). Set auto_interrupt: true for unattended sessions.


Persistent task tracking

A lightweight task tracker for the primary agent (HTTP/Bedrock backends only — subprocess and claude-cli agents don't receive these tools), stored in ~/.milk/tasks/<session-id>.json (session-scoped) and ~/.milk/tasks/global.json (cross-session, survives restart).

Tool Parameters Returns
create_task title, tags? {"id": "<8-char id>"}
update_task id, status (pending|in_progress|done|blocked), title? "ok"
list_tasks include_global? [{id, title, status, tags}]
complete_task id "ok"
Command Description
/tasks List session + global tasks inline
/task done <id> Mark done (accepts id prefix ≥ 4 chars)
/panel tasks Toggle the tasks side-panel (32 cols), auto-updates as the agent works

Live configuration reload

milk watches ~/.milk/config.json while the TUI is running; a save from another terminal is parsed and applied to in-memory state within ~200ms. /reload forces an immediate re-parse (useful after a symlink swap or atomic editor replace).

On success: [milk] config reloaded. On error: [milk] config reload error: <reason> — the existing in-memory config is kept, nothing crashes.

Hot-reloaded: scalar fields (direct_bash, show_reasoning, sticky_escalation, routing rules, OTel settings, …), agent configs (effective next turn), and MCP server connections (stale connections closed and the toolset rebuilt for affected roles, gated on an actual change — a turn already in flight keeps its original snapshot).

Not hot-reloaded: a running turn's config snapshot, new TurnRunner instances for the agents list (built next turn), and MCP OAuth authorization (still requires the interactive /mcp auth <server> flow).

Recovering from a corrupted config.json

Two safety nets: - While running: the reload path above keeps the last-known-good in-memory config on a parse error — nothing is lost, the session keeps working. - At startup: if config.json fails to parse, milk falls back to config.json.bak (refreshed on every successful save/parse) and starts with a warning instead of refusing to launch. Only a config.json that was never successfully loaded before (no backup exists yet) hard-fails, pointing at milk config open.


Graceful degradation

Primary agent Escalation agent Behavior
up available (any provider) normal routing
down available warn once per session, route everything to escalation
up unavailable/not installed warn once per session, stay primary-only
down unavailable error and exit

Remote oversight (Telegram)

Forward agent activity and permission prompts to a mobile device.

Quick setup (interactive wizard): /setup telegram — paste your bot token from @BotFather, message the bot, milk resolves your chat ID and saves the config automatically.

Manual config (token/chat_id redacted — get these from @BotFather and your first message to the bot):

{
  "remote_oversight": {
    "backend": "telegram",
    "telegram": { "token": "<bot-token-from-botfather>", "chat_id": "<your-numeric-chat-id>" },
    "perm_timeout_secs": 120,
    "timeout_action": "deny",
    "notify_tools": true
  }
}

Enable/disable at runtime (credentials preserved): /setup telegram on / /setup telegram off.

Key Default Description
backend "" "telegram" to enable, "" to disable
perm_timeout_secs 120 Wait time for a remote permission reply before timeout_action
timeout_action "deny" "allow" or "deny" on timeout
notify_tools true Forward tool-call notifications

Forwarded: turn start (agent, target, prompt snippet — workflow turns labeled workflow:<role>), tool calls and results (truncated to 500 chars), response text (capped at 3000 chars, streamed as it's produced), permission prompts with y/n reply (first response from either surface wins).

Remote input: any message sent to the bot is injected as a new turn ([telegram] … in the transcript); queued while a turn is in progress, delivered as the next turn once it completes.


Inline diff view

When an agent calls edit_file/write_file (primary) or Edit/Write (Claude CLI), milk renders a colored inline diff in the transcript right after the tool-hint line — deleted lines in red, added in green, 3 lines of context on each side.

Keyboard shortcuts

Shortcut Action
Enter Submit prompt / accept tab completion
Tab Cycle slash-command and @-path completions
Shift-Tab Reverse cycle completions
Ctrl-C Copy selection → clear input → cancel workflow/turn → quit (double press)
Ctrl-D Quit (when input is empty)
Ctrl-R Search backward in transcript
Ctrl-S Search forward in transcript
Ctrl-Left/Right Word navigation
Shift-Arrows Text selection (transcript and input)
Ctrl-X Cut selected input text